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,996 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""The one place settings are assembled from their sources.
|
|
4
|
+
|
|
5
|
+
``Settings`` is a plain value object; this module is what turns the layered
|
|
6
|
+
sources into one: the user document, the dotenv files beside it, the process
|
|
7
|
+
environment as it stood before bootstrap, the project trust domain under the
|
|
8
|
+
caller's ``project_root``, and the pins a frontend passes in.
|
|
9
|
+
|
|
10
|
+
The load runs in two phases. Phase one reads every file whole and audits the
|
|
11
|
+
project layers completely — policy filter, per-key coercion, tighten/loosen
|
|
12
|
+
verdicts — whether or not anything above will shadow them, because a rejected
|
|
13
|
+
project value must warn even when it would have lost anyway. Phase two picks
|
|
14
|
+
each key's winner from the highest layer down; it is the only phase
|
|
15
|
+
``InvalidPolicy.SAFE_DEFAULT`` terminates. The non-project layers keep their
|
|
16
|
+
established warning scope (a layer below the winner stays unexamined), so
|
|
17
|
+
effective values and existing warnings are unchanged by the split.
|
|
18
|
+
|
|
19
|
+
Callers get three things back and are expected to keep all of them: the
|
|
20
|
+
settings, where each value came from, and what was rejected on the way. A
|
|
21
|
+
caller that only wants the settings can say so with ``.settings``, but it
|
|
22
|
+
cannot pretend the other two do not exist — that is how the panel ends up
|
|
23
|
+
unable to explain why the value a user just typed has no effect.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
31
|
+
from dataclasses import asdict, dataclass, replace
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from types import MappingProxyType
|
|
34
|
+
from typing import Any, Final
|
|
35
|
+
|
|
36
|
+
from chrys.foundation.config.coercion import Coerced, CoerceReason, CoerceStatus, invalid
|
|
37
|
+
from chrys.foundation.config.context import EvalContext
|
|
38
|
+
from chrys.foundation.config.env_file import config_env_path
|
|
39
|
+
from chrys.foundation.config.env_layers import (
|
|
40
|
+
POINTER_ENV_NAMES,
|
|
41
|
+
canonical_env_name,
|
|
42
|
+
process_env_snapshot,
|
|
43
|
+
read_dotenv_layer,
|
|
44
|
+
)
|
|
45
|
+
from chrys.foundation.config.runtime_pointer import get_model_pointer
|
|
46
|
+
from chrys.foundation.config.settings import DEFAULT_MAX_TRANSIENT_RETRIES, Settings
|
|
47
|
+
from chrys.foundation.config.spec import (
|
|
48
|
+
FILE_SOURCES,
|
|
49
|
+
InvalidPolicy,
|
|
50
|
+
Kind,
|
|
51
|
+
ProjectMerge,
|
|
52
|
+
SettingOrigin,
|
|
53
|
+
SettingSpec,
|
|
54
|
+
Source,
|
|
55
|
+
kind_accepts,
|
|
56
|
+
specs_by_field,
|
|
57
|
+
)
|
|
58
|
+
from chrys.foundation.config.user_settings import apply_settings_patch, flatten_user_doc, user_settings_path
|
|
59
|
+
from chrys.foundation.config.yaml_store import (
|
|
60
|
+
LOCK_TIMEOUT_SECONDS,
|
|
61
|
+
read_yaml_doc,
|
|
62
|
+
read_yaml_doc_readonly,
|
|
63
|
+
update_yaml_doc,
|
|
64
|
+
)
|
|
65
|
+
from chrys.foundation.platform.runtime_paths import same_path
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger(__name__)
|
|
68
|
+
|
|
69
|
+
DEFAULT_EVAL_CONTEXT = EvalContext(frontend_default_max_transient_retries=DEFAULT_MAX_TRANSIENT_RETRIES)
|
|
70
|
+
"""Interactive-frontend policy. ``chrys run`` passes its own (15)."""
|
|
71
|
+
|
|
72
|
+
_FRONTEND_DEFAULT_FIELD = "frontend_default_max_transient_retries"
|
|
73
|
+
|
|
74
|
+
_EMPTY_MAP: Final[Mapping[str, Any]] = MappingProxyType({})
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class SettingsWarning:
|
|
79
|
+
"""One value a layer offered and the loader refused, or had to clamp."""
|
|
80
|
+
|
|
81
|
+
key: str
|
|
82
|
+
origin: SettingOrigin
|
|
83
|
+
outcome: Coerced
|
|
84
|
+
"""The verdict itself, carried whole.
|
|
85
|
+
|
|
86
|
+
Copying ``raw``/``reason``/``limit`` out field by field is how the two
|
|
87
|
+
representations start disagreeing — a clamp reports both the bound it hit
|
|
88
|
+
and the value it landed on, and only one of those is ``outcome.value``.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def rejected(self) -> bool:
|
|
93
|
+
"""Whether the value was dropped entirely rather than adjusted."""
|
|
94
|
+
return self.outcome.status is CoerceStatus.INVALID
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class DormantProjectConfig:
|
|
99
|
+
"""Project-layer configuration found on disk while the gate was off.
|
|
100
|
+
|
|
101
|
+
Reported as a whole file rather than key by key: the user's decision here
|
|
102
|
+
is a single one — enable ``project.config_enabled`` or leave it — and a
|
|
103
|
+
warning per key would say the same thing as many times as the repository
|
|
104
|
+
has opinions.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
path: Path
|
|
108
|
+
keys: tuple[str, ...]
|
|
109
|
+
"""The dotted keys that would be considered if the layer were enabled."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True, slots=True)
|
|
113
|
+
class DormantProjectSource:
|
|
114
|
+
"""Project hooks or skills found on disk while their own setting was off."""
|
|
115
|
+
|
|
116
|
+
key: str
|
|
117
|
+
"""The setting that loads them: ``project.hooks_enabled`` or ``project.skills_enabled``."""
|
|
118
|
+
|
|
119
|
+
path: Path
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class LoadedSettings:
|
|
124
|
+
"""Settings plus the two things a caller must not silently drop."""
|
|
125
|
+
|
|
126
|
+
settings: Settings
|
|
127
|
+
provenance: Mapping[str, SettingOrigin]
|
|
128
|
+
warnings: tuple[SettingsWarning, ...] = ()
|
|
129
|
+
sealed_keys: frozenset[str] = frozenset()
|
|
130
|
+
"""Keys forced to their built-in default by :attr:`InvalidPolicy.SAFE_DEFAULT`.
|
|
131
|
+
|
|
132
|
+
A sealed key is the one case where a value the user can see in their own
|
|
133
|
+
``settings.yaml`` is deliberately not in effect, so the panel has to be able
|
|
134
|
+
to say why instead of showing a saved value that does nothing.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
unknown_keys: tuple[str, ...] = ()
|
|
138
|
+
"""Dotted keys the user document holds that no field declares.
|
|
139
|
+
|
|
140
|
+
Reported once, not dropped: the keys survive every write untouched (a
|
|
141
|
+
patch never rewrites what it did not name), so a downgrade to the version
|
|
142
|
+
that understood them loses nothing — but silence here would leave a typo'd
|
|
143
|
+
key looking configured forever.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
dormant_project: tuple[DormantProjectConfig, ...] = ()
|
|
147
|
+
"""Project configuration discovered while ``project.config_enabled`` is off.
|
|
148
|
+
|
|
149
|
+
Carried rather than dropped for the same reason warnings are: a repository
|
|
150
|
+
shipped configuration, the user never authorised it, and silence would
|
|
151
|
+
leave them believing it is in force. The caller says so once per file.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
dormant_project_sources: tuple[DormantProjectSource, ...] = ()
|
|
155
|
+
"""Project hooks and skills discovered while their setting is off, reported the same way."""
|
|
156
|
+
|
|
157
|
+
def overlay(self, layer: Source, **values: Any) -> LoadedSettings:
|
|
158
|
+
"""Return a copy carrying *values*, credited to *layer*.
|
|
159
|
+
|
|
160
|
+
The only sanctioned way to change settings after a load. A bare
|
|
161
|
+
``dataclasses.replace`` on ``.settings`` — which is what every runtime
|
|
162
|
+
override did — leaves provenance describing the value the field used to
|
|
163
|
+
hold, and provenance that is right only some of the time is worse for
|
|
164
|
+
the panel than none: it would offer to remove an override the user never
|
|
165
|
+
set, and hide the one they did.
|
|
166
|
+
|
|
167
|
+
Values are checked exactly as pins are, for the same reason: they arrive
|
|
168
|
+
already typed, from our own code, and never meet a coercer otherwise.
|
|
169
|
+
|
|
170
|
+
Only the layers without a file can be spelled this way. A file layer's
|
|
171
|
+
origin must name the file it came from, which a caller holding one value
|
|
172
|
+
does not know.
|
|
173
|
+
"""
|
|
174
|
+
if layer in FILE_SOURCES:
|
|
175
|
+
msg = f"{layer.name} values come from a file, so they cannot be overlaid"
|
|
176
|
+
raise ValueError(msg)
|
|
177
|
+
specs = specs_by_field(Settings)
|
|
178
|
+
unknown = sorted(set(values) - set(specs))
|
|
179
|
+
if unknown:
|
|
180
|
+
msg = f"Unknown settings fields: {', '.join(unknown)}"
|
|
181
|
+
raise TypeError(msg)
|
|
182
|
+
origin = SettingOrigin(layer=layer)
|
|
183
|
+
canonical = {name: _pinned(specs[name], value).value for name, value in values.items()}
|
|
184
|
+
overlaid = {specs[name].key for name in canonical}
|
|
185
|
+
return replace(
|
|
186
|
+
self,
|
|
187
|
+
settings=replace(self.settings, **canonical),
|
|
188
|
+
provenance={**self.provenance, **dict.fromkeys(overlaid, origin)},
|
|
189
|
+
# A sealed key is one nothing was allowed to write, so a key that
|
|
190
|
+
# has just been written is no longer one. Leaving it in would have
|
|
191
|
+
# the panel explain a built-in default that is not what is in use.
|
|
192
|
+
sealed_keys=self.sealed_keys - overlaid,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def source_for(self, key: str) -> SettingOrigin:
|
|
196
|
+
"""Where *key*'s effective value came from, including which file.
|
|
197
|
+
|
|
198
|
+
Scoped to this instance on purpose: with per-session project roots and
|
|
199
|
+
model pins, a process-global ``effective_source(key)`` could not answer
|
|
200
|
+
honestly for more than one session at a time.
|
|
201
|
+
"""
|
|
202
|
+
return self.provenance.get(key, SettingOrigin(layer=Source.DEFAULT))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
206
|
+
class PreparedSettings:
|
|
207
|
+
"""A layered load and its already computed runtime overlay."""
|
|
208
|
+
|
|
209
|
+
base: LoadedSettings
|
|
210
|
+
effective: LoadedSettings
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class SettingsHandle:
|
|
214
|
+
"""The one settings value that everything sharing a session points at.
|
|
215
|
+
|
|
216
|
+
:class:`LoadedSettings` is replaced whole rather than written into, which
|
|
217
|
+
keeps a single holder honest and lets two holders drift: the TUI and the
|
|
218
|
+
engine start from the same load, and a live write rebinds one of them. The
|
|
219
|
+
holders share this instead, so a change is installed once and everyone
|
|
220
|
+
reading through it sees the same value, the same provenance and the same
|
|
221
|
+
seals — the three that only mean something together.
|
|
222
|
+
|
|
223
|
+
It is the mutable half of the split. ``LoadedSettings`` stays immutable so
|
|
224
|
+
a caller can hold one across a rebuild and know it did not move underneath
|
|
225
|
+
them; this names the place where moving is the point.
|
|
226
|
+
|
|
227
|
+
Per session, never process-global. ACP runs many sessions in one process
|
|
228
|
+
with their own project roots and model pins, so a shared cell above the
|
|
229
|
+
session is a cell where one session's pin becomes another's.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
__slots__ = ("_base", "_effective", "_runtime")
|
|
233
|
+
|
|
234
|
+
def __init__(self, loaded: LoadedSettings) -> None:
|
|
235
|
+
self._base = loaded
|
|
236
|
+
self._runtime: dict[str, Any] = {}
|
|
237
|
+
self._effective = loaded
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def loaded(self) -> LoadedSettings:
|
|
241
|
+
"""Current settings together with where each value came from."""
|
|
242
|
+
return self._effective
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def settings(self) -> Settings:
|
|
246
|
+
"""Current values, for the readers that have no use for provenance."""
|
|
247
|
+
return self._effective.settings
|
|
248
|
+
|
|
249
|
+
def install(self, loaded: LoadedSettings) -> None:
|
|
250
|
+
"""Replace the layered load. Live choices stay on top of the new one.
|
|
251
|
+
|
|
252
|
+
A reload re-reads the files and the environment, which is every layer
|
|
253
|
+
*except* the one a live choice lives in — ``Source.RUNTIME`` is defined
|
|
254
|
+
as the layer nothing loads from. Dropping those values here would undo
|
|
255
|
+
the user's theme the next time anything triggered a reload, and the TUI
|
|
256
|
+
would not even follow it back: Textual keeps showing the theme that was
|
|
257
|
+
chosen, so the settings would simply stop describing the screen.
|
|
258
|
+
|
|
259
|
+
The window where that is not merely a misattribution is real, because
|
|
260
|
+
``persist_theme`` and its siblings log and swallow their failures: when
|
|
261
|
+
the write to disk did not land, this overlay is the only record that the
|
|
262
|
+
choice was ever made.
|
|
263
|
+
"""
|
|
264
|
+
self.install_prepared(self.prepare(loaded))
|
|
265
|
+
|
|
266
|
+
def prepare(self, loaded: LoadedSettings) -> PreparedSettings:
|
|
267
|
+
"""Compute a candidate overlay without changing the installed settings."""
|
|
268
|
+
effective = loaded.overlay(Source.RUNTIME, **self._runtime) if self._runtime else loaded
|
|
269
|
+
return PreparedSettings(base=loaded, effective=effective)
|
|
270
|
+
|
|
271
|
+
def install_prepared(self, prepared: PreparedSettings) -> None:
|
|
272
|
+
"""Install values whose runtime overlay has already succeeded."""
|
|
273
|
+
self._base = prepared.base
|
|
274
|
+
self._effective = prepared.effective
|
|
275
|
+
|
|
276
|
+
def override(self, **values: Any) -> None:
|
|
277
|
+
"""Record a live choice, credited to ``Source.RUNTIME``.
|
|
278
|
+
|
|
279
|
+
Kept apart from the load rather than folded into it so that reloading
|
|
280
|
+
and rolling back — both of which replace the whole layered value — can
|
|
281
|
+
do so without deciding what to do about choices they know nothing about.
|
|
282
|
+
"""
|
|
283
|
+
self._runtime.update(values)
|
|
284
|
+
self._reapply()
|
|
285
|
+
|
|
286
|
+
def persist_approval_default(self, mode: str, *, persist: Callable[[str], None]) -> str:
|
|
287
|
+
"""Persist a global choice and mirror its safe launch default in the runtime overlay."""
|
|
288
|
+
persist(mode)
|
|
289
|
+
default = "auto" if mode == "bypass" else mode
|
|
290
|
+
self.override(default_approval_mode=default)
|
|
291
|
+
return default
|
|
292
|
+
|
|
293
|
+
def _reapply(self) -> None:
|
|
294
|
+
"""Rebuild the effective value: the load, then live choices on top.
|
|
295
|
+
|
|
296
|
+
Recomputed from ``_base`` every time rather than accumulated, so a
|
|
297
|
+
rollback that restores an older load cannot resurrect an override that
|
|
298
|
+
was replaced in the meantime, and repeating one is a no-op.
|
|
299
|
+
"""
|
|
300
|
+
self._effective = self._base.overlay(Source.RUNTIME, **self._runtime) if self._runtime else self._base
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def load_settings(
|
|
304
|
+
*,
|
|
305
|
+
project_root: Path | None = None,
|
|
306
|
+
eval_context: EvalContext = DEFAULT_EVAL_CONTEXT,
|
|
307
|
+
env: Mapping[str, str] | None = None,
|
|
308
|
+
**pins: Any,
|
|
309
|
+
) -> LoadedSettings:
|
|
310
|
+
"""Assemble settings from every source, resolving each key downwards.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
project_root: Workspace root whose project layer applies —
|
|
314
|
+
``<root>/.chrys/settings.yaml``, behind ``project.config_enabled``.
|
|
315
|
+
``None`` loads without a project layer, which is what a caller
|
|
316
|
+
that serves no particular root (the ACP manager base) means by it.
|
|
317
|
+
The root a caller passes is a trust decision, not a discovery
|
|
318
|
+
hint: there is deliberately no walk upwards from it.
|
|
319
|
+
eval_context: Frontend policy in force. Must be passed at load time —
|
|
320
|
+
the project layer is evaluated *during* the load, so substituting
|
|
321
|
+
it afterwards would arrive after the verdicts it informs.
|
|
322
|
+
env: Environment to read instead of ``os.environ``. Tests inject here;
|
|
323
|
+
production passes nothing.
|
|
324
|
+
**pins: Explicit per-caller values that outrank every source, e.g. a
|
|
325
|
+
session's restored model profile or a frontend's own default.
|
|
326
|
+
"""
|
|
327
|
+
if env is not None:
|
|
328
|
+
# An injected environment is the whole environment: hermetic, and not
|
|
329
|
+
# this process's, so nothing about the real one may leak into it.
|
|
330
|
+
source: Mapping[str, str] = env
|
|
331
|
+
real_process = False
|
|
332
|
+
else:
|
|
333
|
+
# The environment layer is the environment *as bootstrap found it*.
|
|
334
|
+
# Reading it live would let anything that writes ``os.environ`` mid-run
|
|
335
|
+
# mint an ENV-layer setting that outranks the user's document and is
|
|
336
|
+
# then reported as a shell export they never made — and would make two
|
|
337
|
+
# loads of the same files disagree. Before bootstrap there is no
|
|
338
|
+
# snapshot and the live environment is still the real one.
|
|
339
|
+
snapshot = process_env_snapshot()
|
|
340
|
+
source = snapshot.values if snapshot is not None else os.environ
|
|
341
|
+
real_process = True
|
|
342
|
+
|
|
343
|
+
if _FRONTEND_DEFAULT_FIELD in pins:
|
|
344
|
+
# It is an input to the load, not a value the load produces, and the
|
|
345
|
+
# project layer's tighten/loosen verdicts are evaluated against it. A
|
|
346
|
+
# pin would arrive after those verdicts and be silently overwritten
|
|
347
|
+
# here, so say so instead.
|
|
348
|
+
msg = f"{_FRONTEND_DEFAULT_FIELD} is set through eval_context, not as a pin"
|
|
349
|
+
raise TypeError(msg)
|
|
350
|
+
|
|
351
|
+
values: dict[str, Any] = {}
|
|
352
|
+
provenance: dict[str, SettingOrigin] = {}
|
|
353
|
+
specs = specs_by_field(Settings)
|
|
354
|
+
# An injected environment asks for a hermetic load, so it turns the file
|
|
355
|
+
# layers off along with the real environment — a test passing ``env={}``
|
|
356
|
+
# must not read the developer's own ``~/.chrys`` *or* the project files:
|
|
357
|
+
# hermetic means hermetic, whatever root was passed alongside.
|
|
358
|
+
file_layers = _read_file_layers(specs, project_root) if env is None else None
|
|
359
|
+
# Phase one: the project trust domain is audited completely before any
|
|
360
|
+
# winner is chosen, so a project value a higher layer shadows still gets
|
|
361
|
+
# its policy verdict and its warning.
|
|
362
|
+
project = _project_layers(specs, file_layers, eval_context) if file_layers is not None else _NO_PROJECT_LAYERS
|
|
363
|
+
|
|
364
|
+
warnings: list[SettingsWarning] = list(project.warnings)
|
|
365
|
+
sealed: set[str] = set()
|
|
366
|
+
|
|
367
|
+
for name, entry in specs.items():
|
|
368
|
+
for coerced, origin in _layers(name, entry, pins, source, real_process, file_layers, project):
|
|
369
|
+
if coerced.status is CoerceStatus.INVALID:
|
|
370
|
+
# Project verdicts were already reported whole in phase one
|
|
371
|
+
# (shadowed-still-warns); consulting one here only decides.
|
|
372
|
+
if origin.layer is not Source.PROJECT:
|
|
373
|
+
warnings.append(SettingsWarning(key=entry.key, origin=origin, outcome=coerced))
|
|
374
|
+
if entry.invalid_policy is InvalidPolicy.SAFE_DEFAULT:
|
|
375
|
+
# Stop the walk. The built-in default is the safe landing
|
|
376
|
+
# spot this policy names, and no lower layer may replace it
|
|
377
|
+
# — which is also what makes "sealed" imply "source is
|
|
378
|
+
# DEFAULT": nothing below ever gets to write the key.
|
|
379
|
+
sealed.add(entry.key)
|
|
380
|
+
break
|
|
381
|
+
continue
|
|
382
|
+
if coerced.status is CoerceStatus.CLAMPED:
|
|
383
|
+
warnings.append(SettingsWarning(key=entry.key, origin=origin, outcome=coerced))
|
|
384
|
+
values[name] = coerced.value
|
|
385
|
+
provenance[entry.key] = origin
|
|
386
|
+
break
|
|
387
|
+
|
|
388
|
+
# The frontend's retry policy is a property of how Chrys was launched, not
|
|
389
|
+
# a configured value, so it never appears in any file and is never saved.
|
|
390
|
+
values[_FRONTEND_DEFAULT_FIELD] = eval_context.frontend_default_max_transient_retries
|
|
391
|
+
provenance["llm.retry.frontend_default"] = SettingOrigin(layer=Source.RUNTIME)
|
|
392
|
+
|
|
393
|
+
# A pin naming a field that carries no spec still has to reach the instance;
|
|
394
|
+
# an unknown name falls out of ``Settings(**values)`` as a ``TypeError``.
|
|
395
|
+
values.update({name: value for name, value in pins.items() if name not in specs})
|
|
396
|
+
|
|
397
|
+
settings = Settings(**values)
|
|
398
|
+
return LoadedSettings(
|
|
399
|
+
settings=settings,
|
|
400
|
+
provenance=provenance,
|
|
401
|
+
warnings=tuple(warnings),
|
|
402
|
+
sealed_keys=frozenset(sealed),
|
|
403
|
+
unknown_keys=file_layers.unknown_keys if file_layers is not None else (),
|
|
404
|
+
dormant_project=project.dormant,
|
|
405
|
+
dormant_project_sources=(
|
|
406
|
+
_dormant_project_sources(settings, project_root)
|
|
407
|
+
if file_layers is not None and project_root is not None
|
|
408
|
+
else ()
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
_PROJECT_HOOKS_FILE_NAMES: Final = ("hooks.yaml", "hooks.yml", "hooks.json")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _holds_skill(directory: Path, depth: int = 0) -> bool:
|
|
417
|
+
"""Whether *directory* has a ``SKILL.md`` as deep as skill discovery looks.
|
|
418
|
+
|
|
419
|
+
Mirrors ``service/skills/loader.py::discover_skill_directories``: the
|
|
420
|
+
folder itself and two levels below it.
|
|
421
|
+
"""
|
|
422
|
+
if (directory / "SKILL.md").is_file():
|
|
423
|
+
return True
|
|
424
|
+
if depth >= 2 or not directory.is_dir():
|
|
425
|
+
return False
|
|
426
|
+
try:
|
|
427
|
+
children = [child for child in directory.iterdir() if child.is_dir()]
|
|
428
|
+
except OSError:
|
|
429
|
+
return False # Discovery skips a folder it cannot list, and goes on with the rest.
|
|
430
|
+
return any(_holds_skill(child, depth + 1) for child in children)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _is_same_entry(left: Path, right: Path) -> bool:
|
|
434
|
+
"""Whether two spellings name one file or folder; on a caseless disk they may differ in case."""
|
|
435
|
+
try:
|
|
436
|
+
return left.samefile(right)
|
|
437
|
+
except OSError:
|
|
438
|
+
return same_path(left, right)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _dormant_project_sources(settings: Settings, project_root: Path) -> tuple[DormantProjectSource, ...]:
|
|
442
|
+
"""The project hooks and skills *project_root* holds that their setting keeps unloaded.
|
|
443
|
+
|
|
444
|
+
A workspace rooted at the user's home names the user's own hooks and skills
|
|
445
|
+
folders again; those load as the user's and are never reported.
|
|
446
|
+
"""
|
|
447
|
+
found: list[DormantProjectSource] = []
|
|
448
|
+
try:
|
|
449
|
+
if not settings.project_hooks_enabled:
|
|
450
|
+
hooks_dir = project_root / ".chrys" / "hooks"
|
|
451
|
+
if not _is_same_entry(hooks_dir, user_settings_path().parent / "hooks"):
|
|
452
|
+
hooks_file = next(
|
|
453
|
+
(hooks_dir / name for name in _PROJECT_HOOKS_FILE_NAMES if (hooks_dir / name).is_file()), None
|
|
454
|
+
)
|
|
455
|
+
if hooks_file is not None:
|
|
456
|
+
found.append(DormantProjectSource(key="project.hooks_enabled", path=hooks_file))
|
|
457
|
+
if not settings.project_skills_enabled:
|
|
458
|
+
skills_dir = project_root / ".agents" / "skills"
|
|
459
|
+
if not _is_same_entry(skills_dir, Path.home() / ".agents" / "skills") and _holds_skill(skills_dir):
|
|
460
|
+
found.append(DormantProjectSource(key="project.skills_enabled", path=skills_dir))
|
|
461
|
+
except OSError:
|
|
462
|
+
# A notice, never a reason to fail a settings load.
|
|
463
|
+
logger.debug("Could not check %s for project hooks or skills", project_root, exc_info=True)
|
|
464
|
+
return tuple(found)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@dataclass(frozen=True, slots=True)
|
|
468
|
+
class _FileLayers:
|
|
469
|
+
"""The file-borne layers, read once per load, whole.
|
|
470
|
+
|
|
471
|
+
Reading happens here and not in :func:`_layers` because a settings document
|
|
472
|
+
is parsed once for every key it mentions — one pinned field cannot excuse
|
|
473
|
+
the loader from reading the rest of the file.
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
user_yaml: Mapping[str, Any]
|
|
477
|
+
"""Dotted key → raw value, from ``~/.chrys/settings.yaml``."""
|
|
478
|
+
|
|
479
|
+
user_yaml_path: Path
|
|
480
|
+
user_env: Mapping[str, str]
|
|
481
|
+
"""Canonically folded env name → value, from ``~/.chrys/.env``."""
|
|
482
|
+
|
|
483
|
+
user_env_path: Path
|
|
484
|
+
unknown_keys: tuple[str, ...]
|
|
485
|
+
|
|
486
|
+
project_yaml: Mapping[str, Any] = _EMPTY_MAP
|
|
487
|
+
"""Dotted key → raw value, from ``<root>/.chrys/settings.yaml``.
|
|
488
|
+
|
|
489
|
+
The project layer's only file. A repository's ``.env`` is deliberately
|
|
490
|
+
not read: dotenv is the legacy channel on its way out, so its ``CHRYS_*``
|
|
491
|
+
lines stay inert rather than becoming a second project entrance.
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
project_yaml_unknown: tuple[str, ...] = ()
|
|
495
|
+
project_yaml_path: Path | None = None
|
|
496
|
+
"""``None`` when no distinct project document applies."""
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _read_file_layers(specs: Mapping[str, SettingSpec], project_root: Path | None) -> _FileLayers | None:
|
|
500
|
+
"""Read the file-borne layers, or ``None`` before bootstrap froze the env.
|
|
501
|
+
|
|
502
|
+
The snapshot gate is the hermeticity gate: a process that never called
|
|
503
|
+
:func:`freeze_process_env` — every test that constructs settings directly —
|
|
504
|
+
has not opted into this machine's home directory, so the files stay
|
|
505
|
+
unread rather than leaking one developer's ``~/.chrys`` into assertions.
|
|
506
|
+
It is also what the dotenv layer resolves against: values interpolate over
|
|
507
|
+
the environment as it was at bootstrap, never the mutated live one.
|
|
508
|
+
"""
|
|
509
|
+
snapshot = process_env_snapshot()
|
|
510
|
+
if snapshot is None:
|
|
511
|
+
return None
|
|
512
|
+
yaml_path = user_settings_path()
|
|
513
|
+
# ``None`` covers both "no document yet" and "nothing parseable", and an
|
|
514
|
+
# unparseable document must load as empty rather than not at all: the
|
|
515
|
+
# environment layers above it still apply, and the panel still opens.
|
|
516
|
+
try:
|
|
517
|
+
doc = read_yaml_doc(yaml_path) or {}
|
|
518
|
+
except TimeoutError:
|
|
519
|
+
# Another process was inside the document's lock for the whole wait.
|
|
520
|
+
# The same rule as an unparseable file, for a stronger reason: this
|
|
521
|
+
# read is on the startup path of every entry point, so raising here
|
|
522
|
+
# means a second Chrys cannot start at all while the first is stalled
|
|
523
|
+
# mid-write. The values genuinely are not in force this launch, which
|
|
524
|
+
# is exactly what provenance will report, and the dotenv layer below
|
|
525
|
+
# needs no lock and still applies.
|
|
526
|
+
logger.warning("Settings document was busy; loading without it: %s", yaml_path, exc_info=True)
|
|
527
|
+
doc = {}
|
|
528
|
+
known_keys = frozenset(entry.key for entry in specs.values())
|
|
529
|
+
values, unknown = flatten_user_doc(doc, known_keys)
|
|
530
|
+
env_path = config_env_path()
|
|
531
|
+
layer = read_dotenv_layer(env_path, base=snapshot.values)
|
|
532
|
+
# Fold names the way the OS does, so on Windows a lowercase spelling in
|
|
533
|
+
# the file still answers for the uppercase alias. Later spellings win the
|
|
534
|
+
# collision, matching dotenv's own later-assignment-wins rule.
|
|
535
|
+
user_env = {canonical_env_name(env_name): value for env_name, value in layer.items()}
|
|
536
|
+
|
|
537
|
+
project_yaml: Mapping[str, Any] = _EMPTY_MAP
|
|
538
|
+
project_yaml_unknown: tuple[str, ...] = ()
|
|
539
|
+
project_yaml_path: Path | None = None
|
|
540
|
+
if project_root is not None:
|
|
541
|
+
candidate = project_root / ".chrys" / "settings.yaml"
|
|
542
|
+
# A workspace rooted at the user's home names the user document again.
|
|
543
|
+
# It remains the trusted USER layer; reading it a second time as an
|
|
544
|
+
# untrusted PROJECT layer would reject preferences and, with the gate
|
|
545
|
+
# off, report the user's own settings as dormant project configuration.
|
|
546
|
+
if not _is_same_entry(candidate, yaml_path):
|
|
547
|
+
project_yaml_path = candidate
|
|
548
|
+
# Same empty-on-unparseable rule as the user document, read without
|
|
549
|
+
# lock, backup or repair — the file lives in someone's working tree.
|
|
550
|
+
project_doc = read_yaml_doc_readonly(project_yaml_path) or {}
|
|
551
|
+
project_yaml, project_yaml_unknown = flatten_user_doc(project_doc, known_keys)
|
|
552
|
+
|
|
553
|
+
return _FileLayers(
|
|
554
|
+
user_yaml=values,
|
|
555
|
+
user_yaml_path=yaml_path,
|
|
556
|
+
user_env=user_env,
|
|
557
|
+
user_env_path=env_path,
|
|
558
|
+
unknown_keys=unknown,
|
|
559
|
+
project_yaml=project_yaml,
|
|
560
|
+
project_yaml_unknown=project_yaml_unknown,
|
|
561
|
+
project_yaml_path=project_yaml_path,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@dataclass(frozen=True, slots=True)
|
|
566
|
+
class _ProjectLayers:
|
|
567
|
+
"""Phase-one verdicts for the project trust domain, taken whole.
|
|
568
|
+
|
|
569
|
+
``values`` holds only what survived the policy — already coerced to
|
|
570
|
+
canonical, so phase two contributes it without a second verdict and a
|
|
571
|
+
shadowed value never re-warns. Everything that did not survive is in
|
|
572
|
+
``warnings`` (or ``dormant``, when the gate was off and no per-key
|
|
573
|
+
question was ever asked).
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
values: Mapping[str, Any] = _EMPTY_MAP
|
|
577
|
+
"""Field name → canonical value accepted from ``<root>/.chrys/settings.yaml``."""
|
|
578
|
+
|
|
579
|
+
rejected: Mapping[str, Coerced] = _EMPTY_MAP
|
|
580
|
+
"""Field name → the phase-one verdict on a value the project may set but wrote wrong.
|
|
581
|
+
|
|
582
|
+
Only genuine value invalids — a policy rejection
|
|
583
|
+
(:attr:`CoerceReason.NOT_ALLOWED_IN_PROJECT`,
|
|
584
|
+
:attr:`CoerceReason.LOOSENS_USER_BASELINE`) means the layer had no standing
|
|
585
|
+
for the key at all and stays mute in phase two. A garbage value on a key the
|
|
586
|
+
whitelist *does* grant is a different thing: the layer had standing and
|
|
587
|
+
spoke unintelligibly, and phase two must see that so
|
|
588
|
+
:attr:`InvalidPolicy.SAFE_DEFAULT` can land the key on the built-in default
|
|
589
|
+
instead of letting the bad value uncap whatever a lower layer holds.
|
|
590
|
+
"""
|
|
591
|
+
|
|
592
|
+
warnings: tuple[SettingsWarning, ...] = ()
|
|
593
|
+
dormant: tuple[DormantProjectConfig, ...] = ()
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
_NO_PROJECT_LAYERS = _ProjectLayers()
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _user_doc_verdict(entry: SettingSpec, raw: Any) -> Coerced:
|
|
600
|
+
"""One user-document value's verdict, including the written-``null`` case."""
|
|
601
|
+
coerced = entry.coerce(raw)
|
|
602
|
+
if coerced.status is CoerceStatus.MISSING and raw is None and kind_accepts(entry.kind, raw):
|
|
603
|
+
# A written ``null`` is a value, not a blank. The document is typed
|
|
604
|
+
# — unlike an env string, which is why the coercers read a blank as
|
|
605
|
+
# "say nothing" — and :func:`persist` writes exactly this spelling
|
|
606
|
+
# for an ``OPTIONAL_INT`` the user cleared. Dropping it here would
|
|
607
|
+
# make "no timeout" unstorable: the panel writes ``null``, the load
|
|
608
|
+
# ignores it, and the field snaps back to its default with no
|
|
609
|
+
# warning. Only ``null`` is read back this way, and
|
|
610
|
+
# :func:`kind_accepts` is what makes that exact: it accepts ``None``
|
|
611
|
+
# for no other kind. ``""`` deliberately stays a blank — persisting
|
|
612
|
+
# an empty value removes the key instead of writing one, so an
|
|
613
|
+
# empty string in the document is a hand-edit that most likely
|
|
614
|
+
# means "unset", which is what falling through already gives.
|
|
615
|
+
coerced = Coerced(status=CoerceStatus.VALID, value=raw)
|
|
616
|
+
return coerced
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _default_user_baseline(specs: Mapping[str, SettingSpec], file_layers: _FileLayers) -> Settings:
|
|
620
|
+
"""``DEFAULT + USER`` merged — what every project verdict compares against.
|
|
621
|
+
|
|
622
|
+
The *semantic* baseline, never the effective value: the layers above the
|
|
623
|
+
project (env, CLI, pins) are other people's decisions for this process,
|
|
624
|
+
and measuring a repository's tightening against them would let the
|
|
625
|
+
environment move the fence the user set. This is also where the gate is
|
|
626
|
+
read — ``project.config_enabled`` counts only from here, because reading
|
|
627
|
+
it from any layer a project file feeds would let the layer authorise
|
|
628
|
+
itself.
|
|
629
|
+
"""
|
|
630
|
+
values: dict[str, Any] = {}
|
|
631
|
+
for name, entry in specs.items():
|
|
632
|
+
if not entry.persist or entry.key not in file_layers.user_yaml:
|
|
633
|
+
continue
|
|
634
|
+
coerced = _user_doc_verdict(entry, file_layers.user_yaml[entry.key])
|
|
635
|
+
if coerced.usable:
|
|
636
|
+
values[name] = coerced.value
|
|
637
|
+
return Settings(**values)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
_PROJECT_POLICY_REASONS = frozenset({CoerceReason.NOT_ALLOWED_IN_PROJECT, CoerceReason.LOOSENS_USER_BASELINE})
|
|
641
|
+
"""Rejections that mean "the project has no standing here", not "bad value"."""
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _project_verdict(
|
|
645
|
+
name: str,
|
|
646
|
+
entry: SettingSpec,
|
|
647
|
+
raw: Any,
|
|
648
|
+
*,
|
|
649
|
+
baseline: Settings,
|
|
650
|
+
baseline_values: Mapping[str, Any],
|
|
651
|
+
eval_context: EvalContext,
|
|
652
|
+
) -> Coerced:
|
|
653
|
+
"""One project value through the whole gauntlet, in the fixed order.
|
|
654
|
+
|
|
655
|
+
Policy first (a denied key's value is not worth a grammar opinion), then
|
|
656
|
+
the coercer to a canonical value, then the direction check against the
|
|
657
|
+
user baseline — on the *canonical* value, so ``60`` clamped to ``50`` is
|
|
658
|
+
judged as the ``50`` that would actually apply. ``MISSING`` means the
|
|
659
|
+
layer says nothing (``null`` and blanks are absences here, never values:
|
|
660
|
+
the written-``null`` reading belongs to the document the panel writes,
|
|
661
|
+
and nothing writes these).
|
|
662
|
+
"""
|
|
663
|
+
if entry.project_merge is ProjectMerge.DENY or not entry.persist:
|
|
664
|
+
return invalid(raw, CoerceReason.NOT_ALLOWED_IN_PROJECT)
|
|
665
|
+
coerced = entry.coerce(raw)
|
|
666
|
+
if not coerced.usable:
|
|
667
|
+
return coerced
|
|
668
|
+
if not _within_project_bounds(name, entry, coerced.value, baseline, baseline_values, eval_context):
|
|
669
|
+
return invalid(raw, CoerceReason.LOOSENS_USER_BASELINE)
|
|
670
|
+
return coerced
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _within_project_bounds(
|
|
674
|
+
name: str,
|
|
675
|
+
entry: SettingSpec,
|
|
676
|
+
value: Any,
|
|
677
|
+
baseline: Settings,
|
|
678
|
+
baseline_values: Mapping[str, Any],
|
|
679
|
+
eval_context: EvalContext,
|
|
680
|
+
) -> bool:
|
|
681
|
+
"""Whether *value* respects the field's merge direction over the baseline."""
|
|
682
|
+
merge = entry.project_merge
|
|
683
|
+
if merge is ProjectMerge.FREE:
|
|
684
|
+
return True
|
|
685
|
+
current = baseline_values[name]
|
|
686
|
+
if merge is ProjectMerge.ENABLE_ONLY:
|
|
687
|
+
return bool(value) or not bool(current)
|
|
688
|
+
if merge is ProjectMerge.DISABLE_ONLY:
|
|
689
|
+
return not bool(value) or bool(current)
|
|
690
|
+
# TIGHTEN_ONLY — the spec's ``__post_init__`` guarantees the comparator.
|
|
691
|
+
semantic = entry.semantic_value
|
|
692
|
+
if semantic is None:
|
|
693
|
+
raise RuntimeError("A tighten-only setting requires a semantic comparator.")
|
|
694
|
+
return semantic(replace(baseline, **{name: value}), eval_context) <= semantic(baseline, eval_context)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _project_layers(
|
|
698
|
+
specs: Mapping[str, SettingSpec],
|
|
699
|
+
file_layers: _FileLayers,
|
|
700
|
+
eval_context: EvalContext,
|
|
701
|
+
) -> _ProjectLayers:
|
|
702
|
+
"""Audit the project document completely — phase one of the load."""
|
|
703
|
+
yaml_path = file_layers.project_yaml_path
|
|
704
|
+
if yaml_path is None:
|
|
705
|
+
return _NO_PROJECT_LAYERS
|
|
706
|
+
|
|
707
|
+
baseline = _default_user_baseline(specs, file_layers)
|
|
708
|
+
if not baseline.project_config_enabled:
|
|
709
|
+
keys = (*file_layers.project_yaml, *file_layers.project_yaml_unknown)
|
|
710
|
+
if not keys:
|
|
711
|
+
return _NO_PROJECT_LAYERS
|
|
712
|
+
return _ProjectLayers(dormant=(DormantProjectConfig(path=yaml_path, keys=keys),))
|
|
713
|
+
|
|
714
|
+
baseline_values = asdict(baseline)
|
|
715
|
+
warnings: list[SettingsWarning] = []
|
|
716
|
+
origin = SettingOrigin(layer=Source.PROJECT, path=yaml_path)
|
|
717
|
+
|
|
718
|
+
# An unknown key in the *user* document is tolerated as a downgrade
|
|
719
|
+
# artefact; in a project file it is simply not something a repository
|
|
720
|
+
# may configure, and the warning has to name which file said so.
|
|
721
|
+
warnings.extend(
|
|
722
|
+
SettingsWarning(key=dotted, origin=origin, outcome=invalid("", CoerceReason.NOT_ALLOWED_IN_PROJECT))
|
|
723
|
+
for dotted in file_layers.project_yaml_unknown
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
by_key = {entry.key: (name, entry) for name, entry in specs.items()}
|
|
727
|
+
values: dict[str, Any] = {}
|
|
728
|
+
rejected: dict[str, Coerced] = {}
|
|
729
|
+
for dotted, raw in file_layers.project_yaml.items():
|
|
730
|
+
name, entry = by_key[dotted]
|
|
731
|
+
coerced = _project_verdict(
|
|
732
|
+
name,
|
|
733
|
+
entry,
|
|
734
|
+
raw,
|
|
735
|
+
baseline=baseline,
|
|
736
|
+
baseline_values=baseline_values,
|
|
737
|
+
eval_context=eval_context,
|
|
738
|
+
)
|
|
739
|
+
if coerced.status is CoerceStatus.MISSING:
|
|
740
|
+
continue
|
|
741
|
+
if not coerced.usable or coerced.status is CoerceStatus.CLAMPED:
|
|
742
|
+
warnings.append(SettingsWarning(key=dotted, origin=origin, outcome=coerced))
|
|
743
|
+
if coerced.usable:
|
|
744
|
+
values[name] = coerced.value
|
|
745
|
+
elif coerced.reason not in _PROJECT_POLICY_REASONS:
|
|
746
|
+
rejected[name] = coerced
|
|
747
|
+
|
|
748
|
+
return _ProjectLayers(values=values, rejected=rejected, warnings=tuple(warnings))
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _layers(
|
|
752
|
+
name: str,
|
|
753
|
+
entry: SettingSpec,
|
|
754
|
+
pins: Mapping[str, Any],
|
|
755
|
+
source: Mapping[str, str],
|
|
756
|
+
real_process: bool,
|
|
757
|
+
file_layers: _FileLayers | None,
|
|
758
|
+
project: _ProjectLayers,
|
|
759
|
+
) -> Iterator[tuple[Coerced, SettingOrigin]]:
|
|
760
|
+
"""Yield each layer's verdict for one field, **highest precedence first**.
|
|
761
|
+
|
|
762
|
+
Resolving downwards is what makes a rejected value local to its own layer.
|
|
763
|
+
Sweeping upwards instead — the shape this had while there was only one
|
|
764
|
+
layer — a high layer's typo could seal a key that a pin above it had
|
|
765
|
+
already answered, so the pin would be recorded as the effective source
|
|
766
|
+
while the value in force was the built-in default.
|
|
767
|
+
|
|
768
|
+
A generator, so a layer that nothing asks about costs nothing to skip.
|
|
769
|
+
|
|
770
|
+
Note what this is *not*: it is not where a file gets read. A settings
|
|
771
|
+
document is parsed once, whole, for every key it mentions — one pinned
|
|
772
|
+
field cannot excuse the loader from reading the rest of the file — and the
|
|
773
|
+
project layer's values must be policy-checked and their rejections reported
|
|
774
|
+
even when a higher layer shadows them. The reading happens in
|
|
775
|
+
:func:`_read_file_layers` before this function is ever called, and it keeps
|
|
776
|
+
the one job it has: picking the winner.
|
|
777
|
+
"""
|
|
778
|
+
if name in pins:
|
|
779
|
+
yield _pinned(entry, pins[name]), SettingOrigin(layer=Source.SESSION)
|
|
780
|
+
|
|
781
|
+
if entry.env is not None:
|
|
782
|
+
# The model-profile pointer is the one environment name that is *not* a
|
|
783
|
+
# bootstrap fact: activation, ``--model`` and session restore write it
|
|
784
|
+
# mid-run, and the reload that immediately follows has to see what was
|
|
785
|
+
# just written rather than the value the process started with. Its
|
|
786
|
+
# value and its origin are taken in a single read, because they are one
|
|
787
|
+
# state: fetched separately, a write landing between the two hands this
|
|
788
|
+
# layer one writer's value under another writer's origin, and naming
|
|
789
|
+
# the writer is the only thing the layer adds over the carrier. An
|
|
790
|
+
# unregistered value is a real shell export and stays ``ENV``. Injected
|
|
791
|
+
# test environments are hermetic, so the process-global registration
|
|
792
|
+
# only speaks for the process environment.
|
|
793
|
+
if real_process and entry.env in POINTER_ENV_NAMES:
|
|
794
|
+
raw, registered = get_model_pointer()
|
|
795
|
+
origin = registered if registered is not None else SettingOrigin(layer=Source.ENV)
|
|
796
|
+
else:
|
|
797
|
+
raw = source.get(entry.env, "")
|
|
798
|
+
origin = SettingOrigin(layer=Source.ENV)
|
|
799
|
+
# Presence is read off the value because the coercers already fold a
|
|
800
|
+
# blank string into ``MISSING``; an exported empty name never spoke.
|
|
801
|
+
if raw:
|
|
802
|
+
coerced = (entry.env_coerce or entry.coerce)(raw)
|
|
803
|
+
if coerced.status is not CoerceStatus.MISSING:
|
|
804
|
+
yield coerced, origin
|
|
805
|
+
|
|
806
|
+
if file_layers is None:
|
|
807
|
+
return
|
|
808
|
+
|
|
809
|
+
if entry.env is not None and (folded := canonical_env_name(entry.env)) in file_layers.user_env:
|
|
810
|
+
coerced = (entry.env_coerce or entry.coerce)(file_layers.user_env[folded])
|
|
811
|
+
if coerced.status is not CoerceStatus.MISSING:
|
|
812
|
+
yield coerced, SettingOrigin(layer=Source.USER_ENV, path=file_layers.user_env_path)
|
|
813
|
+
|
|
814
|
+
# The project layer contributes what phase one already judged, at the
|
|
815
|
+
# verdict it judged it — a shadowed project value was audited there, so
|
|
816
|
+
# nothing here may warn a second time. Accepted values arrive canonical;
|
|
817
|
+
# a value invalid arrives as its ``INVALID`` verdict, because the walk is
|
|
818
|
+
# where ``SAFE_DEFAULT`` turns "this layer wrote garbage on a dangerous
|
|
819
|
+
# key" into a seal, and a verdict that never enters the walk can never
|
|
820
|
+
# seal (the exact uncap-the-backstop path the policy exists to close).
|
|
821
|
+
if name in project.values:
|
|
822
|
+
yield (
|
|
823
|
+
Coerced(status=CoerceStatus.VALID, value=project.values[name]),
|
|
824
|
+
SettingOrigin(layer=Source.PROJECT, path=file_layers.project_yaml_path),
|
|
825
|
+
)
|
|
826
|
+
elif name in project.rejected:
|
|
827
|
+
yield project.rejected[name], SettingOrigin(layer=Source.PROJECT, path=file_layers.project_yaml_path)
|
|
828
|
+
|
|
829
|
+
# ``persist=False`` marks a field runtime-only: :func:`persist` refuses to
|
|
830
|
+
# write it, and for the same reason a hand-written spelling does not load.
|
|
831
|
+
if entry.persist and entry.key in file_layers.user_yaml:
|
|
832
|
+
coerced = _user_doc_verdict(entry, file_layers.user_yaml[entry.key])
|
|
833
|
+
if coerced.status is not CoerceStatus.MISSING:
|
|
834
|
+
yield coerced, SettingOrigin(layer=Source.USER, path=file_layers.user_yaml_path)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _pinned(entry: SettingSpec, value: Any) -> Coerced:
|
|
838
|
+
"""Accept one caller-supplied value for *entry*, or raise.
|
|
839
|
+
|
|
840
|
+
Pins come from our own code — a restored session, a frontend's own default
|
|
841
|
+
— so a wrong one is a bug no user can fix and no warning can route around.
|
|
842
|
+
They are also the one layer that reaches ``Settings`` without passing a
|
|
843
|
+
coercer, which is why the check has to be made here rather than trusted:
|
|
844
|
+
``tools.result.ceiling_tokens`` declares ``SAFE_DEFAULT`` precisely so a bad
|
|
845
|
+
value cannot uncap the backstop, and a pinned ``-1`` would do exactly that,
|
|
846
|
+
because the consumer reads any non-positive ceiling as "no ceiling".
|
|
847
|
+
|
|
848
|
+
Two checks, because neither alone is enough. :func:`kind_accepts` answers
|
|
849
|
+
the type question a coercer cannot (``None`` and ``""`` are real values for
|
|
850
|
+
the fields that declare them, not blanks to be dropped); the coercer answers
|
|
851
|
+
the domain question a type cannot. A value the coercer has nothing to say
|
|
852
|
+
about — those spellings, which come back ``MISSING`` — is taken verbatim.
|
|
853
|
+
|
|
854
|
+
``Kind.ENUM`` is the exception, because it is the one kind whose domain is
|
|
855
|
+
written down in full: ``choices`` *is* the domain, so a value outside it is
|
|
856
|
+
outside the domain by construction, and there is no unset spelling left over
|
|
857
|
+
to mean anything. Everywhere else the blank carries meaning the coercer
|
|
858
|
+
cannot see — ``model_profile=""`` is "no pinned profile", and ACP's
|
|
859
|
+
``ask_user_timeout_seconds=None`` is "no timeout, the client owns timing" —
|
|
860
|
+
so those stay verbatim. Without the exception a blank sails past the
|
|
861
|
+
canonical check below and lands in ``Settings`` as a value no ``choices``
|
|
862
|
+
list contains, and, because a rejected value is what seals a dangerous key,
|
|
863
|
+
it takes the seal with it: pinning ``default_approval_mode=""`` over an
|
|
864
|
+
``ENV`` value that failed its coercer leaves the key holding ``""``,
|
|
865
|
+
unsealed, credited to the pin.
|
|
866
|
+
"""
|
|
867
|
+
if not kind_accepts(entry.kind, value):
|
|
868
|
+
msg = f"{entry.key}: {type(value).__name__} is not a valid {entry.kind.name} value"
|
|
869
|
+
raise TypeError(msg)
|
|
870
|
+
coerced = entry.coerce(value)
|
|
871
|
+
if coerced.status is CoerceStatus.MISSING:
|
|
872
|
+
if entry.kind is Kind.ENUM:
|
|
873
|
+
# ``choices`` may be a ChoiceProvider, whose members only a higher
|
|
874
|
+
# layer can enumerate — name it rather than pretending to list it.
|
|
875
|
+
offered = list(entry.choices) if isinstance(entry.choices, tuple) else entry.choices
|
|
876
|
+
msg = f"{entry.key}: {value!r} is not one of {offered}"
|
|
877
|
+
raise TypeError(msg)
|
|
878
|
+
return Coerced(status=CoerceStatus.VALID, value=value)
|
|
879
|
+
if coerced.status is not CoerceStatus.VALID:
|
|
880
|
+
msg = f"{entry.key}: {value!r} is not a valid value ({coerced.status.name.lower()})"
|
|
881
|
+
raise TypeError(msg)
|
|
882
|
+
if coerced.value != value:
|
|
883
|
+
# A pin that only survives normalisation is still a caller passing the
|
|
884
|
+
# wrong thing: it would be stored as one value and echoed back to the
|
|
885
|
+
# panel as another, and the caller would never learn which won.
|
|
886
|
+
msg = f"{entry.key}: {value!r} is not canonical; pass {coerced.value!r}"
|
|
887
|
+
raise TypeError(msg)
|
|
888
|
+
return coerced
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
_APPROVAL_MODE_KEY: Final = "approval.default_mode"
|
|
892
|
+
|
|
893
|
+
_KIND_REASONS: Final = {
|
|
894
|
+
Kind.BOOL: CoerceReason.EXPECTED_BOOL,
|
|
895
|
+
Kind.INT: CoerceReason.EXPECTED_INT,
|
|
896
|
+
Kind.OPTIONAL_INT: CoerceReason.EXPECTED_INT,
|
|
897
|
+
Kind.FLOAT: CoerceReason.EXPECTED_NUMBER,
|
|
898
|
+
Kind.TEXT: CoerceReason.EXPECTED_TEXT,
|
|
899
|
+
Kind.ENUM: CoerceReason.NOT_A_CHOICE,
|
|
900
|
+
Kind.PATH: CoerceReason.EXPECTED_TEXT,
|
|
901
|
+
}
|
|
902
|
+
"""How to word the rejection when a value's *type* is what disqualified it."""
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
@dataclass(frozen=True, slots=True)
|
|
906
|
+
class PersistResult:
|
|
907
|
+
"""What one :func:`persist` call did, in full.
|
|
908
|
+
|
|
909
|
+
Mirrors :class:`LoadedSettings`' contract: a caller gets the outcome and
|
|
910
|
+
the rejections together, and the panel that ignores ``rejected`` is the
|
|
911
|
+
panel that cannot say why a field snapped back to its old value.
|
|
912
|
+
"""
|
|
913
|
+
|
|
914
|
+
written: Mapping[str, Any]
|
|
915
|
+
"""Dotted key → the canonical value actually stored (post-clamp)."""
|
|
916
|
+
|
|
917
|
+
rejected: Mapping[str, Coerced]
|
|
918
|
+
"""Dotted key → the verdict that kept the whole batch out of the file."""
|
|
919
|
+
|
|
920
|
+
@property
|
|
921
|
+
def ok(self) -> bool:
|
|
922
|
+
return not self.rejected
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def persist(
|
|
926
|
+
updates: Mapping[str, Any],
|
|
927
|
+
*,
|
|
928
|
+
remove: Iterable[str] = (),
|
|
929
|
+
lock_timeout: float = LOCK_TIMEOUT_SECONDS,
|
|
930
|
+
) -> PersistResult:
|
|
931
|
+
"""Write dotted-key values into the user settings document, all or nothing.
|
|
932
|
+
|
|
933
|
+
Validation happens first, against the same coercers the loader applies on
|
|
934
|
+
the way back in, and one rejection keeps the entire batch out of the file:
|
|
935
|
+
a partial write would leave the document describing a state no caller ever
|
|
936
|
+
asked for. Clamped values are stored at their clamped canonical — the file
|
|
937
|
+
holds what will be in effect, not what was typed. ``remove`` deletes keys;
|
|
938
|
+
a batch of only removals is a legal write.
|
|
939
|
+
|
|
940
|
+
An unknown key or a runtime-only (``persist=False``) key raises instead of
|
|
941
|
+
rejecting: both are our-code bugs no user input can produce, exactly like
|
|
942
|
+
a bad pin.
|
|
943
|
+
|
|
944
|
+
``approval.default_mode`` is the one key with a store-time rule of its
|
|
945
|
+
own: ``bypass`` is a per-launch decision, so persisting it downgrades to
|
|
946
|
+
``auto`` rather than arming every future session.
|
|
947
|
+
"""
|
|
948
|
+
specs = specs_by_field(Settings)
|
|
949
|
+
by_key = {entry.key: entry for entry in specs.values()}
|
|
950
|
+
removals = tuple(dict.fromkeys(remove))
|
|
951
|
+
path = user_settings_path()
|
|
952
|
+
|
|
953
|
+
unknown = sorted(key for key in {*updates, *removals} if key not in by_key)
|
|
954
|
+
if unknown:
|
|
955
|
+
msg = f"Unknown settings keys: {', '.join(unknown)}"
|
|
956
|
+
raise TypeError(msg)
|
|
957
|
+
runtime_only = sorted(key for key in {*updates, *removals} if not by_key[key].persist)
|
|
958
|
+
if runtime_only:
|
|
959
|
+
msg = f"Runtime-only settings keys cannot be persisted: {', '.join(runtime_only)}"
|
|
960
|
+
raise TypeError(msg)
|
|
961
|
+
|
|
962
|
+
written: dict[str, Any] = {}
|
|
963
|
+
rejected: dict[str, Coerced] = {}
|
|
964
|
+
for key, value in updates.items():
|
|
965
|
+
entry = by_key[key]
|
|
966
|
+
coerced = entry.coerce(value)
|
|
967
|
+
if coerced.status is CoerceStatus.MISSING:
|
|
968
|
+
# The unset spellings are real values for the fields that declare
|
|
969
|
+
# them (``""`` is "no pinned profile"), same as for a pin — and
|
|
970
|
+
# ``ENUM`` is the same exception, its domain being written in full.
|
|
971
|
+
if kind_accepts(entry.kind, value) and entry.kind is not Kind.ENUM:
|
|
972
|
+
written[key] = value
|
|
973
|
+
else:
|
|
974
|
+
rejected[key] = _kind_rejection(entry, value)
|
|
975
|
+
continue
|
|
976
|
+
if coerced.status is CoerceStatus.INVALID:
|
|
977
|
+
rejected[key] = coerced
|
|
978
|
+
continue
|
|
979
|
+
written[key] = coerced.value
|
|
980
|
+
|
|
981
|
+
if written.get(_APPROVAL_MODE_KEY) == "bypass":
|
|
982
|
+
written[_APPROVAL_MODE_KEY] = "auto"
|
|
983
|
+
|
|
984
|
+
if rejected:
|
|
985
|
+
return PersistResult(written={}, rejected=rejected)
|
|
986
|
+
if written or removals:
|
|
987
|
+
update_yaml_doc(path, lambda doc: apply_settings_patch(doc, written, removals), lock_timeout=lock_timeout)
|
|
988
|
+
return PersistResult(written=written, rejected={})
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def _kind_rejection(entry: SettingSpec, value: Any) -> Coerced:
|
|
992
|
+
"""Word the refusal of a value whose type no coercer had a verdict for."""
|
|
993
|
+
if entry.kind is Kind.ENUM:
|
|
994
|
+
choices = entry.choices if isinstance(entry.choices, tuple) else ()
|
|
995
|
+
return invalid(value, CoerceReason.NOT_A_CHOICE, choices=choices)
|
|
996
|
+
return invalid(value, _KIND_REASONS[entry.kind])
|