agentirc-cli 0.14.0__tar.gz → 0.15.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-0.14.0 → agentirc_cli-0.15.0}/.gitignore +4 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/CHANGELOG.md +30 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/CLAUDE.md +2 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/PKG-INFO +1 -1
- agentirc_cli-0.15.0/agentirc/__init__.py +1 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/config.py +1 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/daemon.py +47 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/irc_transport.py +14 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/config.py +1 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/daemon.py +46 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/irc_transport.py +14 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/config.py +1 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/daemon.py +46 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/irc_transport.py +14 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/config.py +1 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/daemon.py +46 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/irc_transport.py +14 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/overview/collector.py +58 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/overview/model.py +6 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/overview/renderer_text.py +13 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/overview/renderer_web.py +6 -5
- agentirc_cli-0.15.0/agentirc/protocol/extensions/rooms.md +66 -0
- agentirc_cli-0.15.0/agentirc/protocol/extensions/tags.md +32 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/channel.py +18 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/client.py +14 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/config.py +1 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/ircd.py +27 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/remote_client.py +1 -0
- agentirc_cli-0.15.0/agentirc/server/room_store.py +63 -0
- agentirc_cli-0.15.0/agentirc/server/rooms_util.py +55 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/server_link.py +126 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/skill.py +3 -0
- agentirc_cli-0.15.0/agentirc/server/skills/rooms.py +669 -0
- agentirc_cli-0.15.0/docs/rooms.md +80 -0
- agentirc_cli-0.15.0/docs/superpowers/plans/2026-03-30-rooms-management.md +3173 -0
- agentirc_cli-0.15.0/docs/superpowers/specs/2026-03-30-rooms-management-design.md +488 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/pyproject.toml +1 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_overview_model.py +54 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_overview_web.py +8 -0
- agentirc_cli-0.15.0/tests/test_room_persistence.py +75 -0
- agentirc_cli-0.15.0/tests/test_rooms.py +732 -0
- agentirc_cli-0.15.0/tests/test_rooms_federation.py +63 -0
- agentirc_cli-0.15.0/tests/test_rooms_integration.py +110 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/uv.lock +1 -1
- agentirc_cli-0.14.0/agentirc/__init__.py +0 -1
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/.pr_agent.toml +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/CNAME +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/Gemfile +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/Gemfile.lock +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/LICENSE +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/README.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/_config.yml +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/__main__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/cli.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/__main__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/ipc.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/socket_server.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/supervisor.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/claude/webhook.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/ipc.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/socket_server.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/supervisor.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/codex/webhook.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/ipc.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/copilot/webhook.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/agent_runner.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/ipc.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/message_buffer.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/skill/SKILL.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/skill/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/skill/irc_client.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/socket_server.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/supervisor.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/clients/opencode/webhook.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/learn_prompt.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/observer.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/overview/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/overview/web/style.css +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/pidfile.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/protocol/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/protocol/commands.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/protocol/extensions/federation.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/protocol/extensions/history.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/protocol/message.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/protocol/protocol-index.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/protocol/replies.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/__main__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/skills/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/agentirc/server/skills/history.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/agent-client.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/agent-harness-spec.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/ci.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/cli.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/codex/configuration.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/copilot/configuration.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/opencode/configuration.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/opencode/context-management.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/opencode/irc-tools.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/opencode/overview.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/opencode/setup.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/opencode/supervisor.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/clients/opencode/webhooks.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/codex-backend.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/copilot-backend.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/design.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/docs-site.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/getting-started.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/grow-your-agent.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/layer1-core-irc.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/layer2-attention.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/layer3-skills.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/layer4-federation.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/layer5-agent-harness.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/opencode-backend.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/overview.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/publishing.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/server-architecture.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases/10-grow-your-agent.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/index.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/__init__.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/conftest.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_channel.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_connection.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_federation.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_history.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_message.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_modes.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_opencode_daemon.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_skills.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-0.14.0 → agentirc_cli-0.15.0}/tests/test_webhook.py +0 -0
|
@@ -4,6 +4,36 @@ 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
|
+
## [0.15.0] - 2026-03-30
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Managed rooms with rich metadata (ROOMCREATE, ROOMMETA, ROOMARCHIVE, ROOMKICK, ROOMINVITE)
|
|
13
|
+
- Tag-based self-organizing room membership for agents and rooms
|
|
14
|
+
- Room persistence to disk for managed rooms
|
|
15
|
+
- S2S federation for room metadata, agent tags, and archives (SROOMMETA, STAGS, SROOMARCHIVE)
|
|
16
|
+
- Agent tags in config and at runtime (TAGS command)
|
|
17
|
+
- Overview integration showing room/agent tags and metadata
|
|
18
|
+
- Protocol extensions: rooms.md, tags.md
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Persistent channels survive when empty (no auto-cleanup)
|
|
24
|
+
- Archived channels block new JOINs
|
|
25
|
+
- All agent backends (claude, codex, copilot, opencode) support tags and ROOMINVITE
|
|
26
|
+
- CLAUDE.md: added all-backends rule for harness changes
|
|
27
|
+
|
|
28
|
+
## [0.14.1] - 2026-03-30
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Web dashboard table rendering (enable mistune table plugin)
|
|
34
|
+
- Status badge injection for indented td tags
|
|
35
|
+
- Metadata table cell escaping in agent detail view
|
|
36
|
+
|
|
7
37
|
## [0.14.0] - 2026-03-30
|
|
8
38
|
|
|
9
39
|
|
|
@@ -25,6 +25,8 @@ For agent backends (`clients/claude/`, `clients/codex/`, etc.):
|
|
|
25
25
|
|
|
26
26
|
If you improve a generic component (e.g., `irc_transport.py`), update the reference in `packages/` too so the next backend starts from the latest version.
|
|
27
27
|
|
|
28
|
+
**All-backends rule:** When adding or changing a feature in any agent harness (config fields, transport capabilities, daemon handlers), propagate the change to **all** backends (`claude`, `codex`, `copilot`, `opencode`) and update `docs/` accordingly. A feature that only exists in one backend is a bug.
|
|
29
|
+
|
|
28
30
|
## Documentation
|
|
29
31
|
|
|
30
32
|
When implementing features, write a corresponding markdown doc in `docs/` describing the feature — its purpose, usage, and any protocol details. Keep `docs/` as the living reference for the project.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.14.1"
|
|
@@ -89,6 +89,8 @@ class AgentDaemon:
|
|
|
89
89
|
channels=list(self.agent.channels),
|
|
90
90
|
buffer=self._buffer,
|
|
91
91
|
on_mention=self._on_mention,
|
|
92
|
+
tags=list(self.agent.tags),
|
|
93
|
+
on_roominvite=self._on_roominvite,
|
|
92
94
|
)
|
|
93
95
|
await self._transport.connect()
|
|
94
96
|
|
|
@@ -249,6 +251,51 @@ class AgentDaemon:
|
|
|
249
251
|
prompt = f"[IRC DM] <{sender}> {text}"
|
|
250
252
|
asyncio.create_task(self._agent_runner.send_prompt(prompt))
|
|
251
253
|
|
|
254
|
+
def _on_roominvite(self, channel: str, meta_text: str) -> None:
|
|
255
|
+
"""Called by IRCTransport when a ROOMINVITE is received."""
|
|
256
|
+
asyncio.create_task(self._handle_roominvite(channel, meta_text))
|
|
257
|
+
|
|
258
|
+
async def _handle_roominvite(self, channel: str, meta_text: str) -> None:
|
|
259
|
+
"""Evaluate a room invitation using the agent's LLM."""
|
|
260
|
+
from agentirc.server.rooms_util import parse_room_meta
|
|
261
|
+
|
|
262
|
+
meta = parse_room_meta(meta_text)
|
|
263
|
+
purpose = meta.get("purpose", "")
|
|
264
|
+
instructions = meta.get("instructions", "")
|
|
265
|
+
tags = meta.get("tags", "")
|
|
266
|
+
requestor = meta.get("requestor")
|
|
267
|
+
|
|
268
|
+
prompt = (
|
|
269
|
+
f"You've been invited to join IRC room {channel}.\n"
|
|
270
|
+
f"Purpose: {purpose}\n"
|
|
271
|
+
f"Instructions: {instructions}\n"
|
|
272
|
+
f"Room tags: {tags}\n"
|
|
273
|
+
f"Your tags: {','.join(self.agent.tags)}\n\n"
|
|
274
|
+
"Think step-by-step about whether this room fits your current work "
|
|
275
|
+
"and capabilities. Then decide: should you join? Answer YES or NO."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if self._agent_runner is None or not self._agent_runner.is_running():
|
|
279
|
+
# No live agent — auto-join without evaluation
|
|
280
|
+
logger.info(
|
|
281
|
+
"ROOMINVITE for %s: no agent runner active, auto-joining %s",
|
|
282
|
+
self.agent.nick, channel,
|
|
283
|
+
)
|
|
284
|
+
assert self._transport is not None
|
|
285
|
+
await self._transport.send_raw(f"JOIN {channel}")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Use the agent runner to evaluate
|
|
289
|
+
await self._agent_runner.send_prompt(prompt)
|
|
290
|
+
# Note: the agent runner processes the prompt asynchronously via the
|
|
291
|
+
# SDK session loop. The agent is expected to use irc_join() / irc tools
|
|
292
|
+
# to act on the decision within its normal turn. We log the invite so
|
|
293
|
+
# the agent has the context to decide.
|
|
294
|
+
logger.info(
|
|
295
|
+
"ROOMINVITE for %s on %s — evaluation prompt sent to agent",
|
|
296
|
+
self.agent.nick, channel,
|
|
297
|
+
)
|
|
298
|
+
|
|
252
299
|
async def _on_agent_message(self, msg: dict) -> None:
|
|
253
300
|
"""Feed agent activity to the supervisor for observation."""
|
|
254
301
|
if self._supervisor:
|
|
@@ -15,7 +15,9 @@ class IRCTransport:
|
|
|
15
15
|
|
|
16
16
|
def __init__(self, host: str, port: int, nick: str, user: str,
|
|
17
17
|
channels: list[str], buffer: MessageBuffer,
|
|
18
|
-
on_mention: Callable[[str, str, str], None] | None = None
|
|
18
|
+
on_mention: Callable[[str, str, str], None] | None = None,
|
|
19
|
+
tags: list[str] | None = None,
|
|
20
|
+
on_roominvite: Callable[[str, str], None] | None = None):
|
|
19
21
|
self.host = host
|
|
20
22
|
self.port = port
|
|
21
23
|
self.nick = nick
|
|
@@ -23,6 +25,8 @@ class IRCTransport:
|
|
|
23
25
|
self.channels = list(channels)
|
|
24
26
|
self.buffer = buffer
|
|
25
27
|
self.on_mention = on_mention
|
|
28
|
+
self.tags = tags or []
|
|
29
|
+
self.on_roominvite = on_roominvite
|
|
26
30
|
self.connected = False
|
|
27
31
|
self._reader: asyncio.StreamReader | None = None
|
|
28
32
|
self._writer: asyncio.StreamWriter | None = None
|
|
@@ -136,6 +140,10 @@ class IRCTransport:
|
|
|
136
140
|
self.connected = True
|
|
137
141
|
for channel in self.channels:
|
|
138
142
|
await self._send_raw(f"JOIN {channel}")
|
|
143
|
+
# Announce agent tags on connect
|
|
144
|
+
if self.tags:
|
|
145
|
+
tags_str = ",".join(self.tags)
|
|
146
|
+
await self._send_raw(f"TAGS {self.nick} {tags_str}")
|
|
139
147
|
elif msg.command == "PRIVMSG" and len(msg.params) >= 2:
|
|
140
148
|
target = msg.params[0]
|
|
141
149
|
text = msg.params[1]
|
|
@@ -154,3 +162,8 @@ class IRCTransport:
|
|
|
154
162
|
sender = msg.prefix.split("!")[0] if msg.prefix else "server"
|
|
155
163
|
if target.startswith("#"):
|
|
156
164
|
self.buffer.add(target, sender, text)
|
|
165
|
+
elif msg.command == "ROOMINVITE" and len(msg.params) >= 3:
|
|
166
|
+
channel = msg.params[0]
|
|
167
|
+
meta_text = msg.params[2]
|
|
168
|
+
if self.on_roominvite:
|
|
169
|
+
self.on_roominvite(channel, meta_text)
|
|
@@ -102,6 +102,8 @@ class CodexDaemon:
|
|
|
102
102
|
channels=list(self.agent.channels),
|
|
103
103
|
buffer=self._buffer,
|
|
104
104
|
on_mention=self._on_mention,
|
|
105
|
+
tags=list(self.agent.tags),
|
|
106
|
+
on_roominvite=self._on_roominvite,
|
|
105
107
|
)
|
|
106
108
|
await self._transport.connect()
|
|
107
109
|
|
|
@@ -261,6 +263,50 @@ class CodexDaemon:
|
|
|
261
263
|
prompt = f"[IRC DM] <{sender}> {text}"
|
|
262
264
|
asyncio.create_task(self._agent_runner.send_prompt(prompt))
|
|
263
265
|
|
|
266
|
+
def _on_roominvite(self, channel: str, meta_text: str) -> None:
|
|
267
|
+
"""Called by IRCTransport when a ROOMINVITE is received."""
|
|
268
|
+
asyncio.create_task(self._handle_roominvite(channel, meta_text))
|
|
269
|
+
|
|
270
|
+
async def _handle_roominvite(self, channel: str, meta_text: str) -> None:
|
|
271
|
+
"""Evaluate a room invitation using the agent's LLM."""
|
|
272
|
+
from agentirc.server.rooms_util import parse_room_meta
|
|
273
|
+
|
|
274
|
+
meta = parse_room_meta(meta_text)
|
|
275
|
+
purpose = meta.get("purpose", "")
|
|
276
|
+
instructions = meta.get("instructions", "")
|
|
277
|
+
tags = meta.get("tags", "")
|
|
278
|
+
requestor = meta.get("requestor")
|
|
279
|
+
|
|
280
|
+
prompt = (
|
|
281
|
+
f"You've been invited to join IRC room {channel}.\n"
|
|
282
|
+
f"Purpose: {purpose}\n"
|
|
283
|
+
f"Instructions: {instructions}\n"
|
|
284
|
+
f"Room tags: {tags}\n"
|
|
285
|
+
f"Your tags: {','.join(self.agent.tags)}\n\n"
|
|
286
|
+
"Think step-by-step about whether this room fits your current work "
|
|
287
|
+
"and capabilities. Then decide: should you join? Answer YES or NO."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if self._agent_runner is None or not self._agent_runner.is_running():
|
|
291
|
+
# No live agent — auto-join without evaluation
|
|
292
|
+
logger.info(
|
|
293
|
+
"ROOMINVITE for %s: no agent runner active, auto-joining %s",
|
|
294
|
+
self.agent.nick, channel,
|
|
295
|
+
)
|
|
296
|
+
assert self._transport is not None
|
|
297
|
+
await self._transport.send_raw(f"JOIN {channel}")
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
# Use the agent runner to evaluate
|
|
301
|
+
# Enqueue a None relay target so the evaluation response doesn't
|
|
302
|
+
# steal a real mention's relay target from the deque.
|
|
303
|
+
self._mention_targets.append(None)
|
|
304
|
+
await self._agent_runner.send_prompt(prompt)
|
|
305
|
+
logger.info(
|
|
306
|
+
"ROOMINVITE for %s on %s — evaluation prompt sent to agent",
|
|
307
|
+
self.agent.nick, channel,
|
|
308
|
+
)
|
|
309
|
+
|
|
264
310
|
async def _on_agent_message(self, msg: dict) -> None:
|
|
265
311
|
"""Relay agent text to IRC and feed to supervisor."""
|
|
266
312
|
# Dequeue the relay target that corresponds to this turn
|
|
@@ -15,7 +15,9 @@ class IRCTransport:
|
|
|
15
15
|
|
|
16
16
|
def __init__(self, host: str, port: int, nick: str, user: str,
|
|
17
17
|
channels: list[str], buffer: MessageBuffer,
|
|
18
|
-
on_mention: Callable[[str, str, str], None] | None = None
|
|
18
|
+
on_mention: Callable[[str, str, str], None] | None = None,
|
|
19
|
+
tags: list[str] | None = None,
|
|
20
|
+
on_roominvite: Callable[[str, str], None] | None = None):
|
|
19
21
|
self.host = host
|
|
20
22
|
self.port = port
|
|
21
23
|
self.nick = nick
|
|
@@ -23,6 +25,8 @@ class IRCTransport:
|
|
|
23
25
|
self.channels = list(channels)
|
|
24
26
|
self.buffer = buffer
|
|
25
27
|
self.on_mention = on_mention
|
|
28
|
+
self.tags = tags or []
|
|
29
|
+
self.on_roominvite = on_roominvite
|
|
26
30
|
self.connected = False
|
|
27
31
|
self._reader: asyncio.StreamReader | None = None
|
|
28
32
|
self._writer: asyncio.StreamWriter | None = None
|
|
@@ -136,6 +140,10 @@ class IRCTransport:
|
|
|
136
140
|
self.connected = True
|
|
137
141
|
for channel in self.channels:
|
|
138
142
|
await self._send_raw(f"JOIN {channel}")
|
|
143
|
+
# Announce agent tags on connect
|
|
144
|
+
if self.tags:
|
|
145
|
+
tags_str = ",".join(self.tags)
|
|
146
|
+
await self._send_raw(f"TAGS {self.nick} {tags_str}")
|
|
139
147
|
elif msg.command == "PRIVMSG" and len(msg.params) >= 2:
|
|
140
148
|
target = msg.params[0]
|
|
141
149
|
text = msg.params[1]
|
|
@@ -154,3 +162,8 @@ class IRCTransport:
|
|
|
154
162
|
sender = msg.prefix.split("!")[0] if msg.prefix else "server"
|
|
155
163
|
if target.startswith("#"):
|
|
156
164
|
self.buffer.add(target, sender, text)
|
|
165
|
+
elif msg.command == "ROOMINVITE" and len(msg.params) >= 3:
|
|
166
|
+
channel = msg.params[0]
|
|
167
|
+
meta_text = msg.params[2]
|
|
168
|
+
if self.on_roominvite:
|
|
169
|
+
self.on_roominvite(channel, meta_text)
|
|
@@ -102,6 +102,8 @@ class CopilotDaemon:
|
|
|
102
102
|
channels=list(self.agent.channels),
|
|
103
103
|
buffer=self._buffer,
|
|
104
104
|
on_mention=self._on_mention,
|
|
105
|
+
tags=list(self.agent.tags),
|
|
106
|
+
on_roominvite=self._on_roominvite,
|
|
105
107
|
)
|
|
106
108
|
await self._transport.connect()
|
|
107
109
|
|
|
@@ -268,6 +270,50 @@ class CopilotDaemon:
|
|
|
268
270
|
prompt = f"[IRC DM] <{sender}> {text}"
|
|
269
271
|
asyncio.create_task(self._agent_runner.send_prompt(prompt))
|
|
270
272
|
|
|
273
|
+
def _on_roominvite(self, channel: str, meta_text: str) -> None:
|
|
274
|
+
"""Called by IRCTransport when a ROOMINVITE is received."""
|
|
275
|
+
asyncio.create_task(self._handle_roominvite(channel, meta_text))
|
|
276
|
+
|
|
277
|
+
async def _handle_roominvite(self, channel: str, meta_text: str) -> None:
|
|
278
|
+
"""Evaluate a room invitation using the agent's LLM."""
|
|
279
|
+
from agentirc.server.rooms_util import parse_room_meta
|
|
280
|
+
|
|
281
|
+
meta = parse_room_meta(meta_text)
|
|
282
|
+
purpose = meta.get("purpose", "")
|
|
283
|
+
instructions = meta.get("instructions", "")
|
|
284
|
+
tags = meta.get("tags", "")
|
|
285
|
+
requestor = meta.get("requestor")
|
|
286
|
+
|
|
287
|
+
prompt = (
|
|
288
|
+
f"You've been invited to join IRC room {channel}.\n"
|
|
289
|
+
f"Purpose: {purpose}\n"
|
|
290
|
+
f"Instructions: {instructions}\n"
|
|
291
|
+
f"Room tags: {tags}\n"
|
|
292
|
+
f"Your tags: {','.join(self.agent.tags)}\n\n"
|
|
293
|
+
"Think step-by-step about whether this room fits your current work "
|
|
294
|
+
"and capabilities. Then decide: should you join? Answer YES or NO."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if self._agent_runner is None or not self._agent_runner.is_running():
|
|
298
|
+
# No live agent — auto-join without evaluation
|
|
299
|
+
logger.info(
|
|
300
|
+
"ROOMINVITE for %s: no agent runner active, auto-joining %s",
|
|
301
|
+
self.agent.nick, channel,
|
|
302
|
+
)
|
|
303
|
+
assert self._transport is not None
|
|
304
|
+
await self._transport.send_raw(f"JOIN {channel}")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
# Use the agent runner to evaluate
|
|
308
|
+
# Enqueue a None relay target so the evaluation response doesn't
|
|
309
|
+
# steal a real mention's relay target from the deque.
|
|
310
|
+
self._mention_targets.append(None)
|
|
311
|
+
await self._agent_runner.send_prompt(prompt)
|
|
312
|
+
logger.info(
|
|
313
|
+
"ROOMINVITE for %s on %s — evaluation prompt sent to agent",
|
|
314
|
+
self.agent.nick, channel,
|
|
315
|
+
)
|
|
316
|
+
|
|
271
317
|
async def _on_agent_message(self, msg: dict) -> None:
|
|
272
318
|
"""Relay agent text to IRC and feed to supervisor."""
|
|
273
319
|
# Dequeue the relay target that corresponds to this turn
|
|
@@ -15,7 +15,9 @@ class IRCTransport:
|
|
|
15
15
|
|
|
16
16
|
def __init__(self, host: str, port: int, nick: str, user: str,
|
|
17
17
|
channels: list[str], buffer: MessageBuffer,
|
|
18
|
-
on_mention: Callable[[str, str, str], None] | None = None
|
|
18
|
+
on_mention: Callable[[str, str, str], None] | None = None,
|
|
19
|
+
tags: list[str] | None = None,
|
|
20
|
+
on_roominvite: Callable[[str, str], None] | None = None):
|
|
19
21
|
self.host = host
|
|
20
22
|
self.port = port
|
|
21
23
|
self.nick = nick
|
|
@@ -23,6 +25,8 @@ class IRCTransport:
|
|
|
23
25
|
self.channels = list(channels)
|
|
24
26
|
self.buffer = buffer
|
|
25
27
|
self.on_mention = on_mention
|
|
28
|
+
self.tags = tags or []
|
|
29
|
+
self.on_roominvite = on_roominvite
|
|
26
30
|
self.connected = False
|
|
27
31
|
self._reader: asyncio.StreamReader | None = None
|
|
28
32
|
self._writer: asyncio.StreamWriter | None = None
|
|
@@ -136,6 +140,10 @@ class IRCTransport:
|
|
|
136
140
|
self.connected = True
|
|
137
141
|
for channel in self.channels:
|
|
138
142
|
await self._send_raw(f"JOIN {channel}")
|
|
143
|
+
# Announce agent tags on connect
|
|
144
|
+
if self.tags:
|
|
145
|
+
tags_str = ",".join(self.tags)
|
|
146
|
+
await self._send_raw(f"TAGS {self.nick} {tags_str}")
|
|
139
147
|
elif msg.command == "PRIVMSG" and len(msg.params) >= 2:
|
|
140
148
|
target = msg.params[0]
|
|
141
149
|
text = msg.params[1]
|
|
@@ -154,3 +162,8 @@ class IRCTransport:
|
|
|
154
162
|
sender = msg.prefix.split("!")[0] if msg.prefix else "server"
|
|
155
163
|
if target.startswith("#"):
|
|
156
164
|
self.buffer.add(target, sender, text)
|
|
165
|
+
elif msg.command == "ROOMINVITE" and len(msg.params) >= 3:
|
|
166
|
+
channel = msg.params[0]
|
|
167
|
+
meta_text = msg.params[2]
|
|
168
|
+
if self.on_roominvite:
|
|
169
|
+
self.on_roominvite(channel, meta_text)
|
|
@@ -102,6 +102,8 @@ class OpenCodeDaemon:
|
|
|
102
102
|
channels=list(self.agent.channels),
|
|
103
103
|
buffer=self._buffer,
|
|
104
104
|
on_mention=self._on_mention,
|
|
105
|
+
tags=list(self.agent.tags),
|
|
106
|
+
on_roominvite=self._on_roominvite,
|
|
105
107
|
)
|
|
106
108
|
await self._transport.connect()
|
|
107
109
|
|
|
@@ -272,6 +274,50 @@ class OpenCodeDaemon:
|
|
|
272
274
|
prompt = f"[IRC DM] <{sender}> {text}"
|
|
273
275
|
asyncio.create_task(self._agent_runner.send_prompt(prompt))
|
|
274
276
|
|
|
277
|
+
def _on_roominvite(self, channel: str, meta_text: str) -> None:
|
|
278
|
+
"""Called by IRCTransport when a ROOMINVITE is received."""
|
|
279
|
+
asyncio.create_task(self._handle_roominvite(channel, meta_text))
|
|
280
|
+
|
|
281
|
+
async def _handle_roominvite(self, channel: str, meta_text: str) -> None:
|
|
282
|
+
"""Evaluate a room invitation using the agent's LLM."""
|
|
283
|
+
from agentirc.server.rooms_util import parse_room_meta
|
|
284
|
+
|
|
285
|
+
meta = parse_room_meta(meta_text)
|
|
286
|
+
purpose = meta.get("purpose", "")
|
|
287
|
+
instructions = meta.get("instructions", "")
|
|
288
|
+
tags = meta.get("tags", "")
|
|
289
|
+
requestor = meta.get("requestor")
|
|
290
|
+
|
|
291
|
+
prompt = (
|
|
292
|
+
f"You've been invited to join IRC room {channel}.\n"
|
|
293
|
+
f"Purpose: {purpose}\n"
|
|
294
|
+
f"Instructions: {instructions}\n"
|
|
295
|
+
f"Room tags: {tags}\n"
|
|
296
|
+
f"Your tags: {','.join(self.agent.tags)}\n\n"
|
|
297
|
+
"Think step-by-step about whether this room fits your current work "
|
|
298
|
+
"and capabilities. Then decide: should you join? Answer YES or NO."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if self._agent_runner is None or not self._agent_runner.is_running():
|
|
302
|
+
# No live agent — auto-join without evaluation
|
|
303
|
+
logger.info(
|
|
304
|
+
"ROOMINVITE for %s: no agent runner active, auto-joining %s",
|
|
305
|
+
self.agent.nick, channel,
|
|
306
|
+
)
|
|
307
|
+
assert self._transport is not None
|
|
308
|
+
await self._transport.send_raw(f"JOIN {channel}")
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
# Use the agent runner to evaluate
|
|
312
|
+
# Enqueue a None relay target so the evaluation response doesn't
|
|
313
|
+
# steal a real mention's relay target from the deque.
|
|
314
|
+
self._mention_targets.append(None)
|
|
315
|
+
await self._agent_runner.send_prompt(prompt)
|
|
316
|
+
logger.info(
|
|
317
|
+
"ROOMINVITE for %s on %s — evaluation prompt sent to agent",
|
|
318
|
+
self.agent.nick, channel,
|
|
319
|
+
)
|
|
320
|
+
|
|
275
321
|
async def _on_agent_message(self, msg: dict) -> None:
|
|
276
322
|
"""Relay agent text to IRC and feed to supervisor."""
|
|
277
323
|
# Dequeue the relay target that corresponds to this turn
|
|
@@ -15,7 +15,9 @@ class IRCTransport:
|
|
|
15
15
|
|
|
16
16
|
def __init__(self, host: str, port: int, nick: str, user: str,
|
|
17
17
|
channels: list[str], buffer: MessageBuffer,
|
|
18
|
-
on_mention: Callable[[str, str, str], None] | None = None
|
|
18
|
+
on_mention: Callable[[str, str, str], None] | None = None,
|
|
19
|
+
tags: list[str] | None = None,
|
|
20
|
+
on_roominvite: Callable[[str, str], None] | None = None):
|
|
19
21
|
self.host = host
|
|
20
22
|
self.port = port
|
|
21
23
|
self.nick = nick
|
|
@@ -23,6 +25,8 @@ class IRCTransport:
|
|
|
23
25
|
self.channels = list(channels)
|
|
24
26
|
self.buffer = buffer
|
|
25
27
|
self.on_mention = on_mention
|
|
28
|
+
self.tags = tags or []
|
|
29
|
+
self.on_roominvite = on_roominvite
|
|
26
30
|
self.connected = False
|
|
27
31
|
self._reader: asyncio.StreamReader | None = None
|
|
28
32
|
self._writer: asyncio.StreamWriter | None = None
|
|
@@ -136,6 +140,10 @@ class IRCTransport:
|
|
|
136
140
|
self.connected = True
|
|
137
141
|
for channel in self.channels:
|
|
138
142
|
await self._send_raw(f"JOIN {channel}")
|
|
143
|
+
# Announce agent tags on connect
|
|
144
|
+
if self.tags:
|
|
145
|
+
tags_str = ",".join(self.tags)
|
|
146
|
+
await self._send_raw(f"TAGS {self.nick} {tags_str}")
|
|
139
147
|
elif msg.command == "PRIVMSG" and len(msg.params) >= 2:
|
|
140
148
|
target = msg.params[0]
|
|
141
149
|
text = msg.params[1]
|
|
@@ -154,3 +162,8 @@ class IRCTransport:
|
|
|
154
162
|
sender = msg.prefix.split("!")[0] if msg.prefix else "server"
|
|
155
163
|
if target.startswith("#"):
|
|
156
164
|
self.buffer.add(target, sender, text)
|
|
165
|
+
elif msg.command == "ROOMINVITE" and len(msg.params) >= 3:
|
|
166
|
+
channel = msg.params[0]
|
|
167
|
+
meta_text = msg.params[2]
|
|
168
|
+
if self.on_roominvite:
|
|
169
|
+
self.on_roominvite(channel, meta_text)
|
|
@@ -63,6 +63,7 @@ async def collect_mesh_state(
|
|
|
63
63
|
room_agents.append(agent)
|
|
64
64
|
|
|
65
65
|
op_nicks = [n for n, is_op in members if is_op]
|
|
66
|
+
room_meta = await _query_roommeta(reader, writer, nick, ch_name)
|
|
66
67
|
rooms.append(Room(
|
|
67
68
|
name=ch_name,
|
|
68
69
|
topic=ch_topic,
|
|
@@ -70,6 +71,11 @@ async def collect_mesh_state(
|
|
|
70
71
|
operators=op_nicks,
|
|
71
72
|
federation_servers=sorted(fed_servers),
|
|
72
73
|
messages=messages,
|
|
74
|
+
room_id=room_meta.get("room_id"),
|
|
75
|
+
owner=room_meta.get("owner"),
|
|
76
|
+
purpose=room_meta.get("purpose"),
|
|
77
|
+
tags=room_meta.get("tags", []),
|
|
78
|
+
persistent=room_meta.get("persistent", False),
|
|
73
79
|
))
|
|
74
80
|
|
|
75
81
|
fed_links = sorted({a.server for a in all_agents.values() if a.server != server_name})
|
|
@@ -78,6 +84,11 @@ async def collect_mesh_state(
|
|
|
78
84
|
if ipc_enabled:
|
|
79
85
|
await _enrich_via_ipc(all_agents, server_name)
|
|
80
86
|
|
|
87
|
+
# Enrich local agents with TAGS
|
|
88
|
+
for agent_nick, agent in all_agents.items():
|
|
89
|
+
if agent.server == server_name:
|
|
90
|
+
agent.tags = await _query_tags(reader, writer, nick, agent_nick)
|
|
91
|
+
|
|
81
92
|
return MeshState(
|
|
82
93
|
server_name=server_name,
|
|
83
94
|
rooms=rooms,
|
|
@@ -245,6 +256,53 @@ async def _query_history(
|
|
|
245
256
|
return result
|
|
246
257
|
|
|
247
258
|
|
|
259
|
+
async def _query_roommeta(
|
|
260
|
+
reader: asyncio.StreamReader,
|
|
261
|
+
writer: asyncio.StreamWriter,
|
|
262
|
+
nick: str,
|
|
263
|
+
channel: str,
|
|
264
|
+
) -> dict:
|
|
265
|
+
"""Query ROOMMETA and return a dict with room metadata fields."""
|
|
266
|
+
writer.write(f"ROOMMETA {channel}\r\n".encode())
|
|
267
|
+
await writer.drain()
|
|
268
|
+
messages = await _recv_until(reader, writer, {"ROOMETAEND", "ERR_NOSUCHCHANNEL", "ERR_UNKNOWNCOMMAND"})
|
|
269
|
+
result: dict = {}
|
|
270
|
+
for msg in messages:
|
|
271
|
+
if msg.command == "ROOMMETA" and len(msg.params) >= 3:
|
|
272
|
+
# Server sends: ROOMMETA <channel> <key> <value>
|
|
273
|
+
key = msg.params[1].strip().lower()
|
|
274
|
+
value = msg.params[2]
|
|
275
|
+
if key == "room_id":
|
|
276
|
+
result["room_id"] = value
|
|
277
|
+
elif key == "owner":
|
|
278
|
+
result["owner"] = value
|
|
279
|
+
elif key == "purpose":
|
|
280
|
+
result["purpose"] = value
|
|
281
|
+
elif key == "tags":
|
|
282
|
+
result["tags"] = [t.strip() for t in value.split(",") if t.strip()]
|
|
283
|
+
elif key == "persistent":
|
|
284
|
+
result["persistent"] = value.lower() in ("1", "true", "yes")
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
async def _query_tags(
|
|
289
|
+
reader: asyncio.StreamReader,
|
|
290
|
+
writer: asyncio.StreamWriter,
|
|
291
|
+
nick: str,
|
|
292
|
+
target_nick: str,
|
|
293
|
+
) -> list[str]:
|
|
294
|
+
"""Query TAGS for an agent and return a list of tag strings."""
|
|
295
|
+
writer.write(f"TAGS {target_nick}\r\n".encode())
|
|
296
|
+
await writer.drain()
|
|
297
|
+
messages = await _recv_until(reader, writer, {"TAGSEND", "ERR_NOSUCHNICK", "ERR_UNKNOWNCOMMAND"})
|
|
298
|
+
for msg in messages:
|
|
299
|
+
if msg.command == "TAGS" and len(msg.params) >= 2:
|
|
300
|
+
# Expected format: TAGS <nick> <tag1,tag2,...>
|
|
301
|
+
tags_str = msg.params[-1]
|
|
302
|
+
return [t.strip() for t in tags_str.split(",") if t.strip()]
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
|
|
248
306
|
async def _enrich_via_ipc(agents: dict[str, Agent], server_name: str) -> None:
|
|
249
307
|
"""Enrich local agents with daemon IPC status data."""
|
|
250
308
|
from agentirc.clients.claude.ipc import decode_message, encode_message, make_request
|
|
@@ -27,6 +27,7 @@ class Agent:
|
|
|
27
27
|
directory: str | None = None
|
|
28
28
|
turns: int | None = None
|
|
29
29
|
uptime: str | None = None
|
|
30
|
+
tags: list[str] = field(default_factory=list)
|
|
30
31
|
|
|
31
32
|
@property
|
|
32
33
|
def is_local(self) -> bool:
|
|
@@ -42,6 +43,11 @@ class Room:
|
|
|
42
43
|
operators: list[str]
|
|
43
44
|
federation_servers: list[str]
|
|
44
45
|
messages: list[Message]
|
|
46
|
+
room_id: str | None = None
|
|
47
|
+
owner: str | None = None
|
|
48
|
+
purpose: str | None = None
|
|
49
|
+
tags: list[str] = field(default_factory=list)
|
|
50
|
+
persistent: bool = False
|
|
45
51
|
|
|
46
52
|
|
|
47
53
|
@dataclass
|
|
@@ -50,6 +50,16 @@ def _render_room(room: Room, message_limit: int) -> str:
|
|
|
50
50
|
"""Render a single room section."""
|
|
51
51
|
parts = [f"## {room.name}"]
|
|
52
52
|
parts.append(f"Topic: {room.topic}" if room.topic else "Topic: (none)")
|
|
53
|
+
if room.room_id:
|
|
54
|
+
parts.append(f"Purpose: {room.purpose or ''}")
|
|
55
|
+
parts.append(f"Tags: {', '.join(room.tags) if room.tags else 'none'}")
|
|
56
|
+
meta_parts = []
|
|
57
|
+
if room.owner:
|
|
58
|
+
meta_parts.append(f"Owner: {room.owner}")
|
|
59
|
+
if room.persistent:
|
|
60
|
+
meta_parts.append("Persistent")
|
|
61
|
+
if meta_parts:
|
|
62
|
+
parts.append(" | ".join(meta_parts))
|
|
53
63
|
parts.append("")
|
|
54
64
|
parts.append(_agent_table(room.members))
|
|
55
65
|
parts.append("")
|
|
@@ -150,11 +160,13 @@ def _render_agent_detail(mesh: MeshState, nick: str, message_limit: int) -> str:
|
|
|
150
160
|
rows.append(("Turns", str(agent.turns)))
|
|
151
161
|
if agent.uptime:
|
|
152
162
|
rows.append(("Uptime", agent.uptime))
|
|
163
|
+
if agent.tags:
|
|
164
|
+
rows.append(("Tags", ", ".join(agent.tags)))
|
|
153
165
|
|
|
154
166
|
parts.append("| Field | Value |")
|
|
155
167
|
parts.append("|-------|-------|")
|
|
156
168
|
for field_name, value in rows:
|
|
157
|
-
parts.append(f"| {field_name} | {value} |")
|
|
169
|
+
parts.append(f"| {field_name} | {_escape_cell(value)} |")
|
|
158
170
|
|
|
159
171
|
# Channels table
|
|
160
172
|
parts.append("")
|