agentirc-cli 9.12.0__tar.gz → 9.13.1__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 (201) hide show
  1. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.github/workflows/tests.yml +18 -0
  2. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/CHANGELOG.md +33 -0
  3. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/PKG-INFO +8 -2
  4. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/README.md +6 -0
  5. agentirc_cli-9.13.1/TRADEMARKS.md +38 -0
  6. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/protocol/replies.py +5 -0
  7. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/client.py +19 -2
  8. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/protocol.py +6 -0
  9. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/api-stability.md +28 -1
  10. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/extension-api.md +1 -1
  11. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/pyproject.toml +1 -1
  12. agentirc_cli-9.13.1/scripts/sonar-pr-issues.py +168 -0
  13. agentirc_cli-9.13.1/sonar-project.properties +6 -0
  14. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_liveness.py +2 -2
  15. agentirc_cli-9.13.1/tests/test_motd.py +69 -0
  16. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_presence_stale.py +2 -2
  17. agentirc_cli-9.13.1/tests/test_sonar_pr_issues.py +128 -0
  18. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/uv.lock +2 -2
  19. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/ask-colleague/SKILL.md +0 -0
  20. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  21. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  22. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  23. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
  24. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  25. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  26. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/cicd/SKILL.md +0 -0
  27. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  28. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  29. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  30. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  31. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  32. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/communicate/SKILL.md +0 -0
  33. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  34. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  35. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  36. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  37. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/recall/SKILL.md +0 -0
  38. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/recall/scripts/recall.sh +0 -0
  39. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/remember/SKILL.md +0 -0
  40. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/remember/scripts/remember.sh +0 -0
  41. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  42. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  43. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/think/SKILL.md +0 -0
  44. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills/think/scripts/think.sh +0 -0
  45. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.claude/skills.local.yaml.example +0 -0
  46. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.devague/current +0 -0
  47. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.devague/current_plan +0 -0
  48. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.devague/frames/agentirc-9-7-0-ships-an-embedded-bot-framework-bot.json +0 -0
  49. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.devague/frames/agentirc-now-speaks-presence-the-ircd-parses-resid.json +0 -0
  50. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.devague/frames/agentirc-ships-an-agent-accessibility-release-ai-a.json +0 -0
  51. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.devague/plans/agentirc-9-7-0-ships-an-embedded-bot-framework-bot.json +0 -0
  52. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.devague/plans/agentirc-now-speaks-presence-the-ircd-parses-resid.json +0 -0
  53. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.devague/plans/agentirc-ships-an-agent-accessibility-release-ai-a.json +0 -0
  54. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.github/workflows/publish.yml +0 -0
  55. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/.gitignore +0 -0
  56. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/CLAUDE.md +0 -0
  57. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/LICENSE +0 -0
  58. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/__init__.py +0 -0
  59. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/__main__.py +0 -0
  60. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/__init__.py +0 -0
  61. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/aio.py +0 -0
  62. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/bots/__init__.py +0 -0
  63. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/bots/bot_manager.py +0 -0
  64. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/bots/http_listener.py +0 -0
  65. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/cli_client.py +0 -0
  66. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/cli_shared/__init__.py +0 -0
  67. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/cli_shared/constants.py +0 -0
  68. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/cli_shared/mesh.py +0 -0
  69. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/constants.py +0 -0
  70. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/event_subscriptions.py +0 -0
  71. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/pidfile.py +0 -0
  72. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/protocol/__init__.py +0 -0
  73. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/protocol/message.py +0 -0
  74. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/telemetry/__init__.py +0 -0
  75. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/telemetry/audit.py +0 -0
  76. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/telemetry/context.py +0 -0
  77. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/telemetry/metrics.py +0 -0
  78. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/telemetry/tracing.py +0 -0
  79. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/_internal/virtual_client.py +0 -0
  80. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/agent_client.py +0 -0
  81. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/__init__.py +0 -0
  82. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/bot.py +0 -0
  83. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/bot_manager.py +0 -0
  84. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/cli.py +0 -0
  85. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/config.py +0 -0
  86. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/filter_dsl.py +0 -0
  87. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/http_listener.py +0 -0
  88. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/template_engine.py +0 -0
  89. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/bots/virtual_client.py +0 -0
  90. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/channel.py +0 -0
  91. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/cli.py +0 -0
  92. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/config.py +0 -0
  93. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/events.py +0 -0
  94. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/history_store.py +0 -0
  95. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/ircd.py +0 -0
  96. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/remote_client.py +0 -0
  97. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/room_store.py +0 -0
  98. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/rooms_util.py +0 -0
  99. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/server_link.py +0 -0
  100. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/skill.py +0 -0
  101. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/skills/__init__.py +0 -0
  102. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/skills/history.py +0 -0
  103. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/skills/icon.py +0 -0
  104. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/skills/presence.py +0 -0
  105. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/skills/rooms.py +0 -0
  106. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/skills/threads.py +0 -0
  107. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/thread_store.py +0 -0
  108. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/agentirc/virtual_client.py +0 -0
  109. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/agent-walkthrough.md +0 -0
  110. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/bots.md +0 -0
  111. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/cli.md +0 -0
  112. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/deployment.md +0 -0
  113. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/plans/2026-06-12-agentirc-9-7-0-ships-an-embedded-bot-framework-bot.md +0 -0
  114. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/plans/2026-07-01-agentirc-ships-an-agent-accessibility-release-ai-a.md +0 -0
  115. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/plans/2026-07-07-agentirc-now-speaks-presence-the-ircd-parses-resid.md +0 -0
  116. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/specs/2026-06-12-agentirc-9-7-0-ships-an-embedded-bot-framework-bot.md +0 -0
  117. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/specs/2026-07-01-agentirc-ships-an-agent-accessibility-release-ai-a.md +0 -0
  118. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/specs/2026-07-02-agent-accessibility-gap-verification.md +0 -0
  119. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/specs/2026-07-02-agent-accessibility-release-audit.md +0 -0
  120. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/specs/2026-07-07-agentirc-now-speaks-presence-the-ircd-parses-resid.md +0 -0
  121. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/steward/onboarding.md +0 -0
  122. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/superpowers/specs/2026-04-30-bootstrap-design.md +0 -0
  123. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/superpowers/specs/2026-05-01-bot-extension-api-design.md +0 -0
  124. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/docs/superpowers/specs/2026-05-01-task14-audit.md +0 -0
  125. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/__init__.py +0 -0
  126. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/_helpers.py +0 -0
  127. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/__init__.py +0 -0
  128. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_bot.py +0 -0
  129. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_bot_host.py +0 -0
  130. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_bot_manager.py +0 -0
  131. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_cli_bot.py +0 -0
  132. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_config_bots.py +0 -0
  133. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_filter_dsl.py +0 -0
  134. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_http_listener.py +0 -0
  135. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_public_surface.py +0 -0
  136. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/bots/test_template_engine.py +0 -0
  137. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/conftest.py +0 -0
  138. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/__init__.py +0 -0
  139. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/_fakes.py +0 -0
  140. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/_metrics_helpers.py +0 -0
  141. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_audit_emit.py +0 -0
  142. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_audit_lifecycle.py +0 -0
  143. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_audit_module.py +0 -0
  144. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_audit_parse_error.py +0 -0
  145. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_config.py +0 -0
  146. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_dispatch_span.py +0 -0
  147. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_emit_event_span.py +0 -0
  148. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_metrics_init.py +0 -0
  149. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_metrics_s2s.py +0 -0
  150. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_outbound_inject.py +0 -0
  151. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_parse_error.py +0 -0
  152. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_s2s_relay_span.py +0 -0
  153. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_server_init.py +0 -0
  154. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_server_link_inject.py +0 -0
  155. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/telemetry/test_tracing.py +0 -0
  156. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_agent_client.py +0 -0
  157. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_agent_walkthrough.py +0 -0
  158. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_api_stability_embedding.py +0 -0
  159. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_bot_capability.py +0 -0
  160. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_channel.py +0 -0
  161. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_cli.py +0 -0
  162. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_cli_client_verbs.py +0 -0
  163. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_cli_json.py +0 -0
  164. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_client_backfill.py +0 -0
  165. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_config_loader.py +0 -0
  166. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_connection.py +0 -0
  167. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_discovery.py +0 -0
  168. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_disk_paths.py +0 -0
  169. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_dm_history.py +0 -0
  170. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_docs_promises.py +0 -0
  171. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_error_tokens.py +0 -0
  172. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_event_subscriptions.py +0 -0
  173. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_eventpub.py +0 -0
  174. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_events_basic.py +0 -0
  175. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_events_catalog.py +0 -0
  176. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_events_federation.py +0 -0
  177. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_events_history.py +0 -0
  178. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_events_lifecycle.py +0 -0
  179. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_events_reserved_nick.py +0 -0
  180. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_federation.py +0 -0
  181. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_history.py +0 -0
  182. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_history_since.py +0 -0
  183. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_link_reconnect.py +0 -0
  184. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_long_messages.py +0 -0
  185. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_mentions.py +0 -0
  186. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_message_tags.py +0 -0
  187. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_messaging.py +0 -0
  188. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_modes.py +0 -0
  189. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_presence.py +0 -0
  190. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_presence_federation.py +0 -0
  191. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_protocol_bot_exports.py +0 -0
  192. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_protocol_presence.py +0 -0
  193. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_reconnect_catchup.py +0 -0
  194. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_room_persistence.py +0 -0
  195. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_rooms_federation.py +0 -0
  196. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_rooms_integration.py +0 -0
  197. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_server_icon_skill.py +0 -0
  198. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_skills.py +0 -0
  199. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_threads.py +0 -0
  200. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_verb_discovery.py +0 -0
  201. {agentirc_cli-9.12.0 → agentirc_cli-9.13.1}/tests/test_wire_format_envelope.py +0 -0
@@ -24,6 +24,11 @@ jobs:
24
24
  runs-on: ubuntu-latest
25
25
  permissions:
26
26
  contents: read
27
+ # A presence flag, not the secret: `secrets.*` isn't allowed in `if:`.
28
+ # Fork and Dependabot PRs get no SONAR_TOKEN, so the SonarCloud gate skips
29
+ # there instead of waiting out its 300 s timeout.
30
+ env:
31
+ SONAR_TOKEN_PRESENT: ${{ secrets.SONAR_TOKEN != '' }}
27
32
  steps:
28
33
  - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
29
34
 
@@ -57,6 +62,19 @@ jobs:
57
62
  UV_PROJECT_ENVIRONMENT: .venv
58
63
  run: uv run pytest -n auto -v
59
64
 
65
+ # No unresolved SonarCloud issue on the PR. SonarCloud's automatic analysis
66
+ # (the GitHub App) scans the PR; a code smell leaves its quality gate green,
67
+ # so this fails the job while the PR has any OPEN or CONFIRMED issue.
68
+ # ACCEPTED and FALSE_POSITIVE ones pass. Cited from culture-rules (#43).
69
+ - name: SonarCloud unresolved issues
70
+ if: steps.bootstrap.outputs.ready == 'true' && env.SONAR_TOKEN_PRESENT == 'true' && github.event_name == 'pull_request'
71
+ env:
72
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
73
+ SONAR_HOST_URL: https://sonarcloud.io
74
+ PR_NUMBER: ${{ github.event.pull_request.number }}
75
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
76
+ run: python3 scripts/sonar-pr-issues.py
77
+
60
78
  version-check:
61
79
  needs: lint
62
80
  runs-on: ubuntu-latest
@@ -4,6 +4,39 @@ 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
+ ## [9.13.1] - 2026-10-30
8
+
9
+ ### Added
10
+
11
+ - CI fails a pull request that has an unresolved SonarCloud issue: after
12
+ SonarCloud's automatic analysis of the PR's head commit,
13
+ `scripts/sonar-pr-issues.py` (cited from culture-rules) fails on any OPEN or
14
+ CONFIRMED issue. ACCEPTED and FALSE_POSITIVE issues pass. Same gate as
15
+ culture, irc-lens and culture-library. `sonar-project.properties` names the
16
+ project key for it.
17
+
18
+ ## [9.13.0] - 2026-10-30
19
+
20
+ First agentirc release that presents itself as CULTURE.DEV on the wire —
21
+ first-use evidence for the IRC surface of the CULTURE.DEV trademark (culture
22
+ spec `docs/specs/2026-10-30-culture-dev-on-all-public-surfaces.md`).
23
+
24
+ ### Added
25
+
26
+ - MOTD: registration now ends with `375`/`372`/`376` carrying a fixed
27
+ CULTURE.DEV message of the day, and the `MOTD` command (registered clients)
28
+ replays it. `RPL_MOTDSTART`, `RPL_MOTD` and `RPL_ENDOFMOTD`
29
+ constants added; no `422` path since a MOTD is always present.
30
+ - `TRADEMARKS.md` — CULTURE.DEV is a trademark of Ori Nachum; Apache-2.0
31
+ grants no trademark rights.
32
+
33
+ ### Changed
34
+
35
+ - `001` now reads `Welcome to CULTURE.DEV on <server> <prefix>` and `002`
36
+ reads `Your host is <server>, running CULTURE.DEV`. The `004` version token
37
+ (`culture`) and the `culture.dev/traceparent` / `tracestate` tags are
38
+ unchanged.
39
+
7
40
  ## [9.12.0] - 2026-07-07
8
41
 
9
42
  The PRESENCE release: the IRCd now parses resident presence heartbeats,
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: agentirc-cli
3
- Version: 9.12.0
3
+ Version: 9.13.1
4
4
  Summary: Agent-friendly IRCd: server core for AI agent meshes
5
5
  Project-URL: Homepage, https://github.com/agentculture/agentirc
6
6
  Project-URL: Issues, https://github.com/agentculture/agentirc/issues
@@ -356,3 +356,9 @@ Dev setup: `uv venv && uv pip install -e ".[dev]"`, then `pytest -n auto`
356
356
  ## License
357
357
 
358
358
  Apache 2.0 — see [`LICENSE`](LICENSE).
359
+
360
+ ## Trademarks
361
+
362
+ CULTURE.DEV is a trademark of Ori Nachum. agentirc greets clients as the
363
+ CULTURE.DEV server; the Apache 2.0 license does not grant trademark rights —
364
+ see [`TRADEMARKS.md`](TRADEMARKS.md).
@@ -317,3 +317,9 @@ Dev setup: `uv venv && uv pip install -e ".[dev]"`, then `pytest -n auto`
317
317
  ## License
318
318
 
319
319
  Apache 2.0 — see [`LICENSE`](LICENSE).
320
+
321
+ ## Trademarks
322
+
323
+ CULTURE.DEV is a trademark of Ori Nachum. agentirc greets clients as the
324
+ CULTURE.DEV server; the Apache 2.0 license does not grant trademark rights —
325
+ see [`TRADEMARKS.md`](TRADEMARKS.md).
@@ -0,0 +1,38 @@
1
+ # Trademarks
2
+
3
+ CULTURE.DEV is a trademark of Ori Nachum. A US trademark application for
4
+ the mark is pending; it is marked CULTURE.DEV™ until registration.
5
+
6
+ CULTURE.DEV names the product and service available at
7
+ [culture.dev](https://culture.dev) and
8
+ [chat.culture.dev](https://chat.culture.dev): the CULTURE.DEV CLI
9
+ (installed with `uv tool install culture`), the CULTURE.DEV live chat, and
10
+ the software, documentation, and agent mesh behind them.
11
+
12
+ ## Trademarks are not licensed with the code
13
+
14
+ The source code and documentation in this repository are licensed under the
15
+ Apache License 2.0 (see `LICENSE`). Section 6 of that license states that it
16
+ does not grant permission to use the licensor's trade names, trademarks,
17
+ service marks, or product names. The same applies here: no trademark
18
+ license, and no right to use the CULTURE.DEV name or logos or to imply
19
+ affiliation or endorsement, is granted unless explicitly stated in writing.
20
+
21
+ The same reservation covers the related names AgentCulture and AgentIRC and
22
+ the CULTURE.DEV logos and badges.
23
+
24
+ ## What you can do
25
+
26
+ - Refer to CULTURE.DEV by name to describe it truthfully, for example "works
27
+ with CULTURE.DEV" or "a fork of the CULTURE.DEV CLI".
28
+ - Keep the command and package names (`culture`, `agentirc-cli`, `irc-lens`)
29
+ when you redistribute the code under its license.
30
+
31
+ ## What needs written permission
32
+
33
+ - Using CULTURE.DEV, or a confusingly similar name or logo, as the name of
34
+ your own product, service, domain, or organization.
35
+ - Suggesting that your product, fork, or service is made, sponsored, or
36
+ endorsed by CULTURE.DEV.
37
+
38
+ Questions: [inbox@culture.dev](mailto:inbox@culture.dev).
@@ -6,6 +6,11 @@ RPL_YOURHOST = "002"
6
6
  RPL_CREATED = "003"
7
7
  RPL_MYINFO = "004"
8
8
 
9
+ # MOTD
10
+ RPL_MOTDSTART = "375"
11
+ RPL_MOTD = "372"
12
+ RPL_ENDOFMOTD = "376"
13
+
9
14
  # Channel
10
15
  RPL_LISTSTART = "321"
11
16
  RPL_LIST = "322"
@@ -521,11 +521,11 @@ class Client:
521
521
  async def _send_welcome(self) -> None:
522
522
  await self.send_numeric(
523
523
  replies.RPL_WELCOME,
524
- f"Welcome to {self.server.config.name} IRC Network {self.prefix}",
524
+ f"Welcome to CULTURE.DEV on {self.server.config.name} {self.prefix}",
525
525
  )
526
526
  await self.send_numeric(
527
527
  replies.RPL_YOURHOST,
528
- f"Your host is {self.server.config.name}, running culture",
528
+ f"Your host is {self.server.config.name}, running CULTURE.DEV",
529
529
  )
530
530
  await self.send_numeric(
531
531
  replies.RPL_CREATED,
@@ -538,6 +538,23 @@ class Client:
538
538
  "o",
539
539
  "ov",
540
540
  )
541
+ await self._send_motd()
542
+
543
+ async def _send_motd(self) -> None:
544
+ """Send the fixed CULTURE.DEV MOTD (375, 372 per line, 376)."""
545
+ name = self.server.config.name
546
+ await self.send_numeric(replies.RPL_MOTDSTART, f"- {name} Message of the day -")
547
+ for line in (
548
+ f"- {name} - CULTURE.DEV",
549
+ "- The professional workspace for agents. https://culture.dev",
550
+ ):
551
+ await self.send_numeric(replies.RPL_MOTD, line)
552
+ await self.send_numeric(replies.RPL_ENDOFMOTD, "End of /MOTD command")
553
+
554
+ async def _handle_motd(self, msg: Message) -> None:
555
+ if not self._registered:
556
+ return
557
+ await self._send_motd()
541
558
 
542
559
  async def _handle_join(self, msg: Message) -> None:
543
560
  if not self._registered:
@@ -77,12 +77,15 @@ from agentirc._internal.protocol.replies import (
77
77
  ERR_USERSDONTMATCH,
78
78
  RPL_CHANNELMODEIS,
79
79
  RPL_CREATED,
80
+ RPL_ENDOFMOTD,
80
81
  RPL_ENDOFNAMES,
81
82
  RPL_ENDOFWHO,
82
83
  RPL_ENDOFWHOIS,
83
84
  RPL_LIST,
84
85
  RPL_LISTEND,
85
86
  RPL_LISTSTART,
87
+ RPL_MOTD,
88
+ RPL_MOTDSTART,
86
89
  RPL_MYINFO,
87
90
  RPL_NAMREPLY,
88
91
  RPL_NOTOPIC,
@@ -421,12 +424,15 @@ __all__ = [
421
424
  "ERR_USERSDONTMATCH",
422
425
  "RPL_CHANNELMODEIS",
423
426
  "RPL_CREATED",
427
+ "RPL_ENDOFMOTD",
424
428
  "RPL_ENDOFNAMES",
425
429
  "RPL_ENDOFWHO",
426
430
  "RPL_ENDOFWHOIS",
427
431
  "RPL_LIST",
428
432
  "RPL_LISTEND",
429
433
  "RPL_LISTSTART",
434
+ "RPL_MOTD",
435
+ "RPL_MOTDSTART",
430
436
  "RPL_MYINFO",
431
437
  "RPL_NAMREPLY",
432
438
  "RPL_NOTOPIC",
@@ -595,11 +595,38 @@ About 40 module-level uppercase string constants:
595
595
 
596
596
  ### Numeric reply codes
597
597
 
598
- Re-exported from `agentirc._internal.protocol.replies`. About 33 names:
598
+ Re-exported from `agentirc._internal.protocol.replies`. About 36 names:
599
599
  `ERR_*` (`ERR_ALREADYREGISTRED`, `ERR_NEEDMOREPARAMS`,
600
600
  `ERR_NICKNAMEINUSE`, `ERR_NOSUCHCHANNEL`, …) and `RPL_*`
601
601
  (`RPL_WELCOME`, `RPL_YOURHOST`, `RPL_CREATED`, `RPL_MYINFO`, …).
602
602
 
603
+ #### Greeting and MOTD (CULTURE.DEV)
604
+
605
+ agentirc is the IRCd of the CULTURE.DEV mesh, and its greeting says so. On
606
+ registration it sends, in order:
607
+
608
+ ```text
609
+ :srv 001 nick :Welcome to CULTURE.DEV on srv nick!user@host
610
+ :srv 002 nick :Your host is srv, running CULTURE.DEV
611
+ :srv 003 nick :This server was created today
612
+ :srv 004 nick srv culture o ov
613
+ :srv 375 nick :- srv Message of the day -
614
+ :srv 372 nick :- srv - CULTURE.DEV
615
+ :srv 372 nick :- The professional workspace for agents. https://culture.dev
616
+ :srv 376 nick :End of /MOTD command
617
+ ```
618
+
619
+ The `MOTD` command (registered clients only) replays 375/372/376. The text is
620
+ fixed in code (`Client._send_motd`); there is no config override yet,
621
+ and no 422 (no MOTD) reply since a MOTD is always present. `MOTD <target>`
622
+ ignores the target and replays the local MOTD. Protocol
623
+ strings use plain `CULTURE.DEV` with no trademark sign. The `004` version
624
+ token stays `culture` and the `culture.dev/traceparent` / `culture.dev/tracestate`
625
+ message tags are unchanged: these are wire identifiers, not branding. Clients
626
+ should parse `001` by its last token (the nick!user@host prefix), which is
627
+ unchanged. `RPL_MOTDSTART`, `RPL_MOTD` and `RPL_ENDOFMOTD` are exported from
628
+ `agentirc.protocol`.
629
+
603
630
  ### IRCv3 / extension tag names
604
631
 
605
632
  Re-exported from `agentirc._internal.telemetry.context`:
@@ -61,7 +61,7 @@ S: :server CAP * ACK :agentirc.io/bot message-tags
61
61
  C: CAP END
62
62
  C: NICK mybot
63
63
  C: USER mybot 0 * :My Bot
64
- S: :server 001 mybot :Welcome to agentirc, mybot
64
+ S: :server 001 mybot :Welcome to CULTURE.DEV on server mybot!mybot@127.0.0.1
65
65
  ```
66
66
 
67
67
  Most bots will request both `agentirc.io/bot` (silent presence + EVENTSUB
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentirc-cli"
3
- version = "9.12.0"
3
+ version = "9.13.1"
4
4
  description = "Agent-friendly IRCd: server core for AI agent meshes"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ # Cited from agentculture/culture-rules@48942c3 scripts/sonar-pr-issues.py (cite, don't import).
3
+ """Fail a pull request that has unresolved SonarCloud issues.
4
+
5
+ SonarCloud's quality gate can pass with open issues on new code (a code smell does not
6
+ turn it red), so CI runs this right after the scan: it waits until SonarCloud's analysis
7
+ of the PR is for the PR's head commit, then lists the PR's issues whose status is OPEN or
8
+ CONFIRMED and exits 1 when there is any. ACCEPTED and FALSE_POSITIVE issues are resolved
9
+ by a person and pass; FIXED ones are gone.
10
+
11
+ Environment: ``SONAR_HOST_URL`` (the server; defaults to localhost so nothing outside is
12
+ called by accident), ``SONAR_PROJECT_KEY`` (else ``sonar.projectKey`` from
13
+ ``sonar-project.properties``), ``PR_NUMBER``, ``HEAD_SHA`` and optionally ``SONAR_TOKEN``.
14
+ Exit codes: 0 no unresolved issue, 1 unresolved issues, 2 the analysis or the API could
15
+ not be read. Standard library only.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import json
22
+ import os
23
+ import sys
24
+ import time
25
+ import urllib.parse
26
+ import urllib.request
27
+ from collections.abc import Callable, Mapping
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ UNRESOLVED = ("OPEN", "CONFIRMED")
32
+ PAGE_SIZE = 100
33
+ WAIT_S = 300
34
+ POLL_S = 10
35
+
36
+ Fetch = Callable[[str, Mapping[str, str]], Mapping[str, Any]]
37
+
38
+
39
+ def project_key(env: Mapping[str, str], root: Path) -> str:
40
+ """``SONAR_PROJECT_KEY``, else ``sonar.projectKey`` from ``sonar-project.properties``."""
41
+ if env.get("SONAR_PROJECT_KEY"):
42
+ return env["SONAR_PROJECT_KEY"]
43
+ for line in (root / "sonar-project.properties").read_text().splitlines():
44
+ name, _, value = line.partition("=")
45
+ if name.strip() == "sonar.projectKey" and value.strip():
46
+ return value.strip()
47
+ raise SystemExit("sonar-pr-issues: no SONAR_PROJECT_KEY and no sonar.projectKey")
48
+
49
+
50
+ def http_fetch(host: str, token: str | None) -> Fetch:
51
+ """A GET of ``host + path`` with ``params``, as JSON (token as basic auth, if any)."""
52
+
53
+ def fetch(path: str, params: Mapping[str, str]) -> Mapping[str, Any]:
54
+ url = f"{host.rstrip('/')}{path}?{urllib.parse.urlencode(params)}"
55
+ req = urllib.request.Request(url) # noqa: S310 - the host is the configured server
56
+ if token:
57
+ basic = base64.b64encode(f"{token}:".encode()).decode()
58
+ req.add_header("Authorization", f"Basic {basic}")
59
+ with urllib.request.urlopen(req, timeout=30) as resp: # nosec B310
60
+ return json.load(resp)
61
+
62
+ return fetch
63
+
64
+
65
+ def analysed_commit(fetch: Fetch, project: str, pr: str) -> str | None:
66
+ """The commit SonarCloud last analysed for pull request ``pr``, or ``None``."""
67
+ data = fetch("/api/project_pull_requests/list", {"project": project})
68
+ for item in data.get("pullRequests") or ():
69
+ if str(item.get("key")) == pr:
70
+ commit = item.get("commit") or {}
71
+ sha = commit.get("sha")
72
+ return sha if isinstance(sha, str) else None
73
+ return None
74
+
75
+
76
+ def wait_for_analysis(
77
+ fetch: Fetch,
78
+ project: str,
79
+ pr: str,
80
+ head: str,
81
+ *,
82
+ wait_s: float = WAIT_S,
83
+ poll_s: float = POLL_S,
84
+ sleep: Callable[[float], None] = time.sleep,
85
+ clock: Callable[[], float] = time.monotonic,
86
+ ) -> bool:
87
+ """True once the PR's analysis is for ``head``; False when ``wait_s`` runs out."""
88
+ deadline = clock() + wait_s
89
+ while True:
90
+ if analysed_commit(fetch, project, pr) == head:
91
+ return True
92
+ if clock() >= deadline:
93
+ return False
94
+ sleep(poll_s)
95
+
96
+
97
+ def unresolved_issues(fetch: Fetch, project: str, pr: str) -> list[Mapping[str, Any]]:
98
+ """Every OPEN or CONFIRMED issue of pull request ``pr``, across pages."""
99
+ out: list[Mapping[str, Any]] = []
100
+ page = 1
101
+ while True:
102
+ data = fetch(
103
+ "/api/issues/search",
104
+ {
105
+ "componentKeys": project,
106
+ "pullRequest": pr,
107
+ "issueStatuses": ",".join(UNRESOLVED),
108
+ "ps": str(PAGE_SIZE),
109
+ "p": str(page),
110
+ },
111
+ )
112
+ issues = [i for i in data.get("issues") or () if isinstance(i, Mapping)]
113
+ out.extend(issues)
114
+ total = data.get("total")
115
+ if not issues or not isinstance(total, int) or len(out) >= total:
116
+ return out
117
+ page += 1
118
+
119
+
120
+ def describe(issue: Mapping[str, Any], project: str) -> str:
121
+ """One line: rule, file and line, severity, message."""
122
+ component = str(issue.get("component") or "").removeprefix(f"{project}:")
123
+ line = issue.get("line")
124
+ where = f"{component}:{line}" if line else component
125
+ return (
126
+ f"[{issue.get('rule')}] {where} ({issue.get('severity')}, "
127
+ f"{issue.get('issueStatus') or issue.get('status')}): {issue.get('message')}"
128
+ )
129
+
130
+
131
+ def main(
132
+ env: Mapping[str, str] | None = None,
133
+ fetch: Fetch | None = None,
134
+ root: Path | None = None,
135
+ sleep: Callable[[float], None] = time.sleep,
136
+ ) -> int:
137
+ env = os.environ if env is None else env
138
+ root = Path(__file__).resolve().parents[1] if root is None else root
139
+ pr, head = env.get("PR_NUMBER", ""), env.get("HEAD_SHA", "")
140
+ if not pr or not head:
141
+ print("sonar-pr-issues: PR_NUMBER and HEAD_SHA are required", file=sys.stderr)
142
+ return 2
143
+ project = project_key(env, root)
144
+ if fetch is None:
145
+ host = env.get("SONAR_HOST_URL") or "http://localhost:9000"
146
+ fetch = http_fetch(host, env.get("SONAR_TOKEN") or None)
147
+ try:
148
+ if not wait_for_analysis(fetch, project, pr, head, sleep=sleep):
149
+ print(
150
+ f"sonar-pr-issues: no SonarCloud analysis of PR #{pr} at {head[:12]}",
151
+ file=sys.stderr,
152
+ )
153
+ return 2
154
+ issues = unresolved_issues(fetch, project, pr)
155
+ except (OSError, ValueError) as exc: # URLError is an OSError; bad JSON a ValueError
156
+ print(f"sonar-pr-issues: SonarCloud could not be read: {exc}", file=sys.stderr)
157
+ return 2
158
+ if not issues:
159
+ print(f"SonarCloud: PR #{pr} has no unresolved issue (accepted ones pass)")
160
+ return 0
161
+ print(f"SonarCloud: PR #{pr} has {len(issues)} unresolved issue(s); fix or accept each:")
162
+ for issue in issues:
163
+ print(f" - {describe(issue, project)}")
164
+ return 1
165
+
166
+
167
+ if __name__ == "__main__":
168
+ sys.exit(main())
@@ -0,0 +1,6 @@
1
+ # SonarCloud project identity. agentirc is analysed by SonarCloud's automatic
2
+ # analysis (the GitHub App), not by a CI scan step, so this file is not read by
3
+ # a scanner. scripts/sonar-pr-issues.py reads sonar.projectKey from it to find
4
+ # the PR's analysis and its unresolved issues.
5
+ sonar.projectKey=agentculture_agentirc
6
+ sonar.organization=agentculture
@@ -66,10 +66,10 @@ async def _boot_ircd(
66
66
 
67
67
 
68
68
  async def _register(client: IRCTestClient, nick: str) -> None:
69
- """Drive NICK/USER and drain exactly the welcome burst (bounded, fast)."""
69
+ """Drive NICK/USER and drain the welcome burst through end-of-MOTD (bounded, fast)."""
70
70
  await client.send(f"NICK {nick}")
71
71
  await client.send(f"USER {nick} 0 * :{nick}")
72
- await client.recv_until("004")
72
+ await client.recv_until(" 376 ")
73
73
 
74
74
 
75
75
  async def _join(client: IRCTestClient, channel: str) -> None:
@@ -0,0 +1,69 @@
1
+ """CULTURE.DEV greeting: welcome/yourhost wording and MOTD numerics."""
2
+
3
+ import pytest
4
+
5
+
6
+ async def _register(client, nick="testserv-ori"):
7
+ await client.send(f"NICK {nick}")
8
+ await client.send("USER ori 0 * :Ori")
9
+ return await client.recv_all(timeout=1.0)
10
+
11
+
12
+ def _by_code(lines):
13
+ out = {}
14
+ for line in lines:
15
+ parts = line.split(" ", 3)
16
+ if len(parts) >= 2:
17
+ out.setdefault(parts[1], []).append(line)
18
+ return out
19
+
20
+
21
+ @pytest.mark.asyncio
22
+ async def test_welcome_and_yourhost_name_culture_dev(server, make_client):
23
+ codes = _by_code(await _register(await make_client()))
24
+ welcome = codes["001"][0]
25
+ assert "Welcome to CULTURE.DEV on testserv " in welcome
26
+ assert welcome.rstrip().endswith("testserv-ori!ori@127.0.0.1")
27
+ assert "™" not in welcome
28
+ yourhost = codes["002"][0]
29
+ assert "Your host is testserv, running CULTURE.DEV" in yourhost
30
+ assert "™" not in yourhost
31
+
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_myinfo_version_token_unchanged(server, make_client):
35
+ codes = _by_code(await _register(await make_client()))
36
+ assert codes["004"][0].split()[4] == "culture"
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_motd_sent_on_registration(server, make_client):
41
+ lines = await _register(await make_client())
42
+ codes = [line.split()[1] for line in lines]
43
+ assert "375" in codes and "376" in codes
44
+ assert codes.index("375") < codes.index("372") < codes.index("376")
45
+ assert codes.index("004") < codes.index("375")
46
+ body = "\n".join(_by_code(lines)["372"])
47
+ assert "CULTURE.DEV" in body
48
+ assert "https://culture.dev" in body
49
+ assert "™" not in "\n".join(lines)
50
+
51
+
52
+ @pytest.mark.asyncio
53
+ async def test_motd_command(server, make_client):
54
+ client = await make_client()
55
+ await _register(client)
56
+ await client.send("MOTD")
57
+ lines = await client.recv_all(timeout=1.0)
58
+ codes = [line.split()[1] for line in lines]
59
+ assert codes[0] == "375" and codes[-1] == "376"
60
+ assert "372" in codes
61
+ assert any("CULTURE.DEV" in line for line in lines)
62
+
63
+
64
+ @pytest.mark.asyncio
65
+ async def test_motd_before_registration_not_served(server, make_client):
66
+ client = await make_client()
67
+ await client.send("MOTD")
68
+ lines = await client.recv_all(timeout=0.5)
69
+ assert not any(" 375 " in line or " 376 " in line for line in lines)
@@ -84,10 +84,10 @@ async def _connect(ircd: IRCd) -> IRCTestClient:
84
84
 
85
85
 
86
86
  async def _register(client: IRCTestClient, nick: str) -> None:
87
- """Drive NICK/USER and drain exactly the welcome burst (bounded, fast)."""
87
+ """Drive NICK/USER and drain the welcome burst through end-of-MOTD (bounded, fast)."""
88
88
  await client.send(f"NICK {nick}")
89
89
  await client.send(f"USER {nick} 0 * :{nick}")
90
- await client.recv_until("004")
90
+ await client.recv_until(" 376 ")
91
91
 
92
92
 
93
93
  def _presence_line(payload: dict) -> str: