agentirc-cli 8.3.0__tar.gz → 8.5.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 (450) hide show
  1. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/CHANGELOG.md +30 -0
  2. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/PKG-INFO +1 -1
  3. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/client.py +76 -2
  4. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/config.py +9 -0
  5. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/ircd.py +50 -3
  6. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/server_link.py +75 -14
  7. agentirc_cli-8.5.0/culture/protocol/extensions/audit.md +109 -0
  8. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/tracing.md +1 -1
  9. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/telemetry/__init__.py +8 -0
  10. agentirc_cli-8.5.0/culture/telemetry/audit.py +380 -0
  11. agentirc_cli-8.5.0/culture/telemetry/metrics.py +237 -0
  12. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/telemetry/tracing.py +4 -1
  13. agentirc_cli-8.5.0/docs/agentirc/audit.md +154 -0
  14. agentirc_cli-8.5.0/docs/agentirc/telemetry.md +166 -0
  15. agentirc_cli-8.5.0/docs/superpowers/plans/2026-04-26-otel-metrics.md +303 -0
  16. agentirc_cli-8.5.0/docs/superpowers/plans/2026-04-27-otel-audit.md +247 -0
  17. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/pyproject.toml +1 -1
  18. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/conftest.py +47 -2
  19. agentirc_cli-8.5.0/tests/telemetry/_metrics_helpers.py +66 -0
  20. agentirc_cli-8.5.0/tests/telemetry/test_audit_emit.py +145 -0
  21. agentirc_cli-8.5.0/tests/telemetry/test_audit_federation.py +132 -0
  22. agentirc_cli-8.5.0/tests/telemetry/test_audit_lifecycle.py +69 -0
  23. agentirc_cli-8.5.0/tests/telemetry/test_audit_module.py +306 -0
  24. agentirc_cli-8.5.0/tests/telemetry/test_audit_parse_error.py +177 -0
  25. agentirc_cli-8.5.0/tests/telemetry/test_metrics_clients.py +79 -0
  26. agentirc_cli-8.5.0/tests/telemetry/test_metrics_events.py +81 -0
  27. agentirc_cli-8.5.0/tests/telemetry/test_metrics_init.py +58 -0
  28. agentirc_cli-8.5.0/tests/telemetry/test_metrics_irc.py +99 -0
  29. agentirc_cli-8.5.0/tests/telemetry/test_metrics_s2s.py +160 -0
  30. agentirc_cli-8.5.0/tests/telemetry/test_metrics_trace_inbound.py +127 -0
  31. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/uv.lock +1 -1
  32. agentirc_cli-8.3.0/docs/agentirc/telemetry.md +0 -101
  33. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.claude/agents/doc-test-alignment.md +0 -0
  34. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.claude/skills/pr-review/SKILL.md +0 -0
  35. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.claude/skills/run-tests/SKILL.md +0 -0
  36. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  37. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.flake8 +0 -0
  38. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.github/workflows/docs-check.yml +0 -0
  39. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.github/workflows/publish.yml +0 -0
  40. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.github/workflows/security-checks.yml +0 -0
  41. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.github/workflows/tests.yml +0 -0
  42. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.gitignore +0 -0
  43. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.markdownlint-cli2.yaml +0 -0
  44. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.pr_agent.toml +0 -0
  45. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.pre-commit-config.yaml +0 -0
  46. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.pylintrc +0 -0
  47. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/CLAUDE.md +0 -0
  48. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/Gemfile +0 -0
  49. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/Gemfile.lock +0 -0
  50. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/LICENSE +0 -0
  51. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/README.md +0 -0
  52. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/SECURITY.md +0 -0
  53. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_config.base.yml +0 -0
  54. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_config.culture.yml +0 -0
  55. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_data/sites.yml +0 -0
  56. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_includes/head_custom.html +0 -0
  57. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_sass/color_schemes/anthropic.scss +0 -0
  58. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_sass/color_schemes/dark-terminal.scss +0 -0
  59. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_sass/custom/custom.scss +0 -0
  60. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/IMG_3183.png +0 -0
  61. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/apple-touch-icon.png +0 -0
  62. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/favicon-16x16.png +0 -0
  63. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/favicon-32x32.png +0 -0
  64. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/favicon.ico +0 -0
  65. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/og-agentirc.png +0 -0
  66. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/og-culture.png +0 -0
  67. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/__init__.py +0 -0
  68. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/__main__.py +0 -0
  69. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/CLAUDE.md +0 -0
  70. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/__init__.py +0 -0
  71. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/__main__.py +0 -0
  72. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/channel.py +0 -0
  73. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/docs/agentirc-architecture.md +0 -0
  74. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/docs/agentirc-features.md +0 -0
  75. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/docs/agentirc-skill.md +0 -0
  76. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/docs/agentirc.md +0 -0
  77. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/events.py +0 -0
  78. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/history_store.py +0 -0
  79. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/remote_client.py +0 -0
  80. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/room_store.py +0 -0
  81. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/rooms_util.py +0 -0
  82. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skill.py +0 -0
  83. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/__init__.py +0 -0
  84. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/history.py +0 -0
  85. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/icon.py +0 -0
  86. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/rooms.py +0 -0
  87. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/threads.py +0 -0
  88. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/thread_store.py +0 -0
  89. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/aio.py +0 -0
  90. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/__init__.py +0 -0
  91. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/bot.py +0 -0
  92. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/bot_manager.py +0 -0
  93. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/config.py +0 -0
  94. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/filter_dsl.py +0 -0
  95. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/http_listener.py +0 -0
  96. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/system/__init__.py +0 -0
  97. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/system/welcome/__init__.py +0 -0
  98. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/system/welcome/bot.yaml +0 -0
  99. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/system/welcome/handler.py +0 -0
  100. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/template_engine.py +0 -0
  101. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/virtual_client.py +0 -0
  102. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/__init__.py +0 -0
  103. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/_passthrough.py +0 -0
  104. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/afi.py +0 -0
  105. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/agent.py +0 -0
  106. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/bot.py +0 -0
  107. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/channel.py +0 -0
  108. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/devex.py +0 -0
  109. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/introspect.py +0 -0
  110. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/mesh.py +0 -0
  111. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/server.py +0 -0
  112. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/__init__.py +0 -0
  113. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/constants.py +0 -0
  114. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/display.py +0 -0
  115. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/formatting.py +0 -0
  116. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/ipc.py +0 -0
  117. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/mesh.py +0 -0
  118. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/process.py +0 -0
  119. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/skills.py +0 -0
  120. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/__init__.py +0 -0
  121. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/__init__.py +0 -0
  122. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/agent_runner.py +0 -0
  123. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/config.py +0 -0
  124. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/culture.yaml +0 -0
  125. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/daemon.py +0 -0
  126. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/ipc.py +0 -0
  127. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/irc_transport.py +0 -0
  128. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/message_buffer.py +0 -0
  129. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/skill/SKILL.md +0 -0
  130. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/skill/__init__.py +0 -0
  131. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/skill/irc_client.py +0 -0
  132. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/socket_server.py +0 -0
  133. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/supervisor.py +0 -0
  134. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/webhook.py +0 -0
  135. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/__init__.py +0 -0
  136. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/__main__.py +0 -0
  137. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/agent_runner.py +0 -0
  138. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/config.py +0 -0
  139. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/culture.yaml +0 -0
  140. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/daemon.py +0 -0
  141. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/ipc.py +0 -0
  142. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/irc_transport.py +0 -0
  143. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/message_buffer.py +0 -0
  144. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/skill/SKILL.md +0 -0
  145. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/skill/__init__.py +0 -0
  146. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/skill/irc_client.py +0 -0
  147. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/socket_server.py +0 -0
  148. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/supervisor.py +0 -0
  149. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/webhook.py +0 -0
  150. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/__init__.py +0 -0
  151. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/agent_runner.py +0 -0
  152. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/config.py +0 -0
  153. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/culture.yaml +0 -0
  154. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/daemon.py +0 -0
  155. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/ipc.py +0 -0
  156. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/irc_transport.py +0 -0
  157. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/message_buffer.py +0 -0
  158. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/skill/SKILL.md +0 -0
  159. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/skill/__init__.py +0 -0
  160. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/skill/irc_client.py +0 -0
  161. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/socket_server.py +0 -0
  162. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/supervisor.py +0 -0
  163. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/webhook.py +0 -0
  164. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/__init__.py +0 -0
  165. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/agent_runner.py +0 -0
  166. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/config.py +0 -0
  167. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/culture.yaml +0 -0
  168. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/daemon.py +0 -0
  169. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/ipc.py +0 -0
  170. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/irc_transport.py +0 -0
  171. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/message_buffer.py +0 -0
  172. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/skill/SKILL.md +0 -0
  173. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/skill/__init__.py +0 -0
  174. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/skill/irc_client.py +0 -0
  175. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/socket_server.py +0 -0
  176. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/supervisor.py +0 -0
  177. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/webhook.py +0 -0
  178. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/config.py +0 -0
  179. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/__init__.py +0 -0
  180. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/app.py +0 -0
  181. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/client.py +0 -0
  182. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/commands.py +0 -0
  183. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/status.py +0 -0
  184. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/widgets/__init__.py +0 -0
  185. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/widgets/chat.py +0 -0
  186. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/widgets/info_panel.py +0 -0
  187. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/widgets/sidebar.py +0 -0
  188. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/constants.py +0 -0
  189. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/credentials.py +0 -0
  190. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/formatting.py +0 -0
  191. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/learn_prompt.py +0 -0
  192. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/mesh_config.py +0 -0
  193. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/observer.py +0 -0
  194. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/__init__.py +0 -0
  195. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/collector.py +0 -0
  196. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/model.py +0 -0
  197. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/renderer_text.py +0 -0
  198. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/renderer_web.py +0 -0
  199. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/web/style.css +0 -0
  200. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/persistence.py +0 -0
  201. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/pidfile.py +0 -0
  202. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/__init__.py +0 -0
  203. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/commands.py +0 -0
  204. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/events.md +0 -0
  205. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/federation.md +0 -0
  206. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/history.md +0 -0
  207. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/icons.md +0 -0
  208. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/rooms.md +0 -0
  209. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/tags.md +0 -0
  210. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/threads.md +0 -0
  211. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/message.py +0 -0
  212. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/protocol-index.md +0 -0
  213. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/replies.py +0 -0
  214. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/skills/culture/SKILL.md +0 -0
  215. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/telemetry/context.py +0 -0
  216. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/README.md +0 -0
  217. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/architecture-overview.md +0 -0
  218. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/bots.md +0 -0
  219. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/events.md +0 -0
  220. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/index.md +0 -0
  221. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/otelcol-template.yaml +0 -0
  222. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/why-agentirc.md +0 -0
  223. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/agent-lifecycle.md +0 -0
  224. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/choose-a-harness.md +0 -0
  225. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/features.md +0 -0
  226. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/index.md +0 -0
  227. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/mental-model.md +0 -0
  228. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/operate.md +0 -0
  229. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/patterns.md +0 -0
  230. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/quickstart.md +0 -0
  231. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/reflective-development.md +0 -0
  232. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/vision-patterns-index.md +0 -0
  233. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/vision.md +0 -0
  234. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/what-is-culture.md +0 -0
  235. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/agent-harness-spec.md +0 -0
  236. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/index.md +0 -0
  237. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/layers.md +0 -0
  238. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/subsites.md +0 -0
  239. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/threads.md +0 -0
  240. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/cli/afi.md +0 -0
  241. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/cli/commands.md +0 -0
  242. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/cli/devex.md +0 -0
  243. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/cli/index.md +0 -0
  244. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/console.md +0 -0
  245. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/acp.md +0 -0
  246. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/claude.md +0 -0
  247. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/codex.md +0 -0
  248. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/copilot.md +0 -0
  249. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/index.md +0 -0
  250. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/index.md +0 -0
  251. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/architecture.md +0 -0
  252. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/config.md +0 -0
  253. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/deployment.md +0 -0
  254. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/index.md +0 -0
  255. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/security.md +0 -0
  256. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
  257. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/resources/positioning.md +0 -0
  258. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/federation.md +0 -0
  259. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/harnesses.md +0 -0
  260. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/humans-and-agents.md +0 -0
  261. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/index.md +0 -0
  262. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/persistence.md +0 -0
  263. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/rooms.md +0 -0
  264. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/demos/magic-demo.md +0 -0
  265. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/first-session.md +0 -0
  266. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/index.md +0 -0
  267. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/join-as-human.md +0 -0
  268. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/local-setup.md +0 -0
  269. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/multi-machine.md +0 -0
  270. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/01-pair-programming.md +0 -0
  271. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/02-code-review-ensemble.md +0 -0
  272. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/03-cross-server-delegation.md +0 -0
  273. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/04-knowledge-propagation.md +0 -0
  274. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/05-the-observer.md +0 -0
  275. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/06-cross-server-ops.md +0 -0
  276. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/07-supervisor-intervention.md +0 -0
  277. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/08-apps-as-agents.md +0 -0
  278. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/09-research-swarm.md +0 -0
  279. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/10-agent-lifecycle.md +0 -0
  280. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases-index.md +0 -0
  281. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  282. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  283. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
  284. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
  285. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
  286. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
  287. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
  288. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
  289. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
  290. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-09-decentralized-agent-config.md +0 -0
  291. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-12-console-enhancements.md +0 -0
  292. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-15-mesh-events.md +0 -0
  293. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-18-culture-dev-positioning.md +0 -0
  294. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-22-agex-integration.md +0 -0
  295. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-24-otel-foundation.md +0 -0
  296. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-25-otel-federation.md +0 -0
  297. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  298. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  299. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
  300. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
  301. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
  302. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
  303. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
  304. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
  305. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
  306. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
  307. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
  308. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
  309. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
  310. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-07-reflective-development-reframe-design.md +0 -0
  311. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-08-reflective-development-deepening-design.md +0 -0
  312. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-09-decentralized-agent-config-design.md +0 -0
  313. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-12-console-enhancements-design.md +0 -0
  314. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-15-mesh-events-design.md +0 -0
  315. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-17-sites-repositioning-design.md +0 -0
  316. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-18-culture-dev-positioning-design.md +0 -0
  317. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-22-agex-integration-design.md +0 -0
  318. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-24-otel-observability-design.md +0 -0
  319. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/favicon.ico +0 -0
  320. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/README.md +0 -0
  321. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/config.py +0 -0
  322. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/culture.yaml +0 -0
  323. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/daemon.py +0 -0
  324. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/ipc.py +0 -0
  325. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/irc_transport.py +0 -0
  326. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/message_buffer.py +0 -0
  327. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/skill/SKILL.md +0 -0
  328. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/skill/irc_client.py +0 -0
  329. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/socket_server.py +0 -0
  330. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/webhook.py +0 -0
  331. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  332. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/plugins/claude-code/skills/culture/SKILL.md +0 -0
  333. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  334. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
  335. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/robots.txt +0 -0
  336. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/sitemap-agentirc.html +0 -0
  337. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/sitemap-main.html +0 -0
  338. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/sitemap.html +0 -0
  339. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/sonar-project.properties +0 -0
  340. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/__init__.py +0 -0
  341. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/__init__.py +0 -0
  342. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/_fakes.py +0 -0
  343. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_config.py +0 -0
  344. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_config_load.py +0 -0
  345. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_context.py +0 -0
  346. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_dispatch_span.py +0 -0
  347. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_emit_event_span.py +0 -0
  348. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_federation_propagation.py +0 -0
  349. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_outbound_inject.py +0 -0
  350. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_parse_error.py +0 -0
  351. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_privmsg_span.py +0 -0
  352. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_s2s_dispatch_span.py +0 -0
  353. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_s2s_relay_span.py +0 -0
  354. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_s2s_session_span.py +0 -0
  355. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_server_init.py +0 -0
  356. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_server_link_inject.py +0 -0
  357. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_session_span.py +0 -0
  358. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_tracing.py +0 -0
  359. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_acp_daemon.py +0 -0
  360. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_agent_runner.py +0 -0
  361. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_archive.py +0 -0
  362. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bot.py +0 -0
  363. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bot_config.py +0 -0
  364. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bot_config_fires_event_toplevel.py +0 -0
  365. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bot_manager.py +0 -0
  366. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bots_integration.py +0 -0
  367. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_channel.py +0 -0
  368. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_channel_cli.py +0 -0
  369. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_cli_afi.py +0 -0
  370. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_cli_devex.py +0 -0
  371. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_cli_introspect.py +0 -0
  372. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_cli_passthrough.py +0 -0
  373. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_codex_daemon.py +0 -0
  374. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_connection.py +0 -0
  375. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_chat_markdown.py +0 -0
  376. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_client.py +0 -0
  377. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_commands.py +0 -0
  378. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_connection.py +0 -0
  379. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_fixes_224_227.py +0 -0
  380. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_icons.py +0 -0
  381. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_integration.py +0 -0
  382. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_status.py +0 -0
  383. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_copilot_daemon.py +0 -0
  384. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_credentials.py +0 -0
  385. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_culture_config.py +0 -0
  386. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_daemon.py +0 -0
  387. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_daemon_config.py +0 -0
  388. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_daemon_ipc.py +0 -0
  389. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_discovery.py +0 -0
  390. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_display.py +0 -0
  391. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_basic.py +0 -0
  392. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_bot_chain.py +0 -0
  393. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_bot_trigger.py +0 -0
  394. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_cap_fallback.py +0 -0
  395. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_catalog.py +0 -0
  396. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_federation.py +0 -0
  397. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_history.py +0 -0
  398. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_lifecycle.py +0 -0
  399. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_reserved_nick.py +0 -0
  400. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_federation.py +0 -0
  401. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_filter_dsl.py +0 -0
  402. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_history.py +0 -0
  403. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_http_listener.py +0 -0
  404. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_integration_layer5.py +0 -0
  405. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_ipc.py +0 -0
  406. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_irc_transport.py +0 -0
  407. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_irc_transport_tags.py +0 -0
  408. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_learn_prompt.py +0 -0
  409. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_link_reconnect.py +0 -0
  410. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_manifest_config.py +0 -0
  411. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mention_alias.py +0 -0
  412. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mention_target_cleanup.py +0 -0
  413. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mention_warning.py +0 -0
  414. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mentions.py +0 -0
  415. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mesh_config.py +0 -0
  416. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mesh_readiness.py +0 -0
  417. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_message.py +0 -0
  418. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_message_buffer.py +0 -0
  419. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_message_tags.py +0 -0
  420. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_messaging.py +0 -0
  421. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_migrate_cli.py +0 -0
  422. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_modes.py +0 -0
  423. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_cli.py +0 -0
  424. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_collector.py +0 -0
  425. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_model.py +0 -0
  426. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_renderer.py +0 -0
  427. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_web.py +0 -0
  428. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_persistence.py +0 -0
  429. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_persistence_timeout.py +0 -0
  430. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_pidfile.py +0 -0
  431. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_poll_loop.py +0 -0
  432. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_register_cli.py +0 -0
  433. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_room_persistence.py +0 -0
  434. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_rooms.py +0 -0
  435. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_rooms_federation.py +0 -0
  436. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_rooms_integration.py +0 -0
  437. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_server_icon_skill.py +0 -0
  438. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_setup_update_cli.py +0 -0
  439. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_skill_client.py +0 -0
  440. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_skill_docs.py +0 -0
  441. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_skills.py +0 -0
  442. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_socket_server.py +0 -0
  443. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_supervisor.py +0 -0
  444. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_template_engine.py +0 -0
  445. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_thread_buffer.py +0 -0
  446. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_threads.py +0 -0
  447. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_virtual_client.py +0 -0
  448. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_wait_for_port.py +0 -0
  449. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_webhook.py +0 -0
  450. {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_welcome_bot.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
+ ## [8.5.0] - 2026-04-25
8
+
9
+ ### Added
10
+
11
+ - `culture/telemetry/audit.py` — `AuditSink` with bounded `asyncio.Queue` + dedicated writer task + daily/size rotation + `0600`/`0700` perms.
12
+ - Public `culture.telemetry.AuditSink`, `init_audit`, `build_audit_record`, `utc_iso_timestamp`.
13
+ - `TelemetryConfig.audit_enabled` (default `True`), `audit_dir`, `audit_max_file_bytes`, `audit_rotate_utc_midnight`, `audit_queue_depth` — independent of `telemetry.enabled` (audit fires even with OTEL off).
14
+ - `culture/protocol/extensions/audit.md` — JSONL record schema as a stable contract.
15
+ - `docs/agentirc/audit.md` — operator guide.
16
+ - Audit metrics extend the Plan-3 `MetricsRegistry`: `culture.audit.writes` (Counter, labels `outcome=ok|error`) and `culture.audit.queue_depth` (UpDownCounter).
17
+ - `IRCd.__init__` creates the sink; `IRCd.start()` awaits `sink.start()`; `IRCd.stop()` awaits `sink.shutdown()` so SERVER_WAKE / SERVER_SLEEP both land in the JSONL.
18
+ - `IRCd.emit_event` submits one record per event after the `irc.event.emit` span; `trace_id` / `span_id` captured inside the span for cross-pillar joins.
19
+ - `Client._process_buffer` submits `PARSE_ERROR` records for malformed inbound lines.
20
+ - Federation audit: federated `message` events arrive on the receiver with `origin=federated`, `peer=<peer_name>`. Federated lifecycle events (JOIN/PART/QUIT) are deferred — see #296.
21
+
22
+ ## [8.4.0] - 2026-04-25
23
+
24
+ ### Added
25
+
26
+ - `culture/telemetry/metrics.py`: `init_metrics(config)` + `MetricsRegistry` dataclass for all 15 server-side instruments — mirrors `tracing.py`'s idempotency + no-op pattern.
27
+ - Public `culture.telemetry.MetricsRegistry` and `culture.telemetry.init_metrics`.
28
+ - `TelemetryConfig.metrics_enabled` (default `True`) and `metrics_export_interval_ms` (default 10000).
29
+ - Message-flow metrics: `culture.irc.bytes_sent`, `culture.irc.bytes_received`, `culture.irc.message.size`, `culture.privmsg.delivered`.
30
+ - Events metrics: `culture.events.emitted`, `culture.events.render.duration`.
31
+ - Federation metrics: `culture.s2s.messages` (inbound), `culture.s2s.relay_latency`, `culture.s2s.links_active`, `culture.s2s.link_events`.
32
+ - Client metrics: `culture.clients.connected`, `culture.client.session.duration`, `culture.client.command.duration`.
33
+ - `culture.trace.inbound` counter — closes Plan 2's deferral.
34
+ - `tests/conftest.py` `metrics_reader` fixture parallel to `tracing_exporter`.
35
+ - `tests/telemetry/_metrics_helpers.py` — `get_counter_value`, `get_histogram_count`, `get_up_down_value`.
36
+
7
37
  ## [8.3.0] - 2026-04-25
8
38
 
9
39
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 8.3.0
3
+ Version: 8.5.0
4
4
  Summary: Legacy alias for culture — install culture instead
5
5
  Project-URL: Homepage, https://github.com/OriNachum/culture
6
6
  Author: Ori Nachum
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  import asyncio
5
5
  import logging
6
6
  import re
7
+ import time
7
8
  from typing import TYPE_CHECKING
8
9
 
9
10
  from opentelemetry import trace as _otel_trace
@@ -16,6 +17,7 @@ from culture.aio import maybe_await
16
17
  from culture.constants import SYSTEM_USER_PREFIX
17
18
  from culture.protocol import replies
18
19
  from culture.protocol.message import Message
20
+ from culture.telemetry.audit import utc_iso_timestamp as _utc_iso_timestamp
19
21
  from culture.telemetry.context import TRACEPARENT_TAG as _TP_TAG_NAME
20
22
  from culture.telemetry.context import (
21
23
  context_from_traceparent,
@@ -77,8 +79,12 @@ class Client:
77
79
  if tp is not None:
78
80
  _inject_traceparent(message, traceparent=tp, tracestate=None)
79
81
  try:
80
- self.writer.write(message.format().encode("utf-8"))
82
+ wire = message.format().encode("utf-8")
83
+ self.writer.write(wire)
81
84
  await self.writer.drain()
85
+ # Record bytes after a successful drain so we don't count
86
+ # writes that immediately faulted.
87
+ self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2c"})
82
88
  except OSError:
83
89
  pass # Client disconnected; cleanup happens in ircd._handle_connection
84
90
 
@@ -96,8 +102,10 @@ class Client:
96
102
  # block; prefix a fresh @tag.
97
103
  line = f"@{_TP_TAG_NAME}={tp} {line}"
98
104
  try:
99
- self.writer.write(f"{line}\r\n".encode("utf-8"))
105
+ wire = f"{line}\r\n".encode("utf-8")
106
+ self.writer.write(wire)
100
107
  await self.writer.drain()
108
+ self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2c"})
101
109
  except OSError:
102
110
  pass # Client disconnected; cleanup happens in ircd._handle_connection
103
111
 
@@ -141,14 +149,68 @@ class Client:
141
149
  "error": type(exc).__name__,
142
150
  },
143
151
  )
152
+ self._submit_parse_error_audit(line, exc)
144
153
  continue
154
+ # Record received bytes + message size for every successfully-parsed
155
+ # line. +2 accounts for the \r\n that was stripped during line-split.
156
+ line_bytes = len(line.encode("utf-8")) + 2
157
+ self.server.metrics.irc_bytes_received.add(line_bytes, {"direction": "c2s"})
158
+ self.server.metrics.irc_message_size.record(
159
+ line_bytes, {"verb": msg.command, "direction": "c2s"}
160
+ )
145
161
  if msg.command:
146
162
  await self._dispatch(msg)
147
163
  return buffer
148
164
 
165
+ def _submit_parse_error_audit(self, line: str, exc: BaseException) -> None:
166
+ """Build and submit a PARSE_ERROR audit record for a malformed inbound line.
167
+
168
+ The record cannot go through build_audit_record (which expects an Event);
169
+ PARSE_ERROR is a synthetic event_type with no Event object behind it.
170
+ """
171
+ # Capture trace/span ids from the active span (the
172
+ # `irc.client.process_buffer` we're inside of).
173
+ span = _otel_trace.get_current_span()
174
+ ctx = span.get_span_context()
175
+ trace_id_hex = format(ctx.trace_id, "032x") if ctx.is_valid else ""
176
+ span_id_hex = format(ctx.span_id, "016x") if ctx.is_valid else ""
177
+
178
+ peer_info = self.writer.get_extra_info("peername")
179
+ remote_addr = f"{peer_info[0]}:{peer_info[1]}" if peer_info else ""
180
+
181
+ tags: dict[str, str] = {}
182
+ tp = current_traceparent()
183
+ if tp:
184
+ tags["culture.dev/traceparent"] = tp
185
+
186
+ record = {
187
+ "ts": _utc_iso_timestamp(time.time()),
188
+ "server": self.server.config.name,
189
+ "event_type": "PARSE_ERROR",
190
+ "origin": "local",
191
+ "peer": "",
192
+ "trace_id": trace_id_hex,
193
+ "span_id": span_id_hex,
194
+ "actor": {
195
+ "nick": self.nick or "",
196
+ "kind": "human",
197
+ "remote_addr": remote_addr,
198
+ },
199
+ "target": {"kind": "", "name": ""},
200
+ "payload": {
201
+ "line_preview": line[:64],
202
+ "error": type(exc).__name__,
203
+ },
204
+ "tags": tags,
205
+ }
206
+ self.server.audit.submit(record)
207
+
149
208
  async def handle(self, initial_msg: str | None = None) -> None:
150
209
  peer_info = self.writer.get_extra_info("peername")
151
210
  remote_addr = f"{peer_info[0]}:{peer_info[1]}" if peer_info else ""
211
+ kind = "human" # Plan 5/6 will refine to bot/harness
212
+ self.server.metrics.clients_connected.add(1, {"kind": kind})
213
+ session_started = time.perf_counter()
152
214
  with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
153
215
  "irc.client.session",
154
216
  attributes={"irc.client.remote_addr": remote_addr},
@@ -172,9 +234,15 @@ class Client:
172
234
  buffer = await self._process_buffer(buffer)
173
235
  except (ConnectionError, asyncio.IncompleteReadError):
174
236
  pass
237
+ finally:
238
+ self.server.metrics.clients_connected.add(-1, {"kind": kind})
239
+ self.server.metrics.client_session_duration.record(
240
+ time.perf_counter() - session_started, {"kind": kind}
241
+ )
175
242
 
176
243
  async def _dispatch(self, msg: Message) -> None:
177
244
  extract = extract_traceparent_from_tags(msg, peer=None)
245
+ self.server.metrics.trace_inbound.add(1, {"result": extract.status, "peer": ""})
178
246
  if extract.status == "valid":
179
247
  parent_ctx: _OtelContext | None = context_from_traceparent(extract.traceparent)
180
248
  else:
@@ -190,6 +258,7 @@ class Client:
190
258
  attrs["culture.trace.dropped_reason"] = extract.status
191
259
 
192
260
  # Per-call get_tracer: test fixture swaps provider between tests.
261
+ cmd_started = time.perf_counter()
193
262
  with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
194
263
  f"irc.command.{verb}",
195
264
  context=parent_ctx,
@@ -211,6 +280,9 @@ class Client:
211
280
  await self.send_numeric(
212
281
  replies.ERR_UNKNOWNCOMMAND, msg.command, "Unknown command"
213
282
  )
283
+ self.server.metrics.client_command_duration.record(
284
+ (time.perf_counter() - cmd_started) * 1000.0, {"verb": verb}
285
+ )
214
286
 
215
287
  async def _handle_ping(self, msg: Message) -> None:
216
288
  token = msg.params[0] if msg.params else ""
@@ -724,6 +796,7 @@ class Client:
724
796
  for member in list(channel.members):
725
797
  if member is not self:
726
798
  await member.send(relay)
799
+ self.server.metrics.privmsg_delivered.add(1, {"kind": "channel", "channel": target})
727
800
  event_data = {"text": text}
728
801
  if is_notice:
729
802
  event_data["notice"] = True
@@ -758,6 +831,7 @@ class Client:
758
831
  )
759
832
  else:
760
833
  await recipient.send(relay)
834
+ self.server.metrics.privmsg_delivered.add(1, {"kind": "dm"})
761
835
  event_data = {"text": text, "target": target}
762
836
  if is_notice:
763
837
  event_data["notice"] = True
@@ -24,6 +24,15 @@ class TelemetryConfig:
24
24
  otlp_compression: str = "gzip" # gzip | none
25
25
  traces_enabled: bool = True
26
26
  traces_sampler: str = "parentbased_always_on"
27
+ metrics_enabled: bool = True
28
+ metrics_export_interval_ms: int = 10000
29
+ # Audit JSONL sink (Plan 4). Independent of `enabled` — audit fires
30
+ # even when telemetry is off so admins always have the trail.
31
+ audit_enabled: bool = True
32
+ audit_dir: str = "~/.culture/audit"
33
+ audit_max_file_bytes: int = 256 * 1024 * 1024 # 256 MiB
34
+ audit_rotate_utc_midnight: bool = True
35
+ audit_queue_depth: int = 10000
27
36
 
28
37
 
29
38
  @dataclass
@@ -5,6 +5,7 @@ import asyncio
5
5
  import base64
6
6
  import json
7
7
  import logging
8
+ import time
8
9
  from collections import deque
9
10
  from typing import TYPE_CHECKING
10
11
 
@@ -22,9 +23,14 @@ from culture.constants import (
22
23
  SYSTEM_USER_PREFIX,
23
24
  )
24
25
  from culture.protocol.message import Message
26
+ from culture.telemetry import build_audit_record, current_traceparent
25
27
 
26
28
  logger = logging.getLogger(__name__)
27
29
 
30
+ # Span/metric attribute keys defined once so a future rename has one edit point.
31
+ _ATTR_EVENT_TYPE = "event.type"
32
+
33
+
28
34
  if TYPE_CHECKING:
29
35
  from culture.agentirc.client import Client
30
36
  from culture.agentirc.remote_client import RemoteClient
@@ -35,10 +41,12 @@ class IRCd:
35
41
  """The culture IRC server."""
36
42
 
37
43
  def __init__(self, config: ServerConfig):
38
- from culture.telemetry import init_telemetry
44
+ from culture.telemetry import init_audit, init_metrics, init_telemetry
39
45
 
40
46
  self.config = config
41
47
  self.tracer = init_telemetry(config)
48
+ self.metrics = init_metrics(config)
49
+ self.audit = init_audit(config, self.metrics)
42
50
  self.clients: dict[str, Client | VirtualClient] = {} # nick -> Client
43
51
  self.channels: dict[str, Channel] = {} # name -> Channel
44
52
  self.skills: list[Skill] = []
@@ -69,6 +77,8 @@ class IRCd:
69
77
  logger.info("Bootstrapping system identity...")
70
78
  self._bootstrap_system_identity()
71
79
 
80
+ await self.audit.start()
81
+
72
82
  await self.emit_event(
73
83
  Event(
74
84
  type=EventType.SERVER_WAKE,
@@ -178,7 +188,7 @@ class IRCd:
178
188
  # server_link.py).
179
189
  event_type_str = event.type.value if hasattr(event.type, "value") else str(event.type)
180
190
  attrs: dict[str, str] = {
181
- "event.type": event_type_str,
191
+ _ATTR_EVENT_TYPE: event_type_str,
182
192
  "event.origin": "federated" if origin_tag else "local",
183
193
  }
184
194
  if event.channel:
@@ -212,20 +222,56 @@ class IRCd:
212
222
  async def emit_event(self, event: Event) -> None:
213
223
  origin_tag = event.data.get("_origin")
214
224
  attrs = self._build_event_span_attrs(event, origin_tag)
225
+ event_type_str = attrs[_ATTR_EVENT_TYPE]
226
+ origin_str = "federated" if origin_tag else "local"
227
+
228
+ self.metrics.events_emitted.add(1, {_ATTR_EVENT_TYPE: event_type_str, "origin": origin_str})
229
+ render_started = time.perf_counter()
230
+
231
+ trace_id_hex = ""
232
+ span_id_hex = ""
233
+ tp: str | None = None
234
+
215
235
  # Per-call get_tracer: the `tracing_exporter` test fixture swaps the
216
236
  # global provider between tests; a cached Tracer would bind to the
217
237
  # first test's provider and stop delivering to later ones.
218
238
  with _otel_trace.get_tracer("culture.agentirc").start_as_current_span(
219
239
  "irc.event.emit", attributes=attrs
220
- ):
240
+ ) as span:
221
241
  seq = self.next_seq()
222
242
  self._event_log.append((seq, event))
243
+ ctx = span.get_span_context()
244
+ if ctx.is_valid:
245
+ trace_id_hex = format(ctx.trace_id, "032x")
246
+ span_id_hex = format(ctx.span_id, "016x")
247
+ # Capture traceparent inside the span so trace_flags reflect
248
+ # the actual sampling decision for this span, not a hardcoded -01.
249
+ tp = current_traceparent()
223
250
  await self._run_skill_hooks(event)
224
251
  if not origin_tag:
225
252
  await self._relay_to_peers(event)
226
253
  await self._dispatch_to_bots(event)
227
254
  await self._surface_event_privmsg(event)
228
255
 
256
+ render_ms = (time.perf_counter() - render_started) * 1000.0
257
+ self.metrics.events_render_duration.record(render_ms, {_ATTR_EVENT_TYPE: event_type_str})
258
+
259
+ # Audit submit happens after the span exits so it doesn't sit inside
260
+ # the irc.event.emit span (would skew render duration). The trace_id/
261
+ # span_id captured inside the span point back at it for cross-pillar
262
+ # joins.
263
+ tags: dict[str, str] = {"culture.dev/traceparent": tp} if tp else {}
264
+ self.audit.submit(
265
+ build_audit_record(
266
+ server_name=self.config.name,
267
+ event=event,
268
+ origin_tag=origin_tag,
269
+ trace_id=trace_id_hex,
270
+ span_id=span_id_hex,
271
+ extra_tags=tags,
272
+ )
273
+ )
274
+
229
275
  _NO_SURFACE_TYPES = NO_SURFACE_EVENT_TYPES
230
276
 
231
277
  @staticmethod
@@ -381,6 +427,7 @@ class IRCd:
381
427
  if self._server:
382
428
  self._server.close()
383
429
  await self._server.wait_closed()
430
+ await self.audit.shutdown()
384
431
  finally:
385
432
  self._stopped.set()
386
433
 
@@ -5,6 +5,7 @@ import asyncio
5
5
  import base64
6
6
  import json
7
7
  import logging
8
+ import time
8
9
  from typing import TYPE_CHECKING
9
10
 
10
11
  from opentelemetry import trace as otel_trace
@@ -137,8 +138,11 @@ class ServerLink:
137
138
  except Exception: # noqa: BLE001 - telemetry must never break the link
138
139
  logger.debug("traceparent injection failed; sending untagged", exc_info=True)
139
140
  try:
140
- self.writer.write(f"{line}\r\n".encode("utf-8"))
141
+ wire = f"{line}\r\n".encode("utf-8")
142
+ self.writer.write(wire)
141
143
  await self.writer.drain()
144
+ if self.server is not None:
145
+ self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2s"})
142
146
  except OSError:
143
147
  pass
144
148
 
@@ -155,6 +159,13 @@ class ServerLink:
155
159
  line, buffer = buffer.split("\n", 1)
156
160
  if line.strip():
157
161
  msg = Message.parse(line)
162
+ if self.server is not None:
163
+ # +2 accounts for the \r\n that was stripped during line-split.
164
+ line_bytes = len(line.encode("utf-8")) + 2
165
+ self.server.metrics.irc_bytes_received.add(line_bytes, {"direction": "s2s"})
166
+ self.server.metrics.irc_message_size.record(
167
+ line_bytes, {"verb": msg.command, "direction": "s2s"}
168
+ )
158
169
  if msg.command:
159
170
  await self._dispatch(msg)
160
171
  return buffer
@@ -187,6 +198,14 @@ class ServerLink:
187
198
  except (ConnectionError, asyncio.IncompleteReadError):
188
199
  pass
189
200
  finally:
201
+ if self._authenticated:
202
+ direction = "outbound" if self.initiator else "inbound"
203
+ self.server.metrics.s2s_links_active.add(
204
+ -1, {"peer": self.peer_name or "", "direction": direction}
205
+ )
206
+ self.server.metrics.s2s_link_events.add(
207
+ 1, {"peer": self.peer_name or "", "event": "disconnect"}
208
+ )
190
209
  await self.server._remove_link(self, squit=self._squit_received)
191
210
  self.writer.close()
192
211
  try:
@@ -203,11 +222,20 @@ class ServerLink:
203
222
  handler = getattr(self, f"_handle_{msg.command.lower()}", None)
204
223
 
205
224
  extracted = extract_traceparent_from_tags(msg, peer=self.peer_name)
225
+ self.server.metrics.trace_inbound.add(
226
+ 1, {"result": extracted.status, "peer": self.peer_name or ""}
227
+ )
206
228
  if extracted.status == "valid":
207
229
  parent_ctx: _OtelContext | None = context_from_traceparent(extracted.traceparent)
208
230
  else:
209
231
  parent_ctx = _OtelContext() # force root: detach from session span
210
232
 
233
+ if self.server is not None:
234
+ self.server.metrics.s2s_messages.add(
235
+ 1,
236
+ {"verb": verb, "direction": "inbound", "peer": self.peer_name or ""},
237
+ )
238
+
211
239
  attrs = {
212
240
  "irc.command": verb,
213
241
  "culture.trace.origin": "remote",
@@ -266,17 +294,26 @@ class ServerLink:
266
294
  """
267
295
  if self._peer_pass != self.password:
268
296
  logger.warning("Bad password from peer %s", self.peer_name)
297
+ self.server.metrics.s2s_link_events.add(
298
+ 1, {"peer": self.peer_name or "", "event": "auth_fail"}
299
+ )
269
300
  await self.send_raw("ERROR :Bad password")
270
301
  raise ConnectionError("Bad S2S password")
271
302
 
272
303
  # Check for duplicate server name
273
304
  if self.peer_name in self.server.links:
274
305
  logger.warning("Duplicate server name %s", self.peer_name)
306
+ self.server.metrics.s2s_link_events.add(
307
+ 1, {"peer": self.peer_name or "", "event": "auth_fail"}
308
+ )
275
309
  await self.send_raw(f"ERROR :Server name {self.peer_name} already linked")
276
310
  raise ConnectionError("Duplicate server name")
277
311
 
278
312
  if self.peer_name == self.server.config.name:
279
313
  logger.warning("Peer has same name as us: %s", self.peer_name)
314
+ self.server.metrics.s2s_link_events.add(
315
+ 1, {"peer": self.peer_name or "", "event": "auth_fail"}
316
+ )
280
317
  await self.send_raw("ERROR :Cannot link to self")
281
318
  raise ConnectionError("Cannot link to self")
282
319
 
@@ -291,6 +328,11 @@ class ServerLink:
291
328
  await self._validate_peer_credentials()
292
329
 
293
330
  self._authenticated = True
331
+ direction = "outbound" if self.initiator else "inbound"
332
+ self.server.metrics.s2s_links_active.add(
333
+ 1, {"peer": self.peer_name, "direction": direction}
334
+ )
335
+ self.server.metrics.s2s_link_events.add(1, {"peer": self.peer_name, "event": "connect"})
294
336
  if self._session_span is not None:
295
337
  self._session_span.set_attribute("s2s.peer", self.peer_name)
296
338
  self.server.links[self.peer_name] = self
@@ -900,6 +942,10 @@ class ServerLink:
900
942
  except ValueError:
901
943
  return
902
944
 
945
+ self.server.metrics.s2s_link_events.add(
946
+ 1, {"peer": self.peer_name or "", "event": "backfill_start"}
947
+ )
948
+
903
949
  # Use the higher of: what peer claims, or what we know they acked
904
950
  # (during real-time relay, peer saw everything up to our _seq at link drop)
905
951
  acked = self.server._peer_acked_seq.get(self.peer_name, 0)
@@ -910,6 +956,9 @@ class ServerLink:
910
956
  await self._replay_event(seq, event)
911
957
 
912
958
  await self.send_raw(f":{self.server.config.name} BACKFILLEND {self.server._seq}")
959
+ self.server.metrics.s2s_link_events.add(
960
+ 1, {"peer": self.peer_name or "", "event": "backfill_complete"}
961
+ )
913
962
 
914
963
  def _handle_backfillend(self, msg: Message) -> None:
915
964
  """Peer finished backfilling."""
@@ -946,6 +995,8 @@ class ServerLink:
946
995
  "event.type": event_type_str,
947
996
  "s2s.peer": self.peer_name or "",
948
997
  }
998
+ relay_started = time.perf_counter()
999
+ relayed = False
949
1000
  # Single span name (no verb suffix): the wire verb is decided downstream
950
1001
  # by _RELAY_DISPATCH or the SEVENT fallback, after this span opens.
951
1002
  with otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
@@ -955,19 +1006,29 @@ class ServerLink:
955
1006
  handler = self._RELAY_DISPATCH.get(event.type)
956
1007
  if handler:
957
1008
  await maybe_await(handler(self, event, origin))
958
- return
959
-
960
- # If no typed relay exists, fall back to generic SEVENT.
961
- # v1 assumes all peers support SEVENT; cap negotiation is deferred — see plan task 12.
962
- payload = self.server._build_event_payload(event)
963
- encoded = self.server._encode_event_data(payload, event_type_str)
964
- target = event.channel or "*"
965
- # Egress trust check: channel-scoped events respect should_relay; global events always relay
966
- if event.channel is not None and not self.should_relay(event.channel):
967
- return
968
- seq = self.server._seq # current local seq; peer stores but doesn't re-sequence
969
- await self.send_raw(
970
- f":{origin} SEVENT {origin} {seq} {event_type_str} {target} :{encoded}"
1009
+ # v1: typed _relay_* handlers may early-return on internal trust checks
1010
+ # without signaling. We optimistically count typed dispatch as relayed.
1011
+ # Future: refactor _relay_* to return bool so latency only fires on
1012
+ # genuine sends.
1013
+ relayed = True
1014
+ else:
1015
+ # If no typed relay exists, fall back to generic SEVENT.
1016
+ # v1 assumes all peers support SEVENT; cap negotiation is deferred — see plan task 12.
1017
+ payload = self.server._build_event_payload(event)
1018
+ encoded = self.server._encode_event_data(payload, event_type_str)
1019
+ target = event.channel or "*"
1020
+ # Egress trust check: channel-scoped events respect should_relay; global events always relay
1021
+ if event.channel is None or self.should_relay(event.channel):
1022
+ seq = self.server._seq # current local seq; peer stores but doesn't re-sequence
1023
+ await self.send_raw(
1024
+ f":{origin} SEVENT {origin} {seq} {event_type_str} {target} :{encoded}"
1025
+ )
1026
+ relayed = True
1027
+
1028
+ if relayed:
1029
+ self.server.metrics.s2s_relay_latency.record(
1030
+ (time.perf_counter() - relay_started) * 1000.0,
1031
+ {"event.type": event_type_str, "peer": self.peer_name or ""},
971
1032
  )
972
1033
 
973
1034
  async def _relay_message(self, event: Event, origin: str) -> None:
@@ -0,0 +1,109 @@
1
+ # Extension: Audit JSONL Sink
2
+
3
+ The audit log is a durable, file-based JSON-Lines (`.jsonl`) trail of every event the server
4
+ emits. It is **separate from OTEL traces / metrics / logs** — audit lands directly on local disk
5
+ and never depends on a running collector. Admin-only "who said what to whom, when, via what path."
6
+
7
+ ## File Layout
8
+
9
+ - **Path:** `<audit_dir>/<server_name>-<YYYY-MM-DD>.jsonl` where `<audit_dir>` defaults to
10
+ `~/.culture/audit/` (configurable via `telemetry.audit_dir`). The date is **UTC**.
11
+ - **File mode:** `0600` (owner read/write only).
12
+ - **Directory mode:** `0700` (owner only). Created on demand if missing; existing dir mode is
13
+ left as-is.
14
+ - **Rotation suffix:** when the daily file hits the size cap, the next file gets `.1`, then
15
+ `.2`, … same date. New day starts a fresh file with no suffix.
16
+
17
+ Example for server `spark` on 2026-04-27 with two size-cap rotations:
18
+
19
+ ```text
20
+ ~/.culture/audit/spark-2026-04-27.jsonl # first 256 MiB
21
+ ~/.culture/audit/spark-2026-04-27.1.jsonl # next 256 MiB
22
+ ~/.culture/audit/spark-2026-04-27.2.jsonl # current
23
+ ```
24
+
25
+ ## Record Schema
26
+
27
+ Each line in the file is a single JSON object. Lines never wrap. Keys are lowercase with `_`
28
+ separators. Order is canonicalized at write time (stable across writes). Future schema additions
29
+ are additive only — consumers must tolerate unknown keys.
30
+
31
+ Keys within each record are alphabetically sorted (the writer uses
32
+ `json.dumps(..., sort_keys=True)`). Consumers SHOULD NOT rely on
33
+ insertion order; future producers may emit keys in any order.
34
+
35
+ | Field | Type | Required | Description |
36
+ |-------|------|----------|-------------|
37
+ | `ts` | string | yes | ISO 8601 UTC timestamp with microsecond precision and trailing `Z` (e.g. `2026-04-27T14:32:05.123456Z`). |
38
+ | `server` | string | yes | Server name from `ServerConfig.name`. |
39
+ | `event_type` | string | yes | `EventType.value` (e.g. `message`, `user.join`, `room.create`) or the special string `PARSE_ERROR` for malformed inbound lines. |
40
+ | `origin` | string | yes | `local` if the event originated on this server; `federated` if it arrived via a peer link. |
41
+ | `peer` | string | yes | Peer server name when `origin=federated`; empty string `""` otherwise. |
42
+ | `trace_id` | string | yes | OTEL trace-id (32 hex chars) of the active span at submit time, or `""` if no span. |
43
+ | `span_id` | string | yes | OTEL span-id (16 hex chars) of the active span, or `""`. |
44
+ | `actor` | object | yes | `{nick, kind, remote_addr}` describing who/what produced the event. |
45
+ | `actor.nick` | string | yes | The nick from the event (`event.nick`), or `""`. |
46
+ | `actor.kind` | string | yes | One of `human`, `bot`, `harness`. v1 always emits `human` — Plans 5/6 refine. |
47
+ | `actor.remote_addr` | string | yes | `"<ip>:<port>"` if known (PARSE_ERROR via Client; empty for server-internal sites). |
48
+ | `target` | object | yes | `{kind, name}` describing what the event affected. |
49
+ | `target.kind` | string | yes | `channel` (event.channel set), `nick` (DM target), or `""` for global events. |
50
+ | `target.name` | string | yes | The channel or nick; `""` for global. |
51
+ | `payload` | object | yes | `event.data` with all underscore-prefix keys (`_origin`, etc.) stripped. May include `nick` / `channel` defaulted from `event.nick`/`event.channel`. |
52
+ | `tags` | object | yes | IRCv3-style tag bag. v1 emits at most `culture.dev/traceparent` derived from the active span; empty `{}` if no span. |
53
+
54
+ ## Rotation
55
+
56
+ Rotation fires when **either** condition is met, checked at the top of every record write:
57
+
58
+ 1. The current UTC date differs from `current_date` (daily roll, controlled by
59
+ `telemetry.audit_rotate_utc_midnight`).
60
+ 2. The current file size + the about-to-be-written record size exceeds
61
+ `telemetry.audit_max_file_bytes` (default 256 MiB).
62
+
63
+ The new file is opened with `O_WRONLY | O_APPEND | O_CREAT` mode `0600`. Writes use a single
64
+ `os.write` per record so partial-line interleaving is impossible.
65
+
66
+ A record larger than `audit_max_file_bytes` is still written — the cap is a soft
67
+ ceiling for accumulated bytes, not a hard reject. The oversized record lands in
68
+ its own freshly-rotated file, and the next record triggers another rotation.
69
+
70
+ ## Durability
71
+
72
+ Records flow through a bounded `asyncio.Queue` (depth `telemetry.audit_queue_depth`, default
73
+ 10000). A dedicated writer task drains the queue and writes each record. On queue overflow, the
74
+ record is **dropped** and `culture.audit.writes{outcome=error}` increments. A stderr warning is
75
+ logged.
76
+
77
+ This is a deliberate trade-off: dropping records is preferable to blocking `IRCd.emit_event`. A
78
+ real-world audit gap is rare and recoverable; a blocked event loop is catastrophic.
79
+
80
+ No `fsync` per record — writes hit the page cache and rely on the OS to flush. A hard crash can
81
+ lose the in-flight record.
82
+
83
+ ## Retention
84
+
85
+ Files are not auto-pruned in v1. Operators prune manually:
86
+
87
+ ```bash
88
+ find ~/.culture/audit -name 'spark-*.jsonl*' -mtime +30 -delete
89
+ ```
90
+
91
+ A future `audit-prune` CLI is TODO.
92
+
93
+ ## Compat
94
+
95
+ The schema is a stable contract:
96
+
97
+ - New fields can be added in future versions; old consumers must tolerate unknown keys.
98
+ - Existing keys keep their type and semantics across versions.
99
+ - If a future version needs a breaking change, a top-level `schema_version` integer will be
100
+ added and bumped — until that exists, treat the schema as version 1.
101
+
102
+ ## Example
103
+
104
+ PRIVMSG from `alpha-alice` (a federated client on the `alpha` peer) to channel `#general` on
105
+ the local server `spark`:
106
+
107
+ ```json
108
+ {"ts":"2026-04-27T14:32:05.123456Z","server":"spark","event_type":"message","origin":"federated","peer":"alpha","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","actor":{"nick":"alpha-alice","kind":"human","remote_addr":""},"target":{"kind":"channel","name":"#general"},"payload":{"text":"hi","nick":"alpha-alice","channel":"#general"},"tags":{"culture.dev/traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}}
109
+ ```
@@ -11,7 +11,7 @@
11
11
 
12
12
  1. Tag absent → start a new root span. Attribute: `culture.trace.origin=local`.
13
13
  2. Tag present and valid → start a child span linked to the extracted context. Attributes: `culture.trace.origin=remote`, `culture.federation.peer=<peer>`.
14
- 3. Tag present but malformed or over the length cap → drop the tag, start a new root span. Attributes: `culture.trace.origin=remote`, `culture.trace.dropped_reason=malformed|too_long`, `culture.federation.peer=<peer>`. Log a rate-limited warning. Increment `culture.trace.inbound{result=malformed|too_long, peer=<peer>}`.
14
+ 3. Tag present but malformed or over the length cap → drop the tag, start a new root span. Attributes: `culture.trace.origin=remote`, `culture.trace.dropped_reason=malformed|too_long`, `culture.federation.peer=<peer>`. Log a rate-limited warning. Increment `culture.trace.inbound{result=malformed|too_long, peer=<peer>}` (shipped in culture 8.4.0; on every inbound regardless of result, so `result=valid` and `result=missing` are also recorded).
15
15
 
16
16
  **Length caps** (hard-coded, not configurable):
17
17