agentirc-cli 7.0.4__tar.gz → 7.1.0__tar.gz
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.
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/CHANGELOG.md +15 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/PKG-INFO +1 -1
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/client.py +63 -30
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/ircd.py +3 -1
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/config.py +18 -1
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/mesh.py +24 -3
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/server.py +5 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/persistence.py +34 -15
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/agentirc/bots.md +7 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/cli/index.md +6 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/pyproject.toml +1 -1
- agentirc_cli-7.1.0/tests/test_bot_config_fires_event_toplevel.py +114 -0
- agentirc_cli-7.1.0/tests/test_persistence_timeout.py +59 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/uv.lock +1 -1
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.claude/agents/doc-test-alignment.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.flake8 +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.github/workflows/docs-check.yml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.github/workflows/security-checks.yml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.gitignore +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.pr_agent.toml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.pre-commit-config.yaml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/.pylintrc +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/CLAUDE.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/Gemfile +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/Gemfile.lock +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/LICENSE +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/README.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/SECURITY.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_config.agentirc.yml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_config.base.yml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_config.culture.yml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_data/sites.yml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_includes/head_custom.html +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_plugins/site_filter.rb +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_sass/color_schemes/dark-terminal.scss +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/assets/images/IMG_3183.png +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/assets/images/apple-touch-icon.png +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/assets/images/favicon-16x16.png +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/assets/images/favicon-32x32.png +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/assets/images/favicon.ico +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/assets/images/og-agentirc.png +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/assets/images/og-culture.png +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/__main__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/CLAUDE.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/__main__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/channel.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/docs/agentirc-architecture.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/docs/agentirc-features.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/docs/agentirc-skill.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/docs/agentirc.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/events.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/history_store.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/remote_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/room_store.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/rooms_util.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/server_link.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/skill.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/skills/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/skills/history.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/skills/icon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/skills/rooms.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/skills/threads.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/agentirc/thread_store.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/aio.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/bot.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/bot_manager.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/filter_dsl.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/http_listener.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/system/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/system/welcome/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/system/welcome/bot.yaml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/system/welcome/handler.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/template_engine.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/bots/virtual_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/agent.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/bot.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/channel.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/shared/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/shared/constants.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/shared/display.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/shared/formatting.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/shared/ipc.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/shared/mesh.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/shared/process.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/cli/skills.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/culture.yaml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/ipc.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/irc_transport.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/message_buffer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/skill/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/skill/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/skill/irc_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/socket_server.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/supervisor.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/acp/webhook.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/__main__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/culture.yaml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/ipc.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/socket_server.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/supervisor.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/claude/webhook.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/culture.yaml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/ipc.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/socket_server.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/supervisor.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/codex/webhook.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/culture.yaml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/ipc.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/irc_transport.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/clients/copilot/webhook.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/app.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/commands.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/status.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/widgets/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/widgets/chat.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/widgets/info_panel.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/console/widgets/sidebar.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/constants.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/credentials.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/formatting.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/learn_prompt.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/mesh_config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/observer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/overview/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/overview/collector.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/overview/model.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/overview/renderer_text.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/overview/renderer_web.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/overview/web/style.css +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/pidfile.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/commands.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/extensions/events.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/extensions/federation.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/extensions/history.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/extensions/icons.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/extensions/tags.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/extensions/threads.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/message.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/protocol-index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/protocol/replies.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/culture/skills/culture/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/README.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/agentirc/architecture-overview.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/agentirc/events.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/agentirc/index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/agentirc/why-agentirc.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/agent-lifecycle.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/choose-a-harness.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/mental-model.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/operate.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/patterns.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/quickstart.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/reflective-development.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/vision-patterns-index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/vision.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/culture/why-culture.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/architecture/agent-harness-spec.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/architecture/index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/architecture/layers.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/architecture/threads.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/cli/commands.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/harnesses/acp.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/harnesses/claude.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/harnesses/codex.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/harnesses/copilot.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/harnesses/index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/server/architecture.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/server/config.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/server/deployment.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/server/index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/reference/server/security.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/concepts/federation.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/concepts/harnesses.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/concepts/humans-and-agents.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/concepts/index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/concepts/persistence.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/concepts/rooms.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/demos/magic-demo.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/guides/first-session.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/guides/index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/guides/join-as-human.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/guides/local-setup.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/guides/multi-machine.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases/10-agent-lifecycle.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/shared/use-cases-index.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-04-09-decentralized-agent-config.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-04-12-console-enhancements.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/plans/2026-04-15-mesh-events.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-07-reflective-development-reframe-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-08-reflective-development-deepening-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-09-decentralized-agent-config-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-12-console-enhancements-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/docs/superpowers/specs/2026-04-15-mesh-events-design.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/favicon.ico +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/culture.yaml +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/plugins/claude-code/skills/culture/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/robots.txt +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/sonar-project.properties +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/__init__.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/conftest.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_archive.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_bot.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_bot_config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_bot_manager.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_bots_integration.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_channel.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_channel_cli.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_connection.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_console_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_console_commands.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_console_connection.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_console_fixes_224_227.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_console_icons.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_console_integration.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_console_status.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_credentials.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_culture_config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_display.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_basic.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_bot_chain.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_bot_trigger.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_cap_fallback.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_catalog.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_federation.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_history.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_lifecycle.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_events_reserved_nick.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_federation.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_filter_dsl.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_history.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_http_listener.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_irc_transport_tags.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_learn_prompt.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_manifest_config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_mention_alias.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_mention_target_cleanup.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_mention_warning.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_mesh_readiness.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_message.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_message_tags.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_migrate_cli.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_modes.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_overview_model.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_overview_web.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_persistence.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_pidfile.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_poll_loop.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_register_cli.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_rooms.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_skill_docs.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_skills.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_template_engine.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_thread_buffer.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_threads.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_virtual_client.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_wait_for_port.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_webhook.py +0 -0
- {agentirc_cli-7.0.4 → agentirc_cli-7.1.0}/tests/test_welcome_bot.py +0 -0
|
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [7.1.0] - 2026-04-17
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Refactored _handle_channel_mode in culture/agentirc/client.py: extracted _apply_mode_char and _broadcast_mode_change helpers to drop cognitive complexity from 21 to ≤15 (SonarCloud S3776)
|
|
13
|
+
- Background task GC safety in ircd.py _notify_local_quit: asyncio.ensure_future replaced with a tracked asyncio.create_task using the existing self._background_tasks set + add_done_callback(discard) pattern
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- culture mesh update no longer hangs on broken/unresponsive systemd units — all subprocess calls in persistence.py and cli/mesh.py now have explicit timeouts (30s for service restarts, 30s for CLI fallbacks, 120s for package upgrade)
|
|
19
|
+
- fires_event chain not triggering downstream bots (#260) — the bot.yaml loader now accepts fires_event at the top level as well as under output:, so configs that put the block at the top level emit events as expected
|
|
20
|
+
- Daemon log flushing stops after startup — replaced logging.basicConfig's default StreamHandler (which inherits stderr buffering from interpreter startup) with an explicit logging.FileHandler so runtime log records flush per-record
|
|
21
|
+
|
|
7
22
|
## [7.0.4] - 2026-04-17
|
|
8
23
|
|
|
9
24
|
|
|
@@ -444,6 +444,56 @@ class Client:
|
|
|
444
444
|
applied_modes.append(("+" if adding else "-") + ch)
|
|
445
445
|
applied_params.append(target_nick)
|
|
446
446
|
|
|
447
|
+
_PARAM_MODES = frozenset({"o", "v", "S"})
|
|
448
|
+
|
|
449
|
+
async def _apply_mode_char(
|
|
450
|
+
self,
|
|
451
|
+
channel,
|
|
452
|
+
channel_name: str,
|
|
453
|
+
ch: str,
|
|
454
|
+
adding: bool,
|
|
455
|
+
param_queue: list[str],
|
|
456
|
+
applied_modes: list[str],
|
|
457
|
+
applied_params: list[str],
|
|
458
|
+
) -> None:
|
|
459
|
+
"""Apply a single mode character. Consumes one param from param_queue when needed."""
|
|
460
|
+
if ch == "R":
|
|
461
|
+
self._apply_mode_r(channel, adding, applied_modes)
|
|
462
|
+
return
|
|
463
|
+
if ch not in self._PARAM_MODES or not param_queue:
|
|
464
|
+
return
|
|
465
|
+
param_value = param_queue.pop(0)
|
|
466
|
+
if ch == "S":
|
|
467
|
+
self._apply_mode_s(channel, adding, param_value, applied_modes, applied_params)
|
|
468
|
+
else:
|
|
469
|
+
await self._apply_mode_membership(
|
|
470
|
+
channel,
|
|
471
|
+
channel_name,
|
|
472
|
+
ch,
|
|
473
|
+
adding,
|
|
474
|
+
param_value,
|
|
475
|
+
applied_modes,
|
|
476
|
+
applied_params,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
async def _broadcast_mode_change(
|
|
480
|
+
self,
|
|
481
|
+
channel,
|
|
482
|
+
channel_name: str,
|
|
483
|
+
applied_modes: list[str],
|
|
484
|
+
applied_params: list[str],
|
|
485
|
+
) -> None:
|
|
486
|
+
"""Send the aggregated MODE message to all channel members."""
|
|
487
|
+
if not applied_modes:
|
|
488
|
+
return
|
|
489
|
+
mode_msg = Message(
|
|
490
|
+
prefix=self.prefix,
|
|
491
|
+
command="MODE",
|
|
492
|
+
params=[channel_name, "".join(applied_modes)] + applied_params,
|
|
493
|
+
)
|
|
494
|
+
for member in list(channel.members):
|
|
495
|
+
await member.send(mode_msg)
|
|
496
|
+
|
|
447
497
|
async def _handle_channel_mode(self, msg: Message) -> None:
|
|
448
498
|
channel_name = msg.params[0]
|
|
449
499
|
channel = self.server.channels.get(channel_name)
|
|
@@ -457,7 +507,6 @@ class Client:
|
|
|
457
507
|
await self.send_numeric(replies.RPL_CHANNELMODEIS, channel_name, "+")
|
|
458
508
|
return
|
|
459
509
|
|
|
460
|
-
modestring = msg.params[1]
|
|
461
510
|
if not channel.is_operator(self):
|
|
462
511
|
await self.send_numeric(
|
|
463
512
|
replies.ERR_CHANOPRIVSNEEDED,
|
|
@@ -466,48 +515,32 @@ class Client:
|
|
|
466
515
|
)
|
|
467
516
|
return
|
|
468
517
|
|
|
518
|
+
modestring = msg.params[1]
|
|
469
519
|
param_queue = list(msg.params[2:])
|
|
470
|
-
param_modes = {"o", "v", "S"}
|
|
471
|
-
|
|
472
520
|
adding = True
|
|
473
|
-
applied_modes = []
|
|
521
|
+
applied_modes: list[str] = []
|
|
474
522
|
applied_params: list[str] = []
|
|
475
523
|
for ch in modestring:
|
|
476
524
|
if ch == "+":
|
|
477
525
|
adding = True
|
|
478
526
|
elif ch == "-":
|
|
479
527
|
adding = False
|
|
480
|
-
|
|
481
|
-
self.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
channel,
|
|
491
|
-
channel_name,
|
|
492
|
-
ch,
|
|
493
|
-
adding,
|
|
494
|
-
param_value,
|
|
495
|
-
applied_modes,
|
|
496
|
-
applied_params,
|
|
497
|
-
)
|
|
528
|
+
else:
|
|
529
|
+
await self._apply_mode_char(
|
|
530
|
+
channel,
|
|
531
|
+
channel_name,
|
|
532
|
+
ch,
|
|
533
|
+
adding,
|
|
534
|
+
param_queue,
|
|
535
|
+
applied_modes,
|
|
536
|
+
applied_params,
|
|
537
|
+
)
|
|
498
538
|
|
|
499
539
|
# Auto-promote if no operators remain
|
|
500
540
|
if not channel.operators and channel.members:
|
|
501
541
|
channel.operators.add(min(channel.members, key=lambda m: m.nick))
|
|
502
542
|
|
|
503
|
-
|
|
504
|
-
mode_msg = Message(
|
|
505
|
-
prefix=self.prefix,
|
|
506
|
-
command="MODE",
|
|
507
|
-
params=[channel_name, "".join(applied_modes)] + applied_params,
|
|
508
|
-
)
|
|
509
|
-
for member in list(channel.members):
|
|
510
|
-
await member.send(mode_msg)
|
|
543
|
+
await self._broadcast_mode_change(channel, channel_name, applied_modes, applied_params)
|
|
511
544
|
|
|
512
545
|
_VALID_USER_MODE_CHARS = frozenset("HABC")
|
|
513
546
|
_USER_MODE_EDGE_EVENTS: dict[tuple[str, bool], EventType] = {
|
|
@@ -562,7 +562,9 @@ class IRCd:
|
|
|
562
562
|
for channel in list(rc.channels):
|
|
563
563
|
for member in list(channel.members):
|
|
564
564
|
if not isinstance(member, RemoteClient) and member not in notified:
|
|
565
|
-
asyncio.
|
|
565
|
+
task = asyncio.create_task(member.send(quit_msg))
|
|
566
|
+
self._background_tasks.add(task)
|
|
567
|
+
task.add_done_callback(self._background_tasks.discard)
|
|
566
568
|
notified.add(member)
|
|
567
569
|
channel.members.discard(rc)
|
|
568
570
|
if not channel.members:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import os
|
|
6
7
|
import tempfile
|
|
7
8
|
from dataclasses import dataclass, field
|
|
@@ -10,6 +11,8 @@ from typing import Any
|
|
|
10
11
|
|
|
11
12
|
import yaml
|
|
12
13
|
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
13
16
|
BOTS_DIR = Path(os.path.expanduser("~/.culture/bots"))
|
|
14
17
|
BOT_CONFIG_FILE = "bot.yaml"
|
|
15
18
|
|
|
@@ -58,8 +61,16 @@ def load_bot_config(path: Path) -> BotConfig:
|
|
|
58
61
|
trigger_section = raw.get("trigger", {})
|
|
59
62
|
output_section = raw.get("output", {})
|
|
60
63
|
|
|
61
|
-
# Parse optional fires_event block
|
|
64
|
+
# Parse optional fires_event block. Canonical location is under `output:`;
|
|
65
|
+
# top-level `fires_event:` is also accepted so configs authored against the
|
|
66
|
+
# intuitive YAML shape still work (see issue #260).
|
|
62
67
|
fires_event_raw = output_section.get("fires_event")
|
|
68
|
+
fires_event_from_top = False
|
|
69
|
+
if not isinstance(fires_event_raw, dict):
|
|
70
|
+
top_level = raw.get("fires_event")
|
|
71
|
+
if isinstance(top_level, dict):
|
|
72
|
+
fires_event_raw = top_level
|
|
73
|
+
fires_event_from_top = True
|
|
63
74
|
fires_event: EmitEventSpec | None = None
|
|
64
75
|
if isinstance(fires_event_raw, dict):
|
|
65
76
|
fe_type = fires_event_raw.get("type", "")
|
|
@@ -69,6 +80,12 @@ def load_bot_config(path: Path) -> BotConfig:
|
|
|
69
80
|
if not isinstance(fe_data, dict):
|
|
70
81
|
fe_data = {}
|
|
71
82
|
fires_event = EmitEventSpec(type=fe_type, data=fe_data)
|
|
83
|
+
if fires_event_from_top:
|
|
84
|
+
logger.info(
|
|
85
|
+
"Bot %s: top-level 'fires_event' accepted; "
|
|
86
|
+
"canonical location is under 'output:'",
|
|
87
|
+
bot_section.get("name", "<unknown>"),
|
|
88
|
+
)
|
|
72
89
|
|
|
73
90
|
return BotConfig(
|
|
74
91
|
name=bot_section.get("name", ""),
|
|
@@ -432,10 +432,24 @@ def _find_upgrade_tool() -> tuple[str, list[str]] | None:
|
|
|
432
432
|
return None
|
|
433
433
|
|
|
434
434
|
|
|
435
|
+
_UPGRADE_TIMEOUT_SECONDS = 120
|
|
436
|
+
_FALLBACK_START_TIMEOUT_SECONDS = 30
|
|
437
|
+
|
|
438
|
+
|
|
435
439
|
def _run_upgrade(tool_name: str, cmd: list[str]) -> None:
|
|
436
|
-
"""Run the upgrade subprocess and exit on failure."""
|
|
440
|
+
"""Run the upgrade subprocess and exit on failure or timeout."""
|
|
437
441
|
print(f"Upgrading via {tool_name}...")
|
|
438
|
-
|
|
442
|
+
try:
|
|
443
|
+
result = subprocess.run(
|
|
444
|
+
cmd, capture_output=True, text=True, timeout=_UPGRADE_TIMEOUT_SECONDS
|
|
445
|
+
)
|
|
446
|
+
except subprocess.TimeoutExpired:
|
|
447
|
+
print(
|
|
448
|
+
f"{tool_name} upgrade timed out after {_UPGRADE_TIMEOUT_SECONDS}s — "
|
|
449
|
+
"rerun with --skip-upgrade to proceed without upgrading",
|
|
450
|
+
file=sys.stderr,
|
|
451
|
+
)
|
|
452
|
+
sys.exit(1)
|
|
439
453
|
if tool_name == "uv":
|
|
440
454
|
print(result.stdout.strip() if result.stdout else "")
|
|
441
455
|
if result.returncode != 0:
|
|
@@ -523,7 +537,14 @@ def _restart_single_service(svc_name: str, fallback_cmd: list[str], restart_serv
|
|
|
523
537
|
)
|
|
524
538
|
else:
|
|
525
539
|
print(" No service file found, starting via CLI...")
|
|
526
|
-
|
|
540
|
+
try:
|
|
541
|
+
subprocess.run(fallback_cmd, check=False, timeout=_FALLBACK_START_TIMEOUT_SECONDS)
|
|
542
|
+
except subprocess.TimeoutExpired:
|
|
543
|
+
print(
|
|
544
|
+
f" Warning: startup command for {svc_name} timed out after "
|
|
545
|
+
f"{_FALLBACK_START_TIMEOUT_SECONDS}s — continuing",
|
|
546
|
+
file=sys.stderr,
|
|
547
|
+
)
|
|
527
548
|
|
|
528
549
|
|
|
529
550
|
def _restart_mesh_services(
|
|
@@ -371,9 +371,14 @@ def _daemonize_server(args: argparse.Namespace, pid_name: str, links: list) -> N
|
|
|
371
371
|
os.dup2(devnull, 0)
|
|
372
372
|
os.close(devnull)
|
|
373
373
|
|
|
374
|
+
# Use an explicit FileHandler: logging.StreamHandler on sys.stderr
|
|
375
|
+
# inherits stderr's buffering from interpreter startup. After dup2'ing
|
|
376
|
+
# fd 2 to a log file, those writes can buffer indefinitely and make
|
|
377
|
+
# the daemon's runtime log appear frozen.
|
|
374
378
|
logging.basicConfig(
|
|
375
379
|
level=logging.INFO,
|
|
376
380
|
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
381
|
+
handlers=[logging.FileHandler(log_path)],
|
|
377
382
|
force=True,
|
|
378
383
|
)
|
|
379
384
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import os
|
|
6
7
|
import shlex
|
|
7
8
|
import subprocess
|
|
@@ -11,6 +12,10 @@ from xml.sax.saxutils import escape as xml_escape
|
|
|
11
12
|
|
|
12
13
|
LOG_DIR = os.path.expanduser("~/.culture/logs")
|
|
13
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
DEFAULT_CMD_TIMEOUT = 30.0
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
def get_platform() -> str:
|
|
16
21
|
"""Detect the current platform."""
|
|
@@ -33,9 +38,20 @@ def _windows_service_dir() -> Path:
|
|
|
33
38
|
return Path(os.path.expandvars(r"%USERPROFILE%\.culture\services"))
|
|
34
39
|
|
|
35
40
|
|
|
36
|
-
def _run_cmd(args: list[str]) ->
|
|
37
|
-
"""Run a command, suppressing output.
|
|
38
|
-
|
|
41
|
+
def _run_cmd(args: list[str], timeout: float = DEFAULT_CMD_TIMEOUT) -> bool:
|
|
42
|
+
"""Run a command, suppressing output.
|
|
43
|
+
|
|
44
|
+
Returns True if the command completed (regardless of exit code), False
|
|
45
|
+
if it timed out. A hung systemd unit can make ``systemctl restart``
|
|
46
|
+
block indefinitely, so every caller needs a bounded wait.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
subprocess.run(args, check=False, capture_output=True, timeout=timeout)
|
|
50
|
+
return True
|
|
51
|
+
except subprocess.TimeoutExpired:
|
|
52
|
+
command = shlex.join(args) if args else "<empty command>"
|
|
53
|
+
logger.warning("Command %s timed out after %.0fs", command, timeout)
|
|
54
|
+
return False
|
|
39
55
|
|
|
40
56
|
|
|
41
57
|
# ---------------------------------------------------------------------------
|
|
@@ -248,8 +264,7 @@ def _restart_linux_service(name: str) -> bool:
|
|
|
248
264
|
path = _systemd_user_dir() / f"{name}.service"
|
|
249
265
|
if not path.exists():
|
|
250
266
|
return False
|
|
251
|
-
_run_cmd(["systemctl", "--user", "restart", name])
|
|
252
|
-
return True
|
|
267
|
+
return _run_cmd(["systemctl", "--user", "restart", name])
|
|
253
268
|
|
|
254
269
|
|
|
255
270
|
def _restart_macos_service(name: str) -> bool:
|
|
@@ -257,21 +272,25 @@ def _restart_macos_service(name: str) -> bool:
|
|
|
257
272
|
path = _launchd_dir() / f"{plist_name}.plist"
|
|
258
273
|
if not path.exists():
|
|
259
274
|
return False
|
|
260
|
-
_run_cmd(["launchctl", "unload", str(path)])
|
|
261
|
-
|
|
262
|
-
return
|
|
275
|
+
if not _run_cmd(["launchctl", "unload", str(path)]):
|
|
276
|
+
return False
|
|
277
|
+
return _run_cmd(["launchctl", "load", str(path)])
|
|
263
278
|
|
|
264
279
|
|
|
265
280
|
def _restart_windows_service(name: str) -> bool:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
try:
|
|
282
|
+
probe = subprocess.run(
|
|
283
|
+
["schtasks", "/Query", "/TN", f"culture\\{name}"],
|
|
284
|
+
capture_output=True,
|
|
285
|
+
text=True,
|
|
286
|
+
timeout=DEFAULT_CMD_TIMEOUT,
|
|
287
|
+
)
|
|
288
|
+
except subprocess.TimeoutExpired:
|
|
289
|
+
logger.warning("schtasks query for %s timed out", name)
|
|
290
|
+
return False
|
|
271
291
|
if probe.returncode != 0:
|
|
272
292
|
return False
|
|
273
|
-
_run_cmd(["schtasks", "/Run", "/TN", f"culture\\{name}"])
|
|
274
|
-
return True
|
|
293
|
+
return _run_cmd(["schtasks", "/Run", "/TN", f"culture\\{name}"])
|
|
275
294
|
|
|
276
295
|
|
|
277
296
|
_PLATFORM_RESTARTERS = {
|
|
@@ -106,6 +106,13 @@ output:
|
|
|
106
106
|
The emitted event flows through the same pipeline as any other event — skills,
|
|
107
107
|
federation relay, history storage, and further bot triggers.
|
|
108
108
|
|
|
109
|
+
> **Location tolerance.** The canonical location for `fires_event` is under
|
|
110
|
+
> `output:` as shown above. A top-level `fires_event:` block is also accepted
|
|
111
|
+
> (the loader falls back to it if `output.fires_event` is missing) so configs
|
|
112
|
+
> authored against the intuitive top-level shape still work. When the top-level
|
|
113
|
+
> form is used, the server logs an INFO message pointing to the canonical
|
|
114
|
+
> location; `save_bot_config` always writes the canonical form.
|
|
115
|
+
|
|
109
116
|
## System Bots
|
|
110
117
|
|
|
111
118
|
System bots are package-bundled bots that ship with AgentIRC. They live at
|
|
@@ -363,6 +363,12 @@ culture mesh update --config /path/mesh.yaml
|
|
|
363
363
|
| `--skip-upgrade` | off | Skip the package upgrade step; just restart services |
|
|
364
364
|
| `--config PATH` | `~/.culture/mesh.yaml` | Path to `mesh.yaml` |
|
|
365
365
|
|
|
366
|
+
Subprocess steps are bounded so a hung service unit cannot freeze the CLI: the
|
|
367
|
+
package upgrade times out after 120s and aborts the command, while each
|
|
368
|
+
service-restart command times out after 30s and the fallback
|
|
369
|
+
`culture server start` step times out after 30s. Restart and fallback
|
|
370
|
+
timeouts are reported on stderr, and the next step proceeds where applicable.
|
|
371
|
+
|
|
366
372
|
## Bots
|
|
367
373
|
|
|
368
374
|
### `culture bot archive`
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Regression tests for issue #260: `fires_event` at the top level of bot.yaml.
|
|
2
|
+
|
|
3
|
+
The canonical location for the block is under `output:`, but prior to the
|
|
4
|
+
fix a top-level `fires_event:` block was silently ignored, producing a bot
|
|
5
|
+
whose `fires_event` attribute was ``None`` — so chained events never fired.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from culture.bots.config import load_bot_config, save_bot_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _write_yaml(tmp_path, data: dict):
|
|
16
|
+
path = tmp_path / "bot.yaml"
|
|
17
|
+
path.write_text(yaml.safe_dump(data))
|
|
18
|
+
return path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_load_top_level_fires_event(tmp_path):
|
|
22
|
+
path = _write_yaml(
|
|
23
|
+
tmp_path,
|
|
24
|
+
{
|
|
25
|
+
"bot": {"name": "spark-greeter", "owner": "spark"},
|
|
26
|
+
"trigger": {"type": "event", "filter": "type == 'user.join'"},
|
|
27
|
+
"output": {"channels": ["#general"], "template": "hi {event.nick}"},
|
|
28
|
+
"fires_event": {
|
|
29
|
+
"type": "custom.greeted",
|
|
30
|
+
"data": {"nick": "{{ event.nick }}"},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
cfg = load_bot_config(path)
|
|
36
|
+
assert cfg.fires_event is not None
|
|
37
|
+
assert cfg.fires_event.type == "custom.greeted"
|
|
38
|
+
assert cfg.fires_event.data == {"nick": "{{ event.nick }}"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_load_canonical_output_fires_event_still_works(tmp_path):
|
|
42
|
+
path = _write_yaml(
|
|
43
|
+
tmp_path,
|
|
44
|
+
{
|
|
45
|
+
"bot": {"name": "spark-greeter", "owner": "spark"},
|
|
46
|
+
"trigger": {"type": "event", "filter": "type == 'user.join'"},
|
|
47
|
+
"output": {
|
|
48
|
+
"channels": ["#general"],
|
|
49
|
+
"template": "hi {event.nick}",
|
|
50
|
+
"fires_event": {
|
|
51
|
+
"type": "custom.greeted",
|
|
52
|
+
"data": {"nick": "{{ event.nick }}"},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
cfg = load_bot_config(path)
|
|
59
|
+
assert cfg.fires_event is not None
|
|
60
|
+
assert cfg.fires_event.type == "custom.greeted"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_output_location_wins_over_top_level(tmp_path):
|
|
64
|
+
"""If both are set, `output.fires_event` is canonical and wins."""
|
|
65
|
+
path = _write_yaml(
|
|
66
|
+
tmp_path,
|
|
67
|
+
{
|
|
68
|
+
"bot": {"name": "spark-greeter", "owner": "spark"},
|
|
69
|
+
"trigger": {"type": "event", "filter": "type == 'user.join'"},
|
|
70
|
+
"output": {
|
|
71
|
+
"channels": ["#general"],
|
|
72
|
+
"fires_event": {"type": "custom.canonical", "data": {}},
|
|
73
|
+
},
|
|
74
|
+
"fires_event": {"type": "custom.toplevel", "data": {}},
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
cfg = load_bot_config(path)
|
|
79
|
+
assert cfg.fires_event is not None
|
|
80
|
+
assert cfg.fires_event.type == "custom.canonical"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_save_round_trip_writes_canonical_location(tmp_path):
|
|
84
|
+
"""save_bot_config always emits the canonical output.fires_event form."""
|
|
85
|
+
src = _write_yaml(
|
|
86
|
+
tmp_path,
|
|
87
|
+
{
|
|
88
|
+
"bot": {"name": "spark-greeter", "owner": "spark"},
|
|
89
|
+
"trigger": {"type": "event", "filter": "type == 'user.join'"},
|
|
90
|
+
"output": {"channels": ["#general"], "template": "hi"},
|
|
91
|
+
"fires_event": {"type": "custom.greeted", "data": {"n": "x"}},
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
cfg = load_bot_config(src)
|
|
96
|
+
out_path = tmp_path / "saved.yaml"
|
|
97
|
+
save_bot_config(out_path, cfg)
|
|
98
|
+
data = yaml.safe_load(out_path.read_text())
|
|
99
|
+
assert "fires_event" not in data, "top-level fires_event must not be re-emitted"
|
|
100
|
+
assert data["output"]["fires_event"]["type"] == "custom.greeted"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_missing_fires_event_still_loads(tmp_path):
|
|
104
|
+
path = _write_yaml(
|
|
105
|
+
tmp_path,
|
|
106
|
+
{
|
|
107
|
+
"bot": {"name": "spark-plain", "owner": "spark"},
|
|
108
|
+
"trigger": {"type": "webhook"},
|
|
109
|
+
"output": {"channels": ["#general"], "template": "hi"},
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
cfg = load_bot_config(path)
|
|
114
|
+
assert cfg.fires_event is None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Regression tests: subprocess timeout handling in persistence._run_cmd.
|
|
2
|
+
|
|
3
|
+
Covers the hang reported for ``culture mesh update``: a broken/hung systemd
|
|
4
|
+
unit caused ``systemctl --user restart`` to block indefinitely because the
|
|
5
|
+
underlying subprocess call had no timeout.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
13
|
+
from culture.persistence import (
|
|
14
|
+
DEFAULT_CMD_TIMEOUT,
|
|
15
|
+
_restart_linux_service,
|
|
16
|
+
_run_cmd,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_run_cmd_returns_true_on_success():
|
|
21
|
+
with patch("culture.persistence.subprocess.run") as mock_run:
|
|
22
|
+
mock_run.return_value = subprocess.CompletedProcess(args=["/bin/true"], returncode=0)
|
|
23
|
+
assert _run_cmd(["/bin/true"]) is True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_run_cmd_returns_false_on_timeout():
|
|
27
|
+
"""_run_cmd must not propagate TimeoutExpired — callers treat it as failure."""
|
|
28
|
+
with patch("culture.persistence.subprocess.run") as mock_run:
|
|
29
|
+
mock_run.side_effect = subprocess.TimeoutExpired(cmd=["systemctl"], timeout=30)
|
|
30
|
+
assert _run_cmd(["systemctl", "restart", "x"]) is False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_run_cmd_passes_timeout_to_subprocess():
|
|
34
|
+
with patch("culture.persistence.subprocess.run") as mock_run:
|
|
35
|
+
mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0)
|
|
36
|
+
_run_cmd(["echo", "hi"], timeout=5)
|
|
37
|
+
kwargs = mock_run.call_args.kwargs
|
|
38
|
+
assert kwargs["timeout"] == 5
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_run_cmd_default_timeout_bounds_wait():
|
|
42
|
+
"""A default timeout must be applied so hung services never hang the CLI."""
|
|
43
|
+
with patch("culture.persistence.subprocess.run") as mock_run:
|
|
44
|
+
mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0)
|
|
45
|
+
_run_cmd(["systemctl", "restart", "x"])
|
|
46
|
+
assert mock_run.call_args.kwargs["timeout"] == DEFAULT_CMD_TIMEOUT
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_restart_linux_service_returns_false_on_timeout(tmp_path, monkeypatch):
|
|
50
|
+
"""When the restart command times out, the restarter must report failure."""
|
|
51
|
+
svc_name = "culture-test-timeout"
|
|
52
|
+
unit_dir = tmp_path / "systemd-user"
|
|
53
|
+
unit_dir.mkdir()
|
|
54
|
+
(unit_dir / f"{svc_name}.service").write_text("[Service]\nExecStart=/bin/true\n")
|
|
55
|
+
|
|
56
|
+
monkeypatch.setattr("culture.persistence._systemd_user_dir", lambda: unit_dir)
|
|
57
|
+
with patch("culture.persistence.subprocess.run") as mock_run:
|
|
58
|
+
mock_run.side_effect = subprocess.TimeoutExpired(cmd=["systemctl"], timeout=30)
|
|
59
|
+
assert _restart_linux_service(svc_name) is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|