agentirc-cli 3.0.0__tar.gz → 3.0.2__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-3.0.0 → agentirc_cli-3.0.2}/CHANGELOG.md +15 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/CLAUDE.md +1 -1
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/PKG-INFO +1 -1
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/cli.py +91 -19
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/overview/collector.py +28 -21
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/ircd.py +20 -6
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/pyproject.toml +1 -1
- agentirc_cli-3.0.2/tests/test_overview_cli.py +133 -0
- agentirc_cli-3.0.2/tests/test_wait_for_port.py +89 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/uv.lock +1 -1
- agentirc_cli-3.0.0/tests/test_overview_cli.py +0 -42
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.claude/skills/run-tests/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.flake8 +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.github/workflows/security-checks.yml +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.gitignore +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.pr_agent.toml +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.pre-commit-config.yaml +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/.pylintrc +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/CNAME +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/Gemfile +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/Gemfile.lock +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/LICENSE +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/README.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/SECURITY.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/_config.yml +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/__main__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/bots/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/bots/bot.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/bots/bot_manager.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/bots/config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/bots/http_listener.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/bots/template_engine.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/bots/virtual_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/ipc.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/irc_transport.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/message_buffer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/skill/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/skill/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/skill/irc_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/socket_server.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/supervisor.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/acp/webhook.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/__main__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/ipc.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/socket_server.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/supervisor.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/claude/webhook.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/ipc.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/socket_server.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/supervisor.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/codex/webhook.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/ipc.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/irc_transport.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/clients/copilot/webhook.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/console/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/console/app.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/console/client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/console/commands.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/console/widgets/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/console/widgets/chat.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/console/widgets/info_panel.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/console/widgets/sidebar.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/credentials.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/learn_prompt.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/mesh_config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/observer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/overview/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/overview/model.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/overview/renderer_text.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/overview/renderer_web.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/overview/web/style.css +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/persistence.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/pidfile.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/commands.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/extensions/federation.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/extensions/history.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/extensions/icons.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/extensions/tags.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/extensions/threads.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/message.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/protocol-index.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/protocol/replies.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/__main__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/channel.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/remote_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/room_store.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/rooms_util.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/server_link.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/skill.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/skills/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/skills/history.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/skills/icon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/skills/rooms.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/skills/threads.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/server/thread_store.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/culture/skills/culture/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/agent-lifecycle.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/agentic-self-learn.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/agent-client.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/agent-harness-spec.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/harness-conformance.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/index.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/layer1-core-irc.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/layer2-attention.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/layer3-skills.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/layer4-federation.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/layer5-agent-harness.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/server-architecture.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/architecture/threads.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/acp/overview.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/codex/configuration.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/copilot/configuration.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/culture-cli.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/getting-started.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/index.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/SECURITY.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/bots.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/ci.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/cli.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/docs-site.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/index.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/ops-tooling.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/overview.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/operations/publishing.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/rooms.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases/10-agent-lifecycle.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/use-cases-index.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/docs/what-is-culture.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/plugins/claude-code/skills/culture/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/sonar-project.properties +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/__init__.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/conftest.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_bot.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_bot_config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_bot_manager.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_bots_integration.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_channel.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_connection.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_console_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_console_commands.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_console_connection.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_console_icons.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_console_integration.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_daemon.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_discovery.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_federation.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_history.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_http_listener.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_ipc.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_mentions.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_message.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_messaging.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_modes.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_overview_model.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_overview_web.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_persistence.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_pidfile.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_rooms.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_skill_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_skills.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_socket_server.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_supervisor.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_template_engine.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_thread_buffer.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_threads.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_virtual_client.py +0 -0
- {agentirc_cli-3.0.0 → agentirc_cli-3.0.2}/tests/test_webhook.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
|
+
## [3.0.2] - 2026-04-06
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Server startup readiness — culture server start now waits for port to accept connections before returning
|
|
13
|
+
- Added startup phase logging to server log for diagnosing slow starts
|
|
14
|
+
|
|
15
|
+
## [3.0.1] - 2026-04-06
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fix empty error message when running `culture overview` against a starting or unreachable server
|
|
21
|
+
|
|
7
22
|
## [3.0.0] - 2026-04-06
|
|
8
23
|
|
|
9
24
|
|
|
@@ -42,7 +42,7 @@ When implementing features, write a corresponding markdown doc in `docs/` descri
|
|
|
42
42
|
|
|
43
43
|
## Testing
|
|
44
44
|
|
|
45
|
-
- `pytest` + `pytest-asyncio`
|
|
45
|
+
- `pytest` + `pytest-asyncio`, always run with `-n auto` for parallel execution
|
|
46
46
|
- No mocks for the server — tests spin up real server instances on random ports with real TCP connections
|
|
47
47
|
- Validate each layer with real IRC clients (weechat/irssi)
|
|
48
48
|
|
|
@@ -29,6 +29,7 @@ import logging
|
|
|
29
29
|
import os
|
|
30
30
|
import shutil
|
|
31
31
|
import signal
|
|
32
|
+
import socket
|
|
32
33
|
import subprocess
|
|
33
34
|
import sys
|
|
34
35
|
import time
|
|
@@ -493,6 +494,39 @@ def _cmd_server(args: argparse.Namespace) -> None:
|
|
|
493
494
|
print(f"Default server set to '{args.name}'")
|
|
494
495
|
|
|
495
496
|
|
|
497
|
+
def _wait_for_port(
|
|
498
|
+
host: str,
|
|
499
|
+
port: int,
|
|
500
|
+
pid: int,
|
|
501
|
+
timeout: float = 30,
|
|
502
|
+
) -> tuple[bool, str]:
|
|
503
|
+
"""Poll *host*:*port* until a TCP connect succeeds or *timeout* expires.
|
|
504
|
+
|
|
505
|
+
Returns ``(True, "")`` on success, or ``(False, reason)`` on failure.
|
|
506
|
+
Checks that *pid* is still alive on every iteration so we fail fast if
|
|
507
|
+
the child crashes (e.g. because the port was already in use).
|
|
508
|
+
"""
|
|
509
|
+
check_host = "127.0.0.1" if host == "0.0.0.0" else host
|
|
510
|
+
deadline = time.monotonic() + timeout
|
|
511
|
+
while time.monotonic() < deadline:
|
|
512
|
+
if not is_process_alive(pid):
|
|
513
|
+
return False, "failed to start"
|
|
514
|
+
try:
|
|
515
|
+
s = socket.create_connection((check_host, port), timeout=0.5)
|
|
516
|
+
s.close()
|
|
517
|
+
except OSError:
|
|
518
|
+
time.sleep(0.2)
|
|
519
|
+
continue
|
|
520
|
+
# Port responded — give the child a moment, then confirm it's
|
|
521
|
+
# still alive (guards against connecting to a *stale* listener
|
|
522
|
+
# on the same port while our child crashes).
|
|
523
|
+
time.sleep(0.1)
|
|
524
|
+
if not is_process_alive(pid):
|
|
525
|
+
return False, "failed to start"
|
|
526
|
+
return True, ""
|
|
527
|
+
return False, "started but not yet accepting connections"
|
|
528
|
+
|
|
529
|
+
|
|
496
530
|
def _server_start(args: argparse.Namespace) -> None:
|
|
497
531
|
pid_name = f"server-{args.name}"
|
|
498
532
|
|
|
@@ -532,19 +566,33 @@ def _server_start(args: argparse.Namespace) -> None:
|
|
|
532
566
|
# Fork to daemonize
|
|
533
567
|
pid = os.fork()
|
|
534
568
|
if pid > 0:
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
write_default_server(args.name)
|
|
569
|
+
log_hint = f"{LOG_DIR}/server-{args.name}.log"
|
|
570
|
+
|
|
571
|
+
if args.port == 0:
|
|
572
|
+
# Ephemeral port — can't probe; fall back to process-alive check
|
|
573
|
+
time.sleep(0.5)
|
|
574
|
+
if not is_process_alive(pid):
|
|
575
|
+
print(f"Server '{args.name}' failed to start", file=sys.stderr)
|
|
576
|
+
print(f" Check logs: {log_hint}", file=sys.stderr)
|
|
577
|
+
sys.exit(1)
|
|
545
578
|
else:
|
|
546
|
-
|
|
547
|
-
|
|
579
|
+
ok, err = _wait_for_port(args.host, args.port, pid, timeout=30)
|
|
580
|
+
if not ok:
|
|
581
|
+
print(
|
|
582
|
+
f"Server '{args.name}' {err}",
|
|
583
|
+
file=sys.stderr,
|
|
584
|
+
)
|
|
585
|
+
print(f" Check logs: {log_hint}", file=sys.stderr)
|
|
586
|
+
sys.exit(1)
|
|
587
|
+
|
|
588
|
+
print(f"Server '{args.name}' started (PID {pid})")
|
|
589
|
+
print(f" Listening on {args.host}:{args.port}")
|
|
590
|
+
print(f" Logs: {log_hint}")
|
|
591
|
+
# Auto-set default server if none is set
|
|
592
|
+
from culture.pidfile import read_default_server, write_default_server
|
|
593
|
+
|
|
594
|
+
if read_default_server() is None:
|
|
595
|
+
write_default_server(args.name)
|
|
548
596
|
return
|
|
549
597
|
|
|
550
598
|
# Child: detach from parent session
|
|
@@ -562,6 +610,13 @@ def _server_start(args: argparse.Namespace) -> None:
|
|
|
562
610
|
os.dup2(devnull, 0)
|
|
563
611
|
os.close(devnull)
|
|
564
612
|
|
|
613
|
+
# Reconfigure logging so handlers write to the redirected stderr (log file)
|
|
614
|
+
logging.basicConfig(
|
|
615
|
+
level=logging.INFO,
|
|
616
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
617
|
+
force=True,
|
|
618
|
+
)
|
|
619
|
+
|
|
565
620
|
write_pid(pid_name, os.getpid())
|
|
566
621
|
|
|
567
622
|
try:
|
|
@@ -1593,14 +1648,31 @@ def _cmd_overview(args: argparse.Namespace) -> None:
|
|
|
1593
1648
|
)
|
|
1594
1649
|
return
|
|
1595
1650
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1651
|
+
host, port = config.server.host, config.server.port
|
|
1652
|
+
try:
|
|
1653
|
+
mesh = asyncio.run(
|
|
1654
|
+
collect_mesh_state(
|
|
1655
|
+
host=host,
|
|
1656
|
+
port=port,
|
|
1657
|
+
server_name=config.server.name,
|
|
1658
|
+
message_limit=message_limit,
|
|
1659
|
+
)
|
|
1602
1660
|
)
|
|
1603
|
-
|
|
1661
|
+
except ConnectionRefusedError:
|
|
1662
|
+
print(
|
|
1663
|
+
f"Error: could not connect to {host}:{port} — is the server running?",
|
|
1664
|
+
file=sys.stderr,
|
|
1665
|
+
)
|
|
1666
|
+
sys.exit(1)
|
|
1667
|
+
except TimeoutError:
|
|
1668
|
+
print(
|
|
1669
|
+
f"Error: server at {host}:{port} not responding" " — it may still be starting up",
|
|
1670
|
+
file=sys.stderr,
|
|
1671
|
+
)
|
|
1672
|
+
sys.exit(1)
|
|
1673
|
+
except OSError as exc:
|
|
1674
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
1675
|
+
sys.exit(1)
|
|
1604
1676
|
output = render_text(
|
|
1605
1677
|
mesh,
|
|
1606
1678
|
room_filter=args.room,
|
|
@@ -116,28 +116,35 @@ async def _connect(
|
|
|
116
116
|
timeout=REGISTER_TIMEOUT,
|
|
117
117
|
)
|
|
118
118
|
nick = _temp_nick(server_name)
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
try:
|
|
120
|
+
writer.write(f"NICK {nick}\r\nUSER overview 0 * :overview\r\n".encode())
|
|
121
|
+
await writer.drain()
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
123
|
+
deadline = asyncio.get_event_loop().time() + REGISTER_TIMEOUT
|
|
124
|
+
while True:
|
|
125
|
+
remaining = deadline - asyncio.get_event_loop().time()
|
|
126
|
+
if remaining <= 0:
|
|
127
|
+
raise TimeoutError("Registration timed out")
|
|
128
|
+
try:
|
|
129
|
+
data = await asyncio.wait_for(reader.readline(), timeout=remaining)
|
|
130
|
+
except asyncio.TimeoutError:
|
|
131
|
+
raise TimeoutError("Registration timed out") from None
|
|
132
|
+
line = data.decode().strip()
|
|
133
|
+
if not line:
|
|
134
|
+
continue
|
|
135
|
+
msg = IRCMessage.parse(line)
|
|
136
|
+
if msg.command == "PING":
|
|
137
|
+
writer.write(f"PONG :{msg.params[0]}\r\n".encode())
|
|
138
|
+
await writer.drain()
|
|
139
|
+
elif msg.command == "001":
|
|
140
|
+
return reader, writer, nick
|
|
141
|
+
elif msg.command == "433":
|
|
142
|
+
nick = _temp_nick(server_name)
|
|
143
|
+
writer.write(f"NICK {nick}\r\n".encode())
|
|
144
|
+
await writer.drain()
|
|
145
|
+
except BaseException:
|
|
146
|
+
await _disconnect(writer)
|
|
147
|
+
raise
|
|
141
148
|
|
|
142
149
|
|
|
143
150
|
async def _disconnect(writer: asyncio.StreamWriter) -> None:
|
|
@@ -10,6 +10,8 @@ from culture.server.channel import Channel
|
|
|
10
10
|
from culture.server.config import ServerConfig
|
|
11
11
|
from culture.server.skill import Event, Skill
|
|
12
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
13
15
|
if TYPE_CHECKING:
|
|
14
16
|
from culture.bots.virtual_client import VirtualClient
|
|
15
17
|
from culture.server.client import Client
|
|
@@ -41,16 +43,25 @@ class IRCd:
|
|
|
41
43
|
self.bot_manager = None # set in start() if webhook_port configured
|
|
42
44
|
|
|
43
45
|
async def start(self) -> None:
|
|
46
|
+
logger.info("Registering default skills...")
|
|
44
47
|
await self._register_default_skills()
|
|
48
|
+
|
|
49
|
+
logger.info("Restoring persistent rooms...")
|
|
45
50
|
self._restore_persistent_rooms()
|
|
46
51
|
|
|
47
52
|
# Initialize bot manager and webhook HTTP listener
|
|
48
53
|
from culture.bots.bot_manager import BotManager
|
|
49
54
|
from culture.bots.http_listener import HttpListener
|
|
50
55
|
|
|
56
|
+
logger.info("Loading bots...")
|
|
51
57
|
self.bot_manager = BotManager(self)
|
|
52
58
|
await self.bot_manager.load_bots()
|
|
53
59
|
|
|
60
|
+
logger.info(
|
|
61
|
+
"Binding IRC socket on %s:%d...",
|
|
62
|
+
self.config.host,
|
|
63
|
+
self.config.port,
|
|
64
|
+
)
|
|
54
65
|
self._server = await asyncio.start_server(
|
|
55
66
|
self._handle_connection,
|
|
56
67
|
self.config.host,
|
|
@@ -63,16 +74,22 @@ class IRCd:
|
|
|
63
74
|
self.config.webhook_port,
|
|
64
75
|
)
|
|
65
76
|
try:
|
|
77
|
+
logger.info(
|
|
78
|
+
"Starting webhook listener on port %d...",
|
|
79
|
+
self.config.webhook_port,
|
|
80
|
+
)
|
|
66
81
|
await self._http_listener.start()
|
|
67
82
|
except OSError:
|
|
68
83
|
# Port unavailable (e.g. in tests using port 0 that got
|
|
69
84
|
# assigned an in-use ephemeral port). Non-fatal — bots
|
|
70
85
|
# still work, just without the HTTP endpoint.
|
|
71
|
-
|
|
86
|
+
logger.warning(
|
|
72
87
|
"Could not start webhook listener on port %d",
|
|
73
88
|
self.config.webhook_port,
|
|
74
89
|
)
|
|
75
90
|
|
|
91
|
+
logger.info("Server ready")
|
|
92
|
+
|
|
76
93
|
async def _register_default_skills(self) -> None:
|
|
77
94
|
from culture.server.skills.history import HistorySkill
|
|
78
95
|
from culture.server.skills.icon import IconSkill
|
|
@@ -101,9 +118,7 @@ class IRCd:
|
|
|
101
118
|
try:
|
|
102
119
|
await skill.on_event(event)
|
|
103
120
|
except Exception:
|
|
104
|
-
|
|
105
|
-
"Skill %s failed on event %s", skill.name, event.type
|
|
106
|
-
)
|
|
121
|
+
logger.exception("Skill %s failed on event %s", skill.name, event.type)
|
|
107
122
|
|
|
108
123
|
# Relay to linked peers — only relay locally-originated events
|
|
109
124
|
# (no mesh routing; scope is direct peers only)
|
|
@@ -112,7 +127,7 @@ class IRCd:
|
|
|
112
127
|
try:
|
|
113
128
|
await link.relay_event(event)
|
|
114
129
|
except Exception:
|
|
115
|
-
|
|
130
|
+
logger.exception("Failed to relay event to %s", peer_name)
|
|
116
131
|
|
|
117
132
|
def get_skill_for_command(self, command: str) -> Skill | None:
|
|
118
133
|
for skill in self.skills:
|
|
@@ -190,7 +205,6 @@ class IRCd:
|
|
|
190
205
|
|
|
191
206
|
async def _retry_link_loop(self, peer_name: str, link_config, state: dict) -> None:
|
|
192
207
|
"""Retry connecting to a peer with exponential backoff."""
|
|
193
|
-
logger = logging.getLogger(__name__)
|
|
194
208
|
try:
|
|
195
209
|
while True:
|
|
196
210
|
await asyncio.sleep(state["delay"])
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Tests for overview CLI subcommand argument parsing and error handling."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_overview_help():
|
|
11
|
+
"""The overview subcommand is registered and has help."""
|
|
12
|
+
result = subprocess.run(
|
|
13
|
+
[sys.executable, "-m", "culture", "overview", "--help"],
|
|
14
|
+
capture_output=True,
|
|
15
|
+
text=True,
|
|
16
|
+
)
|
|
17
|
+
assert result.returncode == 0
|
|
18
|
+
assert "--room" in result.stdout
|
|
19
|
+
assert "--agent" in result.stdout
|
|
20
|
+
assert "--messages" in result.stdout
|
|
21
|
+
assert "--serve" in result.stdout
|
|
22
|
+
assert "--refresh" in result.stdout
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_overview_default_args():
|
|
26
|
+
"""Default args parse correctly."""
|
|
27
|
+
from culture.cli import _build_parser
|
|
28
|
+
|
|
29
|
+
parser = _build_parser()
|
|
30
|
+
args = parser.parse_args(["overview"])
|
|
31
|
+
assert args.command == "overview"
|
|
32
|
+
assert args.room is None
|
|
33
|
+
assert args.agent is None
|
|
34
|
+
assert args.messages == 4
|
|
35
|
+
assert args.serve is False
|
|
36
|
+
assert args.refresh == 5
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_overview_with_flags():
|
|
40
|
+
from culture.cli import _build_parser
|
|
41
|
+
|
|
42
|
+
parser = _build_parser()
|
|
43
|
+
args = parser.parse_args(["overview", "--room", "#general", "--messages", "10"])
|
|
44
|
+
assert args.room == "#general"
|
|
45
|
+
assert args.messages == 10
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_overview_connection_refused(capsys):
|
|
49
|
+
"""ConnectionRefusedError produces a helpful message."""
|
|
50
|
+
from culture.cli import _build_parser, _cmd_overview
|
|
51
|
+
|
|
52
|
+
parser = _build_parser()
|
|
53
|
+
args = parser.parse_args(["overview"])
|
|
54
|
+
|
|
55
|
+
with patch(
|
|
56
|
+
"culture.overview.collector.collect_mesh_state",
|
|
57
|
+
side_effect=ConnectionRefusedError(111, "Connection refused"),
|
|
58
|
+
):
|
|
59
|
+
with pytest.raises(SystemExit, match="1"):
|
|
60
|
+
_cmd_overview(args)
|
|
61
|
+
|
|
62
|
+
captured = capsys.readouterr()
|
|
63
|
+
assert "is the server running?" in captured.err
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_overview_timeout(capsys):
|
|
67
|
+
"""TimeoutError produces a helpful message."""
|
|
68
|
+
from culture.cli import _build_parser, _cmd_overview
|
|
69
|
+
|
|
70
|
+
parser = _build_parser()
|
|
71
|
+
args = parser.parse_args(["overview"])
|
|
72
|
+
|
|
73
|
+
with patch(
|
|
74
|
+
"culture.overview.collector.collect_mesh_state",
|
|
75
|
+
side_effect=TimeoutError("Registration timed out"),
|
|
76
|
+
):
|
|
77
|
+
with pytest.raises(SystemExit, match="1"):
|
|
78
|
+
_cmd_overview(args)
|
|
79
|
+
|
|
80
|
+
captured = capsys.readouterr()
|
|
81
|
+
assert "not responding" in captured.err
|
|
82
|
+
assert "still be starting up" in captured.err
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_overview_os_error(capsys):
|
|
86
|
+
"""OSError shows the original error details."""
|
|
87
|
+
from culture.cli import _build_parser, _cmd_overview
|
|
88
|
+
|
|
89
|
+
parser = _build_parser()
|
|
90
|
+
args = parser.parse_args(["overview"])
|
|
91
|
+
|
|
92
|
+
with patch(
|
|
93
|
+
"culture.overview.collector.collect_mesh_state",
|
|
94
|
+
side_effect=OSError("Name or service not known"),
|
|
95
|
+
):
|
|
96
|
+
with pytest.raises(SystemExit, match="1"):
|
|
97
|
+
_cmd_overview(args)
|
|
98
|
+
|
|
99
|
+
captured = capsys.readouterr()
|
|
100
|
+
assert "Name or service not known" in captured.err
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.asyncio
|
|
104
|
+
async def test_connect_timeout_has_message():
|
|
105
|
+
"""_connect raises TimeoutError with a non-empty message on registration timeout."""
|
|
106
|
+
import asyncio
|
|
107
|
+
|
|
108
|
+
from culture.overview.collector import _connect
|
|
109
|
+
|
|
110
|
+
# TCP server that accepts but never sends IRC 001 (silent handshake)
|
|
111
|
+
stop = asyncio.Event()
|
|
112
|
+
|
|
113
|
+
async def hold_open(reader, writer):
|
|
114
|
+
await stop.wait()
|
|
115
|
+
writer.close()
|
|
116
|
+
|
|
117
|
+
server = await asyncio.start_server(hold_open, "127.0.0.1", 0)
|
|
118
|
+
port = server.sockets[0].getsockname()[1]
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
with pytest.raises(TimeoutError, match="Registration timed out"):
|
|
122
|
+
import culture.overview.collector as col
|
|
123
|
+
|
|
124
|
+
original = col.REGISTER_TIMEOUT
|
|
125
|
+
col.REGISTER_TIMEOUT = 0.5
|
|
126
|
+
try:
|
|
127
|
+
await _connect("127.0.0.1", port, "test")
|
|
128
|
+
finally:
|
|
129
|
+
col.REGISTER_TIMEOUT = original
|
|
130
|
+
finally:
|
|
131
|
+
stop.set()
|
|
132
|
+
server.close()
|
|
133
|
+
await server.wait_closed()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Tests for the _wait_for_port readiness helper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from culture.cli import _wait_for_port
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _start_listener(port_holder: list, delay: float = 0) -> socket.socket:
|
|
13
|
+
"""Start a TCP listener, optionally after *delay* seconds.
|
|
14
|
+
|
|
15
|
+
Stores the assigned port in *port_holder[0]*.
|
|
16
|
+
"""
|
|
17
|
+
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
18
|
+
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
19
|
+
|
|
20
|
+
def _bind():
|
|
21
|
+
if delay:
|
|
22
|
+
time.sleep(delay)
|
|
23
|
+
srv.bind(("127.0.0.1", 0))
|
|
24
|
+
srv.listen(1)
|
|
25
|
+
port_holder.append(srv.getsockname()[1])
|
|
26
|
+
|
|
27
|
+
t = threading.Thread(target=_bind, daemon=True)
|
|
28
|
+
t.start()
|
|
29
|
+
t.join(timeout=delay + 2)
|
|
30
|
+
return srv
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_success_immediate():
|
|
34
|
+
"""Port already open — should return immediately."""
|
|
35
|
+
port_holder: list[int] = []
|
|
36
|
+
srv = _start_listener(port_holder)
|
|
37
|
+
try:
|
|
38
|
+
ok, err = _wait_for_port(
|
|
39
|
+
"127.0.0.1",
|
|
40
|
+
port_holder[0],
|
|
41
|
+
# Use our own PID (always alive)
|
|
42
|
+
pid=threading.current_thread().native_id or 1,
|
|
43
|
+
timeout=5,
|
|
44
|
+
)
|
|
45
|
+
assert ok
|
|
46
|
+
assert err == ""
|
|
47
|
+
finally:
|
|
48
|
+
srv.close()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_timeout_no_listener():
|
|
52
|
+
"""No listener — should time out."""
|
|
53
|
+
# Bind then close to get a free port that nothing listens on
|
|
54
|
+
tmp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
55
|
+
tmp.bind(("127.0.0.1", 0))
|
|
56
|
+
port = tmp.getsockname()[1]
|
|
57
|
+
tmp.close()
|
|
58
|
+
|
|
59
|
+
import os
|
|
60
|
+
|
|
61
|
+
ok, err = _wait_for_port("127.0.0.1", port, pid=os.getpid(), timeout=1)
|
|
62
|
+
assert not ok
|
|
63
|
+
assert "not yet accepting connections" in err
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_process_dies():
|
|
67
|
+
"""Dead PID — should fail fast."""
|
|
68
|
+
import subprocess
|
|
69
|
+
|
|
70
|
+
# Spawn a process that exits immediately to get a dead PID
|
|
71
|
+
p = subprocess.Popen(["true"])
|
|
72
|
+
p.wait()
|
|
73
|
+
|
|
74
|
+
ok, err = _wait_for_port("127.0.0.1", 1, pid=p.pid, timeout=5)
|
|
75
|
+
assert not ok
|
|
76
|
+
assert "failed to start" in err
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_host_0000_uses_localhost():
|
|
80
|
+
"""0.0.0.0 host should probe 127.0.0.1."""
|
|
81
|
+
port_holder: list[int] = []
|
|
82
|
+
srv = _start_listener(port_holder)
|
|
83
|
+
try:
|
|
84
|
+
import os
|
|
85
|
+
|
|
86
|
+
ok, _ = _wait_for_port("0.0.0.0", port_holder[0], pid=os.getpid(), timeout=5)
|
|
87
|
+
assert ok
|
|
88
|
+
finally:
|
|
89
|
+
srv.close()
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Tests for overview CLI subcommand argument parsing."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_overview_help():
|
|
8
|
-
"""The overview subcommand is registered and has help."""
|
|
9
|
-
result = subprocess.run(
|
|
10
|
-
[sys.executable, "-m", "culture", "overview", "--help"],
|
|
11
|
-
capture_output=True,
|
|
12
|
-
text=True,
|
|
13
|
-
)
|
|
14
|
-
assert result.returncode == 0
|
|
15
|
-
assert "--room" in result.stdout
|
|
16
|
-
assert "--agent" in result.stdout
|
|
17
|
-
assert "--messages" in result.stdout
|
|
18
|
-
assert "--serve" in result.stdout
|
|
19
|
-
assert "--refresh" in result.stdout
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_overview_default_args():
|
|
23
|
-
"""Default args parse correctly."""
|
|
24
|
-
from culture.cli import _build_parser
|
|
25
|
-
|
|
26
|
-
parser = _build_parser()
|
|
27
|
-
args = parser.parse_args(["overview"])
|
|
28
|
-
assert args.command == "overview"
|
|
29
|
-
assert args.room is None
|
|
30
|
-
assert args.agent is None
|
|
31
|
-
assert args.messages == 4
|
|
32
|
-
assert args.serve is False
|
|
33
|
-
assert args.refresh == 5
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_overview_with_flags():
|
|
37
|
-
from culture.cli import _build_parser
|
|
38
|
-
|
|
39
|
-
parser = _build_parser()
|
|
40
|
-
args = parser.parse_args(["overview", "--room", "#general", "--messages", "10"])
|
|
41
|
-
assert args.room == "#general"
|
|
42
|
-
assert args.messages == 10
|
|
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
|