agentirc-cli 0.14.1__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.
Files changed (225) hide show
  1. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/.gitignore +4 -0
  2. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/CHANGELOG.md +21 -0
  3. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/CLAUDE.md +2 -0
  4. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/PKG-INFO +1 -1
  5. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/config.py +1 -0
  6. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/daemon.py +47 -0
  7. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/irc_transport.py +14 -1
  8. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/config.py +1 -0
  9. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/daemon.py +46 -0
  10. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/irc_transport.py +14 -1
  11. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/config.py +1 -0
  12. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/daemon.py +46 -0
  13. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/irc_transport.py +14 -1
  14. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/config.py +1 -0
  15. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/daemon.py +46 -0
  16. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/irc_transport.py +14 -1
  17. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/overview/collector.py +58 -0
  18. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/overview/model.py +6 -0
  19. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/overview/renderer_text.py +12 -0
  20. agentirc_cli-0.15.0/agentirc/protocol/extensions/rooms.md +66 -0
  21. agentirc_cli-0.15.0/agentirc/protocol/extensions/tags.md +32 -0
  22. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/channel.py +18 -0
  23. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/client.py +14 -1
  24. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/config.py +1 -0
  25. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/ircd.py +27 -1
  26. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/remote_client.py +1 -0
  27. agentirc_cli-0.15.0/agentirc/server/room_store.py +63 -0
  28. agentirc_cli-0.15.0/agentirc/server/rooms_util.py +55 -0
  29. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/server_link.py +126 -0
  30. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/skill.py +3 -0
  31. agentirc_cli-0.15.0/agentirc/server/skills/rooms.py +669 -0
  32. agentirc_cli-0.15.0/docs/rooms.md +80 -0
  33. agentirc_cli-0.15.0/docs/superpowers/plans/2026-03-30-rooms-management.md +3173 -0
  34. agentirc_cli-0.15.0/docs/superpowers/specs/2026-03-30-rooms-management-design.md +488 -0
  35. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/pyproject.toml +1 -1
  36. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_overview_model.py +54 -0
  37. agentirc_cli-0.15.0/tests/test_room_persistence.py +75 -0
  38. agentirc_cli-0.15.0/tests/test_rooms.py +732 -0
  39. agentirc_cli-0.15.0/tests/test_rooms_federation.py +63 -0
  40. agentirc_cli-0.15.0/tests/test_rooms_integration.py +110 -0
  41. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/uv.lock +1 -1
  42. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/.claude/skills/pr-review/SKILL.md +0 -0
  43. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/.github/workflows/pages.yml +0 -0
  44. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/.github/workflows/publish.yml +0 -0
  45. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/.github/workflows/tests.yml +0 -0
  46. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/.markdownlint-cli2.yaml +0 -0
  47. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/.pr_agent.toml +0 -0
  48. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/CNAME +0 -0
  49. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/Gemfile +0 -0
  50. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/Gemfile.lock +0 -0
  51. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/LICENSE +0 -0
  52. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/README.md +0 -0
  53. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/_config.yml +0 -0
  54. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/_sass/color_schemes/anthropic.scss +0 -0
  55. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/_sass/custom/custom.scss +0 -0
  56. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/__init__.py +0 -0
  57. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/__main__.py +0 -0
  58. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/cli.py +0 -0
  59. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/__init__.py +0 -0
  60. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/__init__.py +0 -0
  61. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/__main__.py +0 -0
  62. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/agent_runner.py +0 -0
  63. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/ipc.py +0 -0
  64. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/message_buffer.py +0 -0
  65. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
  66. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/skill/__init__.py +0 -0
  67. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
  68. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/socket_server.py +0 -0
  69. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/supervisor.py +0 -0
  70. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/claude/webhook.py +0 -0
  71. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/__init__.py +0 -0
  72. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/agent_runner.py +0 -0
  73. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/ipc.py +0 -0
  74. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/message_buffer.py +0 -0
  75. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
  76. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/skill/__init__.py +0 -0
  77. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
  78. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/socket_server.py +0 -0
  79. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/supervisor.py +0 -0
  80. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/codex/webhook.py +0 -0
  81. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/__init__.py +0 -0
  82. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/agent_runner.py +0 -0
  83. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/ipc.py +0 -0
  84. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/message_buffer.py +0 -0
  85. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
  86. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/skill/__init__.py +0 -0
  87. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
  88. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/socket_server.py +0 -0
  89. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/supervisor.py +0 -0
  90. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/copilot/webhook.py +0 -0
  91. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/__init__.py +0 -0
  92. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/agent_runner.py +0 -0
  93. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/ipc.py +0 -0
  94. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/message_buffer.py +0 -0
  95. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/skill/SKILL.md +0 -0
  96. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/skill/__init__.py +0 -0
  97. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/skill/irc_client.py +0 -0
  98. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/socket_server.py +0 -0
  99. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/supervisor.py +0 -0
  100. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/clients/opencode/webhook.py +0 -0
  101. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/learn_prompt.py +0 -0
  102. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/observer.py +0 -0
  103. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/overview/__init__.py +0 -0
  104. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/overview/renderer_web.py +0 -0
  105. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/overview/web/style.css +0 -0
  106. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/pidfile.py +0 -0
  107. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/protocol/__init__.py +0 -0
  108. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/protocol/commands.py +0 -0
  109. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/protocol/extensions/federation.md +0 -0
  110. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/protocol/extensions/history.md +0 -0
  111. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/protocol/message.py +0 -0
  112. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/protocol/protocol-index.md +0 -0
  113. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/protocol/replies.py +0 -0
  114. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/__init__.py +0 -0
  115. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/__main__.py +0 -0
  116. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/skills/__init__.py +0 -0
  117. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/agentirc/server/skills/history.py +0 -0
  118. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/agent-client.md +0 -0
  119. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/agent-harness-spec.md +0 -0
  120. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/ci.md +0 -0
  121. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/cli.md +0 -0
  122. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/claude/configuration.md +0 -0
  123. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/claude/context-management.md +0 -0
  124. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/claude/irc-tools.md +0 -0
  125. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/claude/overview.md +0 -0
  126. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/claude/setup.md +0 -0
  127. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/claude/supervisor.md +0 -0
  128. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/claude/webhooks.md +0 -0
  129. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/codex/configuration.md +0 -0
  130. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/codex/context-management.md +0 -0
  131. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/codex/irc-tools.md +0 -0
  132. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/codex/overview.md +0 -0
  133. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/codex/setup.md +0 -0
  134. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/codex/supervisor.md +0 -0
  135. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/codex/webhooks.md +0 -0
  136. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/copilot/configuration.md +0 -0
  137. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/copilot/context-management.md +0 -0
  138. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/copilot/irc-tools.md +0 -0
  139. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/copilot/overview.md +0 -0
  140. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/copilot/setup.md +0 -0
  141. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/copilot/supervisor.md +0 -0
  142. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/copilot/webhooks.md +0 -0
  143. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/opencode/configuration.md +0 -0
  144. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/opencode/context-management.md +0 -0
  145. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/opencode/irc-tools.md +0 -0
  146. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/opencode/overview.md +0 -0
  147. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/opencode/setup.md +0 -0
  148. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/opencode/supervisor.md +0 -0
  149. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/clients/opencode/webhooks.md +0 -0
  150. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/codex-backend.md +0 -0
  151. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/copilot-backend.md +0 -0
  152. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/design.md +0 -0
  153. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/docs-site.md +0 -0
  154. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/getting-started.md +0 -0
  155. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/grow-your-agent.md +0 -0
  156. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/layer1-core-irc.md +0 -0
  157. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/layer2-attention.md +0 -0
  158. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/layer3-skills.md +0 -0
  159. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/layer4-federation.md +0 -0
  160. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/layer5-agent-harness.md +0 -0
  161. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/opencode-backend.md +0 -0
  162. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/overview.md +0 -0
  163. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/publishing.md +0 -0
  164. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  165. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/server-architecture.md +0 -0
  166. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  167. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  168. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  169. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  170. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/01-pair-programming.md +0 -0
  171. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
  172. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
  173. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
  174. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/05-the-observer.md +0 -0
  175. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/06-cross-server-ops.md +0 -0
  176. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
  177. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/08-apps-as-agents.md +0 -0
  178. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/09-research-swarm.md +0 -0
  179. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases/10-grow-your-agent.md +0 -0
  180. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/docs/use-cases-index.md +0 -0
  181. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/index.md +0 -0
  182. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/README.md +0 -0
  183. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/config.py +0 -0
  184. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/daemon.py +0 -0
  185. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/ipc.py +0 -0
  186. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/irc_transport.py +0 -0
  187. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/message_buffer.py +0 -0
  188. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/skill/SKILL.md +0 -0
  189. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/skill/irc_client.py +0 -0
  190. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/socket_server.py +0 -0
  191. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/packages/agent-harness/webhook.py +0 -0
  192. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  193. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  194. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
  195. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/__init__.py +0 -0
  196. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/conftest.py +0 -0
  197. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_agent_runner.py +0 -0
  198. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_channel.py +0 -0
  199. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_codex_daemon.py +0 -0
  200. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_connection.py +0 -0
  201. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_copilot_daemon.py +0 -0
  202. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_daemon.py +0 -0
  203. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_daemon_config.py +0 -0
  204. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_daemon_ipc.py +0 -0
  205. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_discovery.py +0 -0
  206. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_federation.py +0 -0
  207. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_history.py +0 -0
  208. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_integration_layer5.py +0 -0
  209. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_ipc.py +0 -0
  210. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_irc_transport.py +0 -0
  211. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_mentions.py +0 -0
  212. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_message.py +0 -0
  213. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_message_buffer.py +0 -0
  214. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_messaging.py +0 -0
  215. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_modes.py +0 -0
  216. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_opencode_daemon.py +0 -0
  217. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_overview_cli.py +0 -0
  218. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_overview_collector.py +0 -0
  219. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_overview_renderer.py +0 -0
  220. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_overview_web.py +0 -0
  221. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_skill_client.py +0 -0
  222. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_skills.py +0 -0
  223. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_socket_server.py +0 -0
  224. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_supervisor.py +0 -0
  225. {agentirc_cli-0.14.1 → agentirc_cli-0.15.0}/tests/test_webhook.py +0 -0
@@ -205,3 +205,7 @@ cython_debug/
205
205
  marimo/_static/
206
206
  marimo/_lsp/
207
207
  __marimo__/
208
+
209
+ # Superpowers & worktrees
210
+ .superpowers/
211
+ .worktrees/
@@ -4,6 +4,27 @@ 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
+
7
28
  ## [0.14.1] - 2026-03-30
8
29
 
9
30
 
@@ -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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 0.14.1
3
+ Version: 0.15.0
4
4
  Summary: IRC protocol chatrooms for AI agents (and humans allowed)
5
5
  Project-URL: Homepage, https://github.com/OriNachum/agentirc
6
6
  Author: Ori Nachum
@@ -49,6 +49,7 @@ class AgentConfig:
49
49
  model: str = "claude-opus-4-6"
50
50
  thinking: str = "medium"
51
51
  system_prompt: str = ""
52
+ tags: list[str] = field(default_factory=list)
52
53
 
53
54
 
54
55
  @dataclass
@@ -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)
@@ -47,6 +47,7 @@ class AgentConfig:
47
47
  channels: list[str] = field(default_factory=lambda: ["#general"])
48
48
  model: str = "gpt-5.4"
49
49
  system_prompt: str = ""
50
+ tags: list[str] = field(default_factory=list)
50
51
 
51
52
 
52
53
  @dataclass
@@ -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)
@@ -47,6 +47,7 @@ class AgentConfig:
47
47
  channels: list[str] = field(default_factory=lambda: ["#general"])
48
48
  model: str = "gpt-4.1"
49
49
  system_prompt: str = ""
50
+ tags: list[str] = field(default_factory=list)
50
51
 
51
52
 
52
53
  @dataclass
@@ -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)
@@ -47,6 +47,7 @@ class AgentConfig:
47
47
  channels: list[str] = field(default_factory=lambda: ["#general"])
48
48
  model: str = "anthropic/claude-sonnet-4-6"
49
49
  system_prompt: str = ""
50
+ tags: list[str] = field(default_factory=list)
50
51
 
51
52
 
52
53
  @dataclass
@@ -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,6 +160,8 @@ 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("|-------|-------|")