iCode-TUI 0.29.1__py3-none-any.whl
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.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Cross-session mutation coordination registry.
|
|
4
|
+
|
|
5
|
+
Resolves the chrys-vs-chrys attribution case: N chrys sessions working in
|
|
6
|
+
the same working tree, each publishing what it *provably* wrote, each
|
|
7
|
+
subtracting peers' proven writes from its own window inferences.
|
|
8
|
+
|
|
9
|
+
Storage layout::
|
|
10
|
+
|
|
11
|
+
{session_root}/attribution/{repo_key}/{session_id}.json
|
|
12
|
+
|
|
13
|
+
- ``repo_key`` = first 16 hex chars of sha256 over the canonicalized
|
|
14
|
+
working-tree root (``.git`` directory *or* file — linked worktrees and
|
|
15
|
+
submodules resolve to their own tree). Outside any repo, a path keys
|
|
16
|
+
by its own directory — a pure function of the path, so independent
|
|
17
|
+
sessions land in the same tree. Keying by tree root is for
|
|
18
|
+
*discovery* only; matching itself is path-exact.
|
|
19
|
+
- One file per (tree, session), **single writer** (the owning session),
|
|
20
|
+
replaced atomically. Readers tolerate missing/partial/undecodable
|
|
21
|
+
files by skipping them — no cross-process locks. Within the session
|
|
22
|
+
the registry is in-memory state behind one lock and the file is its
|
|
23
|
+
serialization (no read-modify-write of the file itself).
|
|
24
|
+
- Files outlive their session: clean shutdown stamps ``closed_at`` but
|
|
25
|
+
does not delete (late rollbacks in other sessions still need the
|
|
26
|
+
claims). Removal happens only via opportunistic GC — on write,
|
|
27
|
+
peer files whose mtime is older than :data:`REGISTRY_GC_AGE_SECONDS`
|
|
28
|
+
are deleted.
|
|
29
|
+
|
|
30
|
+
Reclassification (read side) is **retroactive, idempotent, and runs at
|
|
31
|
+
every opportunity** — own finalize, pre-summary display refresh, and
|
|
32
|
+
rollback-plan construction. Matching uses write *intervals* (never
|
|
33
|
+
point timestamps) on *canonical* path keys (``realpath`` +
|
|
34
|
+
``normcase``). Conflict rules per overlapping path:
|
|
35
|
+
|
|
36
|
+
========== ============================== ===========================
|
|
37
|
+
local row evidence result
|
|
38
|
+
========== ============================== ===========================
|
|
39
|
+
ASSUMED content identity demote to FOREIGN
|
|
40
|
+
ASSUMED overlap without identity stays ASSUMED, contested
|
|
41
|
+
PROVEN any overlap stays PROVEN, contested
|
|
42
|
+
========== ============================== ===========================
|
|
43
|
+
|
|
44
|
+
Content identity = hashes present and equal, or both sides gone.
|
|
45
|
+
Skip-marked sides and MOVEs never demote (they fall to ``contested``).
|
|
46
|
+
Coordination must never break tool execution: every filesystem failure
|
|
47
|
+
is swallowed and logged at debug level.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import contextlib
|
|
53
|
+
import hashlib
|
|
54
|
+
import json
|
|
55
|
+
import logging
|
|
56
|
+
import os
|
|
57
|
+
import threading
|
|
58
|
+
import time
|
|
59
|
+
import uuid
|
|
60
|
+
from dataclasses import dataclass, field
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from typing import TYPE_CHECKING, Any
|
|
63
|
+
|
|
64
|
+
from chrys.foundation.branding import APP_DISPLAY_NAME
|
|
65
|
+
from chrys.foundation.platform import get_platform
|
|
66
|
+
from chrys.foundation.platform.files import atomic_write_text
|
|
67
|
+
from chrys.service.mutations.types import (
|
|
68
|
+
FileMutation,
|
|
69
|
+
MutationProvenance,
|
|
70
|
+
RollbackExclusionReason,
|
|
71
|
+
RollbackPlan,
|
|
72
|
+
RollbackWarning,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if TYPE_CHECKING:
|
|
76
|
+
from collections.abc import Callable, Iterable
|
|
77
|
+
|
|
78
|
+
from chrys.service.mutations.tracker import MutationTracker
|
|
79
|
+
from chrys.service.mutations.types import MutationLog
|
|
80
|
+
|
|
81
|
+
logger = logging.getLogger(__name__)
|
|
82
|
+
|
|
83
|
+
REGISTRY_SCHEMA = 1
|
|
84
|
+
MAX_CLAIMS = 1000
|
|
85
|
+
REGISTRY_GC_AGE_SECONDS = 7 * 24 * 60 * 60
|
|
86
|
+
ATTRIBUTION_DIR_NAME = "attribution"
|
|
87
|
+
|
|
88
|
+
# In-flight peer warning (RollbackWarning.code).
|
|
89
|
+
PEER_ACTIVE_WINDOW_WARNING = "peer_active_window"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Path canonicalization and working-tree discovery
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def fold_case(text: str) -> str:
|
|
98
|
+
"""The case fold ``canonical_key`` applies — usable on path fragments.
|
|
99
|
+
|
|
100
|
+
``normcase`` folds on Windows ONLY — darwin needs the explicit fold,
|
|
101
|
+
since default APFS/HFS+ volumes are case-insensitive too. (On a
|
|
102
|
+
case-SENSITIVE mac volume case twins then merely share a registry
|
|
103
|
+
namespace; matching still demands interval overlap plus content
|
|
104
|
+
identity — the same trade Windows makes for case-sensitive NTFS
|
|
105
|
+
directories.)
|
|
106
|
+
"""
|
|
107
|
+
folded = os.path.normcase(text)
|
|
108
|
+
if get_platform().is_macos:
|
|
109
|
+
folded = folded.lower()
|
|
110
|
+
return folded
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def canonical_key(path: str) -> str:
|
|
114
|
+
"""Canonical matching key: ``realpath`` + case folding.
|
|
115
|
+
|
|
116
|
+
The codebase's usual ``abspath``+``normpath`` misses case-variant
|
|
117
|
+
launches (Windows, default macOS volumes) and symlinked routes; a
|
|
118
|
+
miss only costs a demotion, but it is cheap to avoid.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
real = os.path.realpath(path)
|
|
122
|
+
except OSError:
|
|
123
|
+
real = os.path.abspath(path)
|
|
124
|
+
return fold_case(real)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def resolve_tree_root(path: str) -> str | None:
|
|
128
|
+
"""Nearest ancestor whose ``.git`` exists — directory *or* file.
|
|
129
|
+
|
|
130
|
+
File paths are normalized to their containing directory before the
|
|
131
|
+
ancestor walk. Linked worktrees/submodules pair up by their own
|
|
132
|
+
``.git`` file, so they stay separate from their parent repository.
|
|
133
|
+
"""
|
|
134
|
+
current = os.path.abspath(path)
|
|
135
|
+
if not os.path.isdir(current):
|
|
136
|
+
current = os.path.dirname(current)
|
|
137
|
+
while True:
|
|
138
|
+
if os.path.exists(os.path.join(current, ".git")):
|
|
139
|
+
return current
|
|
140
|
+
parent = os.path.dirname(current)
|
|
141
|
+
if parent == current:
|
|
142
|
+
return None
|
|
143
|
+
current = parent
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def repo_key_for_root(tree_root: str) -> str:
|
|
147
|
+
"""First 16 hex chars of sha256 over the canonicalized tree root."""
|
|
148
|
+
return hashlib.sha256(canonical_key(tree_root).encode("utf-8")).hexdigest()[:16]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_pid_alive(pid: int) -> bool:
|
|
152
|
+
"""Cross-platform liveness probe.
|
|
153
|
+
|
|
154
|
+
A peer whose recorded pid is no longer alive counts as closed —
|
|
155
|
+
crash leftovers must not warn until GC.
|
|
156
|
+
"""
|
|
157
|
+
if pid <= 0:
|
|
158
|
+
return False
|
|
159
|
+
import psutil
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
return psutil.pid_exists(pid)
|
|
163
|
+
except Exception: # pragma: no cover - psutil failure → assume alive (conservative)
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _strict_overlap(a0: float, a1: float, b0: float, b1: float) -> bool:
|
|
168
|
+
"""Strict interval overlap — no slack, touching endpoints don't pair.
|
|
169
|
+
|
|
170
|
+
Both intervals are conservative supersets of the true write instant
|
|
171
|
+
taken from the same machine's wall clock, so a genuinely in-window
|
|
172
|
+
peer write always overlaps strictly; slack could only manufacture
|
|
173
|
+
pairings that never happened.
|
|
174
|
+
"""
|
|
175
|
+
return a0 < b1 and b0 < a1
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Registry records
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class MutationClaim:
|
|
185
|
+
"""One published proven write: path identity, after-state, write interval.
|
|
186
|
+
|
|
187
|
+
``key``/``old_key`` are canonical (:func:`canonical_key`) forms
|
|
188
|
+
carried alongside the display paths. ``after_exists`` is skip-aware,
|
|
189
|
+
mirroring ``FileMutation``: it distinguishes deletion from a
|
|
190
|
+
policy-skipped backup — both read ``after_hash: null``.
|
|
191
|
+
``[t0, t1]`` bounds when the disk write actually occurred.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
path: str
|
|
195
|
+
key: str
|
|
196
|
+
after_hash: str | None
|
|
197
|
+
after_exists: bool
|
|
198
|
+
t0: float
|
|
199
|
+
t1: float
|
|
200
|
+
source: str = ""
|
|
201
|
+
old_path: str | None = None
|
|
202
|
+
old_key: str | None = None
|
|
203
|
+
|
|
204
|
+
def to_dict(self) -> dict[str, Any]:
|
|
205
|
+
d: dict[str, Any] = {
|
|
206
|
+
"path": self.path,
|
|
207
|
+
"key": self.key,
|
|
208
|
+
"after_hash": self.after_hash,
|
|
209
|
+
"after_exists": self.after_exists,
|
|
210
|
+
"t0": self.t0,
|
|
211
|
+
"t1": self.t1,
|
|
212
|
+
"source": self.source,
|
|
213
|
+
}
|
|
214
|
+
if self.old_path is not None:
|
|
215
|
+
d["old_path"] = self.old_path
|
|
216
|
+
if self.old_key is not None:
|
|
217
|
+
d["old_key"] = self.old_key
|
|
218
|
+
return d
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def from_dict(cls, d: dict[str, Any]) -> MutationClaim:
|
|
222
|
+
return cls(
|
|
223
|
+
path=str(d["path"]),
|
|
224
|
+
key=str(d.get("key") or canonical_key(str(d["path"]))),
|
|
225
|
+
after_hash=d.get("after_hash"),
|
|
226
|
+
after_exists=bool(d.get("after_exists", d.get("after_hash") is not None)),
|
|
227
|
+
t0=float(d["t0"]),
|
|
228
|
+
t1=float(d["t1"]),
|
|
229
|
+
source=str(d.get("source", "")),
|
|
230
|
+
old_path=d.get("old_path"),
|
|
231
|
+
old_key=d.get("old_key"),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass
|
|
236
|
+
class ObservationWindow:
|
|
237
|
+
"""One advertised observation window (shell / implicit tool call).
|
|
238
|
+
|
|
239
|
+
Opened in ``prepare_mutation_tracking`` (``end: None``) *before* any
|
|
240
|
+
observation (git capture, pre-scan, snapshots) so the advertised
|
|
241
|
+
window is a superset of the observation; closed in finalize. Peers
|
|
242
|
+
use still-open windows for the in-flight rollback warning.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
id: str
|
|
246
|
+
scope: list[str]
|
|
247
|
+
start: float
|
|
248
|
+
end: float | None = None
|
|
249
|
+
|
|
250
|
+
def to_dict(self) -> dict[str, Any]:
|
|
251
|
+
return {"id": self.id, "scope": list(self.scope), "start": self.start, "end": self.end}
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def from_dict(cls, d: dict[str, Any]) -> ObservationWindow:
|
|
255
|
+
return cls(
|
|
256
|
+
id=str(d.get("id", "")),
|
|
257
|
+
scope=[str(s) for s in d.get("scope", [])],
|
|
258
|
+
start=float(d["start"]),
|
|
259
|
+
end=None if d.get("end") is None else float(d["end"]),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass
|
|
264
|
+
class PeerRegistry:
|
|
265
|
+
"""Parsed peer registry file (tolerant read)."""
|
|
266
|
+
|
|
267
|
+
session_id: str
|
|
268
|
+
pid: int
|
|
269
|
+
closed_at: float | None
|
|
270
|
+
windows: list[ObservationWindow]
|
|
271
|
+
claims: list[MutationClaim]
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def is_active(self) -> bool:
|
|
275
|
+
"""Open for business: not cleanly closed AND its process is alive."""
|
|
276
|
+
return self.closed_at is None and _is_pid_alive(self.pid)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def parse_registry_payload(payload: str) -> PeerRegistry | None:
|
|
280
|
+
"""Parse one registry file's text; ``None`` on any structural problem.
|
|
281
|
+
|
|
282
|
+
Readers must tolerate missing/partial/undecodable files by skipping
|
|
283
|
+
them — a peer may be mid-replace or of a future schema.
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
data = json.loads(payload)
|
|
287
|
+
if not isinstance(data, dict) or int(data.get("schema", 0)) != REGISTRY_SCHEMA:
|
|
288
|
+
return None
|
|
289
|
+
closed_raw = data.get("closed_at")
|
|
290
|
+
windows = [ObservationWindow.from_dict(w) for w in data.get("windows", [])]
|
|
291
|
+
claims = [MutationClaim.from_dict(c) for c in data.get("claims", [])]
|
|
292
|
+
return PeerRegistry(
|
|
293
|
+
session_id=str(data.get("session_id", "")),
|
|
294
|
+
pid=int(data.get("pid", 0)),
|
|
295
|
+
closed_at=None if closed_raw is None else float(closed_raw),
|
|
296
|
+
windows=windows,
|
|
297
|
+
claims=claims,
|
|
298
|
+
)
|
|
299
|
+
except Exception:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass
|
|
304
|
+
class _TreeState:
|
|
305
|
+
"""In-memory single-writer state for one (tree, session) file."""
|
|
306
|
+
|
|
307
|
+
windows: dict[str, ObservationWindow] = field(default_factory=dict)
|
|
308
|
+
claims: list[MutationClaim] = field(default_factory=list)
|
|
309
|
+
loaded_existing: bool = False
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
# Coordinator
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class MutationCoordinator:
|
|
318
|
+
"""Owns cross-session attribution for one session.
|
|
319
|
+
|
|
320
|
+
Built alongside the :class:`MutationTracker` at engine construction
|
|
321
|
+
(it needs session identity and settings the tracker never sees);
|
|
322
|
+
the tracker itself stays coordination-unaware. All methods are
|
|
323
|
+
thread-safe (pipeline calls arrive from executor threads) and all
|
|
324
|
+
registry I/O failures degrade to a no-op with a debug log.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
def __init__(
|
|
328
|
+
self,
|
|
329
|
+
*,
|
|
330
|
+
registry_root: str | Path,
|
|
331
|
+
session_id: str,
|
|
332
|
+
enabled: bool = True,
|
|
333
|
+
pid: int | None = None,
|
|
334
|
+
now: Callable[[], float] = time.time,
|
|
335
|
+
max_claims: int = MAX_CLAIMS,
|
|
336
|
+
gc_age_seconds: float = REGISTRY_GC_AGE_SECONDS,
|
|
337
|
+
) -> None:
|
|
338
|
+
self._root = Path(registry_root)
|
|
339
|
+
self._session_id = session_id
|
|
340
|
+
self._enabled = enabled
|
|
341
|
+
self._pid = os.getpid() if pid is None else pid
|
|
342
|
+
self._now = now
|
|
343
|
+
self._max_claims = max_claims
|
|
344
|
+
self._gc_age = gc_age_seconds
|
|
345
|
+
self._lock = threading.Lock()
|
|
346
|
+
# repo_key -> single-writer state (this session's own file).
|
|
347
|
+
self._states: dict[str, _TreeState] = {}
|
|
348
|
+
# window id -> repo_keys the window was published under.
|
|
349
|
+
self._window_trees: dict[str, list[str]] = {}
|
|
350
|
+
# Directory (canonical) -> resolved tree root; cheap memo for the
|
|
351
|
+
# hot path (every mutation path resolves its tree on publish and
|
|
352
|
+
# reclassify).
|
|
353
|
+
self._tree_cache: dict[str, str | None] = {}
|
|
354
|
+
# Short-circuit signatures for reclassify: peer-dir signature per
|
|
355
|
+
# repo_key + local log signature at the last completed pass.
|
|
356
|
+
self._last_peer_sig: dict[str, tuple] = {}
|
|
357
|
+
self._last_local_sig: tuple | None = None
|
|
358
|
+
self._unsaved_reclassification = False
|
|
359
|
+
self._closed_at: float | None = None
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def enabled(self) -> bool:
|
|
363
|
+
return self._enabled
|
|
364
|
+
|
|
365
|
+
# -- Tree routing -------------------------------------------------
|
|
366
|
+
|
|
367
|
+
def _tree_root_for(self, path: str, fallback_root: str | None) -> str | None:
|
|
368
|
+
# Window scope paths are directories; claim paths are files.
|
|
369
|
+
absolute = os.path.abspath(path)
|
|
370
|
+
directory = absolute if os.path.isdir(absolute) else (os.path.dirname(absolute) or absolute)
|
|
371
|
+
cache_key = os.path.normcase(directory)
|
|
372
|
+
if cache_key not in self._tree_cache:
|
|
373
|
+
self._tree_cache[cache_key] = resolve_tree_root(directory)
|
|
374
|
+
root = self._tree_cache[cache_key]
|
|
375
|
+
if root is not None:
|
|
376
|
+
return root
|
|
377
|
+
# Non-git paths key by their OWN directory, never the caller's
|
|
378
|
+
# fallback root: the registry key must be a pure function of
|
|
379
|
+
# the path — sessions with different primary workspaces touching
|
|
380
|
+
# the same untracked directory must land in the same tree or
|
|
381
|
+
# they never see each other's claims. ``fallback_root`` only
|
|
382
|
+
# gates WHETHER untracked paths participate (None → the caller
|
|
383
|
+
# has no workspace context → they don't).
|
|
384
|
+
if fallback_root:
|
|
385
|
+
return directory
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
def _repo_keys_for_paths(self, paths: Iterable[str], fallback_root: str | None) -> dict[str, str]:
|
|
389
|
+
"""Map repo_key -> tree root for every resolvable path."""
|
|
390
|
+
keys: dict[str, str] = {}
|
|
391
|
+
for p in paths:
|
|
392
|
+
root = self._tree_root_for(p, fallback_root)
|
|
393
|
+
if root is not None:
|
|
394
|
+
keys.setdefault(repo_key_for_root(root), root)
|
|
395
|
+
return keys
|
|
396
|
+
|
|
397
|
+
# -- Own-file serialization ----------------------------------------
|
|
398
|
+
|
|
399
|
+
def _tree_dir(self, repo_key: str) -> Path:
|
|
400
|
+
return self._root / repo_key
|
|
401
|
+
|
|
402
|
+
def _own_file(self, repo_key: str) -> Path:
|
|
403
|
+
return self._tree_dir(repo_key) / f"{self._session_id}.json"
|
|
404
|
+
|
|
405
|
+
def _state_for(self, repo_key: str) -> _TreeState:
|
|
406
|
+
"""Get (lazily creating) the in-memory state for one tree.
|
|
407
|
+
|
|
408
|
+
On first touch, merge any existing file left by a previous
|
|
409
|
+
process of this session (resume): claims are kept — peers'
|
|
410
|
+
late rollbacks still need them — and stale open windows are
|
|
411
|
+
closed at the file's mtime (the last instant that process
|
|
412
|
+
provably lived); the dead pid would already read as closed, but
|
|
413
|
+
the file is about to be re-stamped with *our* live pid.
|
|
414
|
+
"""
|
|
415
|
+
state = self._states.get(repo_key)
|
|
416
|
+
if state is not None:
|
|
417
|
+
return state
|
|
418
|
+
state = _TreeState()
|
|
419
|
+
own = self._own_file(repo_key)
|
|
420
|
+
try:
|
|
421
|
+
if own.exists():
|
|
422
|
+
stamp = own.stat().st_mtime
|
|
423
|
+
prior = parse_registry_payload(own.read_text(encoding="utf-8"))
|
|
424
|
+
if prior is not None:
|
|
425
|
+
state.claims = prior.claims[-self._max_claims :]
|
|
426
|
+
for w in prior.windows:
|
|
427
|
+
if w.end is None:
|
|
428
|
+
w.end = max(w.start, stamp)
|
|
429
|
+
state.windows[w.id] = w
|
|
430
|
+
state.loaded_existing = True
|
|
431
|
+
except OSError:
|
|
432
|
+
logger.debug("Coordination: could not merge prior registry file %s", own, exc_info=True)
|
|
433
|
+
self._states[repo_key] = state
|
|
434
|
+
return state
|
|
435
|
+
|
|
436
|
+
def _write_state(self, repo_key: str) -> None:
|
|
437
|
+
state = self._state_for(repo_key)
|
|
438
|
+
payload = {
|
|
439
|
+
"session_id": self._session_id,
|
|
440
|
+
"pid": self._pid,
|
|
441
|
+
"schema": REGISTRY_SCHEMA,
|
|
442
|
+
# Sticky close stamp: a straggler racing close() (an
|
|
443
|
+
# in-flight finalize's close_window / publish_claims) must
|
|
444
|
+
# not revive the file as open — close() itself never
|
|
445
|
+
# re-stamps (idempotence guard), so the stamp would be lost
|
|
446
|
+
# for good and peers would read a live pid as "active".
|
|
447
|
+
"closed_at": self._closed_at,
|
|
448
|
+
"windows": [w.to_dict() for w in state.windows.values()],
|
|
449
|
+
"claims": [c.to_dict() for c in state.claims],
|
|
450
|
+
}
|
|
451
|
+
try:
|
|
452
|
+
self._tree_dir(repo_key).mkdir(parents=True, exist_ok=True)
|
|
453
|
+
atomic_write_text(self._own_file(repo_key), json.dumps(payload))
|
|
454
|
+
except OSError:
|
|
455
|
+
logger.debug("Coordination: registry write failed for %s", repo_key, exc_info=True)
|
|
456
|
+
return
|
|
457
|
+
self._gc_tree(repo_key)
|
|
458
|
+
|
|
459
|
+
def _gc_tree(self, repo_key: str) -> None:
|
|
460
|
+
"""Opportunistic on-write GC: drop peer files older than the age cap."""
|
|
461
|
+
cutoff = self._now() - self._gc_age
|
|
462
|
+
own_name = f"{self._session_id}.json"
|
|
463
|
+
try:
|
|
464
|
+
with os.scandir(self._tree_dir(repo_key)) as it:
|
|
465
|
+
for entry in it:
|
|
466
|
+
if entry.name == own_name or not entry.name.endswith(".json"):
|
|
467
|
+
continue
|
|
468
|
+
with contextlib.suppress(OSError):
|
|
469
|
+
if entry.stat().st_mtime < cutoff:
|
|
470
|
+
os.unlink(entry.path)
|
|
471
|
+
except OSError:
|
|
472
|
+
pass
|
|
473
|
+
|
|
474
|
+
# -- Write side ------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
def open_window(self, scope_paths: list[str], *, fallback_root: str | None = None) -> str | None:
|
|
477
|
+
"""Advertise an observation window; returns its id (None when disabled).
|
|
478
|
+
|
|
479
|
+
Must be called as the *first* operation of an observation
|
|
480
|
+
(before git capture, shell pre-scan, or any snapshot) so the
|
|
481
|
+
advertised window is a superset — a peer write in a pre-scan
|
|
482
|
+
gap would otherwise be attributed by the diff yet never overlap
|
|
483
|
+
our interval.
|
|
484
|
+
"""
|
|
485
|
+
if not self._enabled or not scope_paths:
|
|
486
|
+
return None
|
|
487
|
+
window = ObservationWindow(
|
|
488
|
+
id=uuid.uuid4().hex, scope=[os.path.abspath(p) for p in scope_paths], start=self._now()
|
|
489
|
+
)
|
|
490
|
+
with self._lock:
|
|
491
|
+
keys = self._repo_keys_for_paths(scope_paths, fallback_root)
|
|
492
|
+
if not keys:
|
|
493
|
+
return None
|
|
494
|
+
self._window_trees[window.id] = list(keys)
|
|
495
|
+
for repo_key in keys:
|
|
496
|
+
self._state_for(repo_key).windows[window.id] = window
|
|
497
|
+
self._write_state(repo_key)
|
|
498
|
+
return window.id
|
|
499
|
+
|
|
500
|
+
def close_window(self, window_id: str | None) -> None:
|
|
501
|
+
"""Stamp the window's end and republish the affected tree files."""
|
|
502
|
+
if not self._enabled or not window_id:
|
|
503
|
+
return
|
|
504
|
+
with self._lock:
|
|
505
|
+
end = self._now()
|
|
506
|
+
for repo_key in self._window_trees.pop(window_id, []):
|
|
507
|
+
window = self._states[repo_key].windows.get(window_id)
|
|
508
|
+
if window is not None:
|
|
509
|
+
window.end = end
|
|
510
|
+
self._write_state(repo_key)
|
|
511
|
+
|
|
512
|
+
def publish_claims(self, mutations: list[FileMutation], *, fallback_root: str | None = None) -> int:
|
|
513
|
+
"""Publish claims for the eligible subset of ``mutations``.
|
|
514
|
+
|
|
515
|
+
Eligibility is decided here for the *evidence* conditions
|
|
516
|
+
(proven provenance, provable net change); the *authorship*
|
|
517
|
+
condition — did the tool actually execute and succeed for
|
|
518
|
+
write/edit — is the caller's to enforce by not passing such
|
|
519
|
+
mutations at all (the pipeline sees the tool result; we don't).
|
|
520
|
+
Over-publishing falsely poisons peers' rollbacks via contested
|
|
521
|
+
marks, while under-publishing only costs missed demotions.
|
|
522
|
+
"""
|
|
523
|
+
if not self._enabled or not mutations:
|
|
524
|
+
return 0
|
|
525
|
+
eligible = [m for m in mutations if self._claim_eligible(m)]
|
|
526
|
+
if not eligible:
|
|
527
|
+
return 0
|
|
528
|
+
published = 0
|
|
529
|
+
with self._lock:
|
|
530
|
+
touched: set[str] = set()
|
|
531
|
+
for m in eligible:
|
|
532
|
+
t0 = m.t_start if m.t_start is not None else m.timestamp
|
|
533
|
+
t1 = m.t_end if m.t_end is not None else self._now()
|
|
534
|
+
claim = MutationClaim(
|
|
535
|
+
path=m.path,
|
|
536
|
+
key=canonical_key(m.path),
|
|
537
|
+
after_hash=m.after_hash,
|
|
538
|
+
after_exists=m.after_hash is not None or m.after_skip is not None,
|
|
539
|
+
t0=t0,
|
|
540
|
+
t1=t1,
|
|
541
|
+
source=m.source.value,
|
|
542
|
+
old_path=m.old_path,
|
|
543
|
+
old_key=canonical_key(m.old_path) if m.old_path else None,
|
|
544
|
+
)
|
|
545
|
+
# A MOVE names two paths, but a same-repo rename (the
|
|
546
|
+
# common case) resolves both to one tree — dedupe or the
|
|
547
|
+
# claim lands twice and burns MAX_CLAIMS double.
|
|
548
|
+
repo_keys: set[str] = set()
|
|
549
|
+
for path in (m.path, m.old_path) if m.old_path else (m.path,):
|
|
550
|
+
root = self._tree_root_for(path, fallback_root)
|
|
551
|
+
if root is not None:
|
|
552
|
+
repo_keys.add(repo_key_for_root(root))
|
|
553
|
+
for repo_key in sorted(repo_keys):
|
|
554
|
+
state = self._state_for(repo_key)
|
|
555
|
+
state.claims.append(claim)
|
|
556
|
+
if len(state.claims) > self._max_claims:
|
|
557
|
+
# Bounded by count, not age — FIFO.
|
|
558
|
+
del state.claims[: len(state.claims) - self._max_claims]
|
|
559
|
+
touched.add(repo_key)
|
|
560
|
+
published += 1
|
|
561
|
+
for repo_key in touched:
|
|
562
|
+
self._write_state(repo_key)
|
|
563
|
+
return published
|
|
564
|
+
|
|
565
|
+
@staticmethod
|
|
566
|
+
def _claim_eligible(m: FileMutation) -> bool:
|
|
567
|
+
"""Evidence test: proven provenance + provable net change.
|
|
568
|
+
|
|
569
|
+
The change test is the same skip-aware rule as ``is_net_zero``:
|
|
570
|
+
hash inequality or existence flip. A fully-skipped MODIFY
|
|
571
|
+
proves nothing and publishes nothing. ``ASSUMED`` inferences
|
|
572
|
+
are never published.
|
|
573
|
+
"""
|
|
574
|
+
if m.provenance is not MutationProvenance.PROVEN:
|
|
575
|
+
return False
|
|
576
|
+
before_exists = m.before_hash is not None or m.before_skip is not None
|
|
577
|
+
after_exists = m.after_hash is not None or m.after_skip is not None
|
|
578
|
+
return m.before_hash != m.after_hash or before_exists != after_exists
|
|
579
|
+
|
|
580
|
+
def close(self) -> None:
|
|
581
|
+
"""Stamp ``closed_at`` on every tree file this session wrote.
|
|
582
|
+
|
|
583
|
+
Idempotent; files are never deleted here (peers' late rollbacks
|
|
584
|
+
still need the claims — removal is GC's job).
|
|
585
|
+
"""
|
|
586
|
+
if not self._enabled or self._closed_at is not None:
|
|
587
|
+
return
|
|
588
|
+
with self._lock:
|
|
589
|
+
if self._closed_at is not None:
|
|
590
|
+
return
|
|
591
|
+
end = self._now()
|
|
592
|
+
self._closed_at = end
|
|
593
|
+
for repo_key, state in self._states.items():
|
|
594
|
+
for window in state.windows.values():
|
|
595
|
+
if window.end is None:
|
|
596
|
+
window.end = end
|
|
597
|
+
self._write_state(repo_key)
|
|
598
|
+
|
|
599
|
+
# -- Read side -------------------------------------------------------
|
|
600
|
+
|
|
601
|
+
def _peer_dir_signature(self, repo_key: str) -> tuple:
|
|
602
|
+
"""Cheap change signature over peer files in one tree dir."""
|
|
603
|
+
own_name = f"{self._session_id}.json"
|
|
604
|
+
entries: list[tuple[str, int, int]] = []
|
|
605
|
+
try:
|
|
606
|
+
with os.scandir(self._tree_dir(repo_key)) as it:
|
|
607
|
+
for entry in it:
|
|
608
|
+
if entry.name == own_name or not entry.name.endswith(".json"):
|
|
609
|
+
continue
|
|
610
|
+
with contextlib.suppress(OSError):
|
|
611
|
+
st = entry.stat()
|
|
612
|
+
entries.append((entry.name, st.st_mtime_ns, st.st_size))
|
|
613
|
+
except OSError:
|
|
614
|
+
return ()
|
|
615
|
+
entries.sort()
|
|
616
|
+
return tuple(entries)
|
|
617
|
+
|
|
618
|
+
def _read_peers(self, repo_key: str) -> list[PeerRegistry]:
|
|
619
|
+
own_name = f"{self._session_id}.json"
|
|
620
|
+
peers: list[PeerRegistry] = []
|
|
621
|
+
try:
|
|
622
|
+
with os.scandir(self._tree_dir(repo_key)) as it:
|
|
623
|
+
names = [e.name for e in it if e.name.endswith(".json") and e.name != own_name]
|
|
624
|
+
except OSError:
|
|
625
|
+
return []
|
|
626
|
+
for name in names:
|
|
627
|
+
try:
|
|
628
|
+
payload = (self._tree_dir(repo_key) / name).read_text(encoding="utf-8")
|
|
629
|
+
except OSError:
|
|
630
|
+
continue
|
|
631
|
+
peer = parse_registry_payload(payload)
|
|
632
|
+
if peer is not None and peer.session_id != self._session_id:
|
|
633
|
+
peers.append(peer)
|
|
634
|
+
return peers
|
|
635
|
+
|
|
636
|
+
@staticmethod
|
|
637
|
+
def _local_log_signature(log: MutationLog) -> tuple:
|
|
638
|
+
# The completed-interval count is load-bearing: file-tool rows
|
|
639
|
+
# exist from record() time with an OPEN interval (t_end unset)
|
|
640
|
+
# and only become matchable when finalize stamps t_end. A read
|
|
641
|
+
# refresh (/diff, ACP) landing mid-call would otherwise cache a
|
|
642
|
+
# signature the t_end stamp cannot change — turn/row counts stay
|
|
643
|
+
# identical — and the finalize reclassify would short-circuit
|
|
644
|
+
# past the completed row forever.
|
|
645
|
+
rows = 0
|
|
646
|
+
complete = 0
|
|
647
|
+
for turn in log.periods:
|
|
648
|
+
rows += len(turn.mutations)
|
|
649
|
+
for m in turn.mutations:
|
|
650
|
+
if m.t_end is not None:
|
|
651
|
+
complete += 1
|
|
652
|
+
return (len(log.periods), rows, complete)
|
|
653
|
+
|
|
654
|
+
def reclassify(self, tracker: MutationTracker, *, force: bool = False, fallback_root: str | None = None) -> bool:
|
|
655
|
+
"""Retroactively re-attribute the tracker's log against peer claims.
|
|
656
|
+
|
|
657
|
+
Idempotent; safe to run at every opportunity. Returns True when
|
|
658
|
+
any row changed (caller persists via the normal session-state
|
|
659
|
+
save). ``force=True`` bypasses the no-change short-circuit —
|
|
660
|
+
used at rollback-plan construction, the authoritative last check
|
|
661
|
+
before the only destructive consumer acts.
|
|
662
|
+
"""
|
|
663
|
+
if not self._enabled:
|
|
664
|
+
return False
|
|
665
|
+
|
|
666
|
+
# Collect the candidate rows and their trees under the tracker
|
|
667
|
+
# lock; read peer files outside it; apply matches under it again.
|
|
668
|
+
def _collect(log: MutationLog) -> tuple[tuple, list[str]]:
|
|
669
|
+
paths: list[str] = []
|
|
670
|
+
for turn in log.periods:
|
|
671
|
+
for m in turn.mutations:
|
|
672
|
+
if m.provenance is MutationProvenance.FOREIGN:
|
|
673
|
+
continue
|
|
674
|
+
if m.t_start is None or m.t_end is None:
|
|
675
|
+
continue # legacy row — no interval, never matches
|
|
676
|
+
paths.append(m.path)
|
|
677
|
+
if m.old_path:
|
|
678
|
+
paths.append(m.old_path)
|
|
679
|
+
return self._local_log_signature(log), paths
|
|
680
|
+
|
|
681
|
+
local_sig, paths = tracker.with_locked_log(_collect)
|
|
682
|
+
if not paths:
|
|
683
|
+
return False
|
|
684
|
+
|
|
685
|
+
with self._lock:
|
|
686
|
+
repo_keys = self._repo_keys_for_paths(paths, fallback_root)
|
|
687
|
+
peer_sigs = {k: self._peer_dir_signature(k) for k in repo_keys}
|
|
688
|
+
if (
|
|
689
|
+
not force
|
|
690
|
+
and self._last_local_sig == local_sig
|
|
691
|
+
and all(self._last_peer_sig.get(k) == sig for k, sig in peer_sigs.items())
|
|
692
|
+
):
|
|
693
|
+
return False
|
|
694
|
+
|
|
695
|
+
claims_by_key: dict[str, list[MutationClaim]] = {}
|
|
696
|
+
for repo_key in repo_keys:
|
|
697
|
+
for peer in self._read_peers(repo_key):
|
|
698
|
+
for claim in peer.claims:
|
|
699
|
+
claims_by_key.setdefault(claim.key, []).append(claim)
|
|
700
|
+
if claim.old_key:
|
|
701
|
+
claims_by_key.setdefault(claim.old_key, []).append(claim)
|
|
702
|
+
|
|
703
|
+
changed = False
|
|
704
|
+
if claims_by_key:
|
|
705
|
+
key_cache: dict[str, str] = {}
|
|
706
|
+
|
|
707
|
+
def _key(p: str) -> str:
|
|
708
|
+
if p not in key_cache:
|
|
709
|
+
key_cache[p] = canonical_key(p)
|
|
710
|
+
return key_cache[p]
|
|
711
|
+
|
|
712
|
+
def _apply(log: MutationLog) -> bool:
|
|
713
|
+
any_change = False
|
|
714
|
+
for turn in log.periods:
|
|
715
|
+
for m in turn.mutations:
|
|
716
|
+
if m.provenance is MutationProvenance.FOREIGN:
|
|
717
|
+
continue
|
|
718
|
+
if m.t_start is None or m.t_end is None:
|
|
719
|
+
continue
|
|
720
|
+
candidates = list(claims_by_key.get(_key(m.path), ()))
|
|
721
|
+
if m.old_path:
|
|
722
|
+
candidates += claims_by_key.get(_key(m.old_path), ())
|
|
723
|
+
for claim in candidates:
|
|
724
|
+
if not _strict_overlap(m.t_start, m.t_end, claim.t0, claim.t1):
|
|
725
|
+
continue
|
|
726
|
+
if (
|
|
727
|
+
m.provenance is MutationProvenance.ASSUMED
|
|
728
|
+
and m.old_path is None
|
|
729
|
+
# MOVEs never demote — EITHER side. A peer
|
|
730
|
+
# MOVE claim is indexed under both key and
|
|
731
|
+
# old_key, so a local simple row on the move
|
|
732
|
+
# destination can carry identical bytes; the
|
|
733
|
+
# rename still isn't proof the peer authored
|
|
734
|
+
# our row's content. Fall to contested.
|
|
735
|
+
and claim.old_path is None
|
|
736
|
+
and self._content_identity(m, claim)
|
|
737
|
+
):
|
|
738
|
+
# Foreign by policy: identity proves the peer
|
|
739
|
+
# wrote these bytes; preferring under-claiming
|
|
740
|
+
# our own work over clobbering theirs.
|
|
741
|
+
m.provenance = MutationProvenance.FOREIGN
|
|
742
|
+
any_change = True
|
|
743
|
+
break
|
|
744
|
+
if not m.contested:
|
|
745
|
+
m.contested = True
|
|
746
|
+
any_change = True
|
|
747
|
+
return any_change
|
|
748
|
+
|
|
749
|
+
changed = tracker.with_locked_log(_apply)
|
|
750
|
+
|
|
751
|
+
with self._lock:
|
|
752
|
+
self._last_local_sig = tracker.with_locked_log(self._local_log_signature)
|
|
753
|
+
self._last_peer_sig.update(peer_sigs)
|
|
754
|
+
if changed:
|
|
755
|
+
# The signature update above makes this change invisible
|
|
756
|
+
# to the next non-forced call — remember that no one has
|
|
757
|
+
# persisted it yet (see consume_unsaved_reclassification).
|
|
758
|
+
self._unsaved_reclassification = True
|
|
759
|
+
return changed
|
|
760
|
+
|
|
761
|
+
def consume_unsaved_reclassification(self) -> bool:
|
|
762
|
+
"""Return-and-clear the "rows changed, nobody saved yet" flag.
|
|
763
|
+
|
|
764
|
+
``reclassify`` updates its short-circuit signatures as it
|
|
765
|
+
applies changes, so a later refresh that itself changes nothing
|
|
766
|
+
cannot re-discover that the serialized session is behind — a
|
|
767
|
+
finalize-time reclassification (which has no saver) would leave
|
|
768
|
+
detached readers of ``session.json`` stale forever. The
|
|
769
|
+
engine's refresh entry point (the persister) consumes this
|
|
770
|
+
instead of trusting only its own call's return value.
|
|
771
|
+
"""
|
|
772
|
+
with self._lock:
|
|
773
|
+
dirty = self._unsaved_reclassification
|
|
774
|
+
self._unsaved_reclassification = False
|
|
775
|
+
return dirty
|
|
776
|
+
|
|
777
|
+
@staticmethod
|
|
778
|
+
def _content_identity(m: FileMutation, claim: MutationClaim) -> bool:
|
|
779
|
+
"""Positive evidence the resulting bytes are the peer's own write.
|
|
780
|
+
|
|
781
|
+
Hashes present and equal, or both sides gone. Skip-marked sides
|
|
782
|
+
(no bytes to compare) never qualify.
|
|
783
|
+
"""
|
|
784
|
+
if m.after_hash is not None and claim.after_hash is not None:
|
|
785
|
+
return m.after_hash == claim.after_hash
|
|
786
|
+
local_gone = m.after_hash is None and m.after_skip is None
|
|
787
|
+
return local_gone and not claim.after_exists
|
|
788
|
+
|
|
789
|
+
# -- Rollback augmentation --------------------------------------------
|
|
790
|
+
|
|
791
|
+
def augment_rollback_plan(
|
|
792
|
+
self,
|
|
793
|
+
tracker: MutationTracker,
|
|
794
|
+
plan: RollbackPlan,
|
|
795
|
+
*,
|
|
796
|
+
scope_paths: list[str] | None = None,
|
|
797
|
+
fallback_root: str | None = None,
|
|
798
|
+
) -> RollbackPlan:
|
|
799
|
+
"""Final peer check on a freshly built plan.
|
|
800
|
+
|
|
801
|
+
- A path carrying a peer claim strictly *newer* than our last
|
|
802
|
+
write on it is excluded (``PEER_MODIFIED_SINCE``): restoring
|
|
803
|
+
our older snapshot would clobber the peer's newer write, and
|
|
804
|
+
warning-only would let non-modal rollback paths proceed
|
|
805
|
+
straight through the data loss. (Claims *overlapping* our
|
|
806
|
+
writes were already handled by reclassification — the caller
|
|
807
|
+
runs ``reclassify(force=True)`` before building the plan.)
|
|
808
|
+
- An active peer (not ``closed_at``, live pid) with a still-open
|
|
809
|
+
window whose scope overlaps ours produces a repo-level
|
|
810
|
+
:class:`RollbackWarning` — covering the one case attribution
|
|
811
|
+
cannot: writes on disk whose claims don't exist yet.
|
|
812
|
+
"""
|
|
813
|
+
if not self._enabled or (not plan.entries and not plan.exclusions):
|
|
814
|
+
return plan
|
|
815
|
+
|
|
816
|
+
# Last local write end per plan path (from the log, under lock).
|
|
817
|
+
entry_paths = [path for path, _snap in plan.entries]
|
|
818
|
+
wanted = set(entry_paths)
|
|
819
|
+
|
|
820
|
+
def _last_writes(log: MutationLog) -> dict[str, float]:
|
|
821
|
+
last: dict[str, float] = {}
|
|
822
|
+
for turn in log.periods:
|
|
823
|
+
for m in turn.mutations:
|
|
824
|
+
if m.provenance is MutationProvenance.FOREIGN:
|
|
825
|
+
continue
|
|
826
|
+
end = m.t_end if m.t_end is not None else m.timestamp
|
|
827
|
+
for p in (m.path, m.old_path) if m.old_path else (m.path,):
|
|
828
|
+
if p in wanted and end > last.get(p, float("-inf")):
|
|
829
|
+
last[p] = end
|
|
830
|
+
return last
|
|
831
|
+
|
|
832
|
+
last_write = tracker.with_locked_log(_last_writes)
|
|
833
|
+
|
|
834
|
+
reference_paths = list(entry_paths) + [path for path, _reason in plan.exclusions] + list(scope_paths or [])
|
|
835
|
+
with self._lock:
|
|
836
|
+
repo_keys = self._repo_keys_for_paths(reference_paths, fallback_root)
|
|
837
|
+
peers = [peer for repo_key in repo_keys for peer in self._read_peers(repo_key)]
|
|
838
|
+
|
|
839
|
+
claims_by_key: dict[str, list[MutationClaim]] = {}
|
|
840
|
+
for peer in peers:
|
|
841
|
+
for claim in peer.claims:
|
|
842
|
+
claims_by_key.setdefault(claim.key, []).append(claim)
|
|
843
|
+
if claim.old_key:
|
|
844
|
+
claims_by_key.setdefault(claim.old_key, []).append(claim)
|
|
845
|
+
|
|
846
|
+
kept: list[tuple[str, Any]] = []
|
|
847
|
+
added_exclusions: list[tuple[str, RollbackExclusionReason]] = []
|
|
848
|
+
for path, snap in plan.entries:
|
|
849
|
+
claims = claims_by_key.get(canonical_key(path), ())
|
|
850
|
+
threshold = last_write.get(path, float("-inf"))
|
|
851
|
+
if any(c.t0 > threshold for c in claims):
|
|
852
|
+
added_exclusions.append((path, RollbackExclusionReason.PEER_MODIFIED_SINCE))
|
|
853
|
+
else:
|
|
854
|
+
kept.append((path, snap))
|
|
855
|
+
|
|
856
|
+
warnings = list(plan.warnings)
|
|
857
|
+
our_scope = [canonical_key(p) for p in reference_paths]
|
|
858
|
+
warned_sessions: set[str] = set()
|
|
859
|
+
for peer in peers:
|
|
860
|
+
if not peer.is_active or peer.session_id in warned_sessions:
|
|
861
|
+
continue
|
|
862
|
+
for window in peer.windows:
|
|
863
|
+
if window.end is None and self._scopes_overlap(window.scope, our_scope):
|
|
864
|
+
warned_sessions.add(peer.session_id)
|
|
865
|
+
warnings.append(
|
|
866
|
+
RollbackWarning(
|
|
867
|
+
code=PEER_ACTIVE_WINDOW_WARNING,
|
|
868
|
+
message=(
|
|
869
|
+
f"Another {APP_DISPLAY_NAME} session has an active command in this "
|
|
870
|
+
"workspace; its changes may not be attributed yet."
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
break
|
|
875
|
+
|
|
876
|
+
if not added_exclusions and len(warnings) == len(plan.warnings):
|
|
877
|
+
return plan
|
|
878
|
+
return RollbackPlan(
|
|
879
|
+
entries=kept,
|
|
880
|
+
exclusions=sorted([*plan.exclusions, *added_exclusions], key=lambda item: item[0]),
|
|
881
|
+
warnings=warnings,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
@staticmethod
|
|
885
|
+
def _scopes_overlap(peer_scope: list[str], our_scope_keys: list[str]) -> bool:
|
|
886
|
+
"""Ancestor-or-equal containment in either direction, canonically."""
|
|
887
|
+
for raw in peer_scope:
|
|
888
|
+
p = canonical_key(raw)
|
|
889
|
+
for q in our_scope_keys:
|
|
890
|
+
if p == q:
|
|
891
|
+
return True
|
|
892
|
+
if q.startswith(p.rstrip(os.sep) + os.sep) or p.startswith(q.rstrip(os.sep) + os.sep):
|
|
893
|
+
return True
|
|
894
|
+
return False
|