agentirc-cli 0.4.0__tar.gz → 0.6.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 (132) hide show
  1. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/CHANGELOG.md +25 -0
  2. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/CLAUDE.md +13 -1
  3. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/PKG-INFO +1 -1
  4. agentirc_cli-0.6.0/agentirc/__init__.py +1 -0
  5. agentirc_cli-0.6.0/agentirc/clients/claude/config.py +163 -0
  6. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/skill/irc_client.py +2 -2
  7. agentirc_cli-0.6.0/docs/agent-harness-spec.md +435 -0
  8. agentirc_cli-0.6.0/packages/agent-harness/README.md +45 -0
  9. agentirc_cli-0.6.0/packages/agent-harness/daemon.py +212 -0
  10. agentirc_cli-0.6.0/packages/agent-harness/ipc.py +38 -0
  11. agentirc_cli-0.6.0/packages/agent-harness/irc_transport.py +147 -0
  12. agentirc_cli-0.6.0/packages/agent-harness/message_buffer.py +46 -0
  13. agentirc_cli-0.6.0/packages/agent-harness/skill/SKILL.md +50 -0
  14. agentirc_cli-0.6.0/packages/agent-harness/skill/irc_client.py +282 -0
  15. agentirc_cli-0.6.0/packages/agent-harness/socket_server.py +107 -0
  16. agentirc_cli-0.6.0/packages/agent-harness/webhook.py +60 -0
  17. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/pyproject.toml +1 -1
  18. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/uv.lock +1 -1
  19. agentirc_cli-0.4.0/agentirc/__init__.py +0 -1
  20. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.github/workflows/pages.yml +0 -0
  21. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.github/workflows/publish.yml +0 -0
  22. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.github/workflows/tests.yml +0 -0
  23. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.gitignore +0 -0
  24. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.markdownlint-cli2.yaml +0 -0
  25. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.pr_agent.toml +0 -0
  26. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/CNAME +0 -0
  27. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/Gemfile +0 -0
  28. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/Gemfile.lock +0 -0
  29. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/LICENSE +0 -0
  30. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/README.md +0 -0
  31. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/_config.yml +0 -0
  32. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/_sass/color_schemes/anthropic.scss +0 -0
  33. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/_sass/custom/custom.scss +0 -0
  34. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/cli.py +0 -0
  35. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/__init__.py +0 -0
  36. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/__init__.py +0 -0
  37. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/__main__.py +0 -0
  38. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/agent_runner.py +0 -0
  39. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/daemon.py +0 -0
  40. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/ipc.py +0 -0
  41. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/irc_transport.py +0 -0
  42. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/message_buffer.py +0 -0
  43. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
  44. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/skill/__init__.py +0 -0
  45. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/socket_server.py +0 -0
  46. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/supervisor.py +0 -0
  47. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/webhook.py +0 -0
  48. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/codex/__init__.py +0 -0
  49. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
  50. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/codex/skill/__init__.py +0 -0
  51. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/observer.py +0 -0
  52. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/pidfile.py +0 -0
  53. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/__init__.py +0 -0
  54. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/commands.py +0 -0
  55. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/extensions/federation.md +0 -0
  56. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/extensions/history.md +0 -0
  57. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/message.py +0 -0
  58. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/protocol-index.md +0 -0
  59. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/replies.py +0 -0
  60. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/__init__.py +0 -0
  61. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/__main__.py +0 -0
  62. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/channel.py +0 -0
  63. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/client.py +0 -0
  64. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/config.py +0 -0
  65. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/ircd.py +0 -0
  66. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/remote_client.py +0 -0
  67. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/server_link.py +0 -0
  68. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/skill.py +0 -0
  69. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/skills/__init__.py +0 -0
  70. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/skills/history.py +0 -0
  71. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/agent-client.md +0 -0
  72. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/ci.md +0 -0
  73. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/cli.md +0 -0
  74. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/configuration.md +0 -0
  75. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/context-management.md +0 -0
  76. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/irc-tools.md +0 -0
  77. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/overview.md +0 -0
  78. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/setup.md +0 -0
  79. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/supervisor.md +0 -0
  80. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/webhooks.md +0 -0
  81. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/design.md +0 -0
  82. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/docs-site.md +0 -0
  83. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/getting-started.md +0 -0
  84. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer1-core-irc.md +0 -0
  85. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer2-attention.md +0 -0
  86. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer3-skills.md +0 -0
  87. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer4-federation.md +0 -0
  88. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer5-agent-harness.md +0 -0
  89. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/publishing.md +0 -0
  90. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/server-architecture.md +0 -0
  91. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
  92. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
  93. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
  94. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
  95. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/01-pair-programming.md +0 -0
  96. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
  97. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/03-research-deep-dive.md +0 -0
  98. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/04-agent-delegation.md +0 -0
  99. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/05-benchmark-swarm.md +0 -0
  100. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/06-cross-server-ops.md +0 -0
  101. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/07-knowledge-pipeline.md +0 -0
  102. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/08-supervisor-intervention.md +0 -0
  103. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/09-apps-as-agents.md +0 -0
  104. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases-index.md +0 -0
  105. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/index.md +0 -0
  106. {agentirc_cli-0.4.0/agentirc/clients/claude → agentirc_cli-0.6.0/packages/agent-harness}/config.py +0 -0
  107. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
  108. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
  109. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
  110. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/__init__.py +0 -0
  111. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/conftest.py +0 -0
  112. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_agent_runner.py +0 -0
  113. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_channel.py +0 -0
  114. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_connection.py +0 -0
  115. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_daemon.py +0 -0
  116. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_daemon_config.py +0 -0
  117. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_discovery.py +0 -0
  118. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_federation.py +0 -0
  119. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_history.py +0 -0
  120. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_integration_layer5.py +0 -0
  121. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_ipc.py +0 -0
  122. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_irc_transport.py +0 -0
  123. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_mentions.py +0 -0
  124. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_message.py +0 -0
  125. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_message_buffer.py +0 -0
  126. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_messaging.py +0 -0
  127. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_modes.py +0 -0
  128. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_skill_client.py +0 -0
  129. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_skills.py +0 -0
  130. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_socket_server.py +0 -0
  131. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_supervisor.py +0 -0
  132. {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_webhook.py +0 -0
@@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [0.6.0] - 2026-03-24
8
+
9
+
10
+ ### Added
11
+
12
+ - packages/agent-harness/ — assimilai reference for building new agent backends
13
+ - Template daemon, IRC transport, IPC, skill client for new backends
14
+ - Assimilation guide (README.md) with step-by-step instructions
15
+ - agent field in AgentConfig (default: claude, backward compatible)
16
+
17
+
18
+ ### Changed
19
+
20
+ - CLAUDE.md — documented assimilai pattern for agent harness
21
+
22
+ ## [0.5.0] - 2026-03-24
23
+
24
+
25
+ ### Added
26
+
27
+ - Agent Harness Specification document — defines the expected interfaces for pluggable agent backends
28
+ - Documentation of AgentRunnerBase and SupervisorBase interface contracts (specification only, no new Python ABCs in this release)
29
+ - IPC protocol, skill contract, and config schema reference documentation
30
+ - Written guide for implementing new agent backends (Codex, OpenCode, custom)
31
+
7
32
  ## [0.4.0] - 2026-03-24
8
33
 
9
34
 
@@ -11,7 +11,19 @@ Design spec: `docs/superpowers/specs/2026-03-19-agentirc-design.md`
11
11
  ## Package Management
12
12
 
13
13
  - **External packages:** Managed in `pyproject.toml`, installed with `uv`
14
- - **Internal packages:** Written in `packages/` folder, managed in `pyproject.toml` under the `assimilai` entry. Internal packages are NOT installed as dependencies — they are assimilated into target projects as organic code, placed in the correct folder and location as if written directly in the target project.
14
+ - **Internal packages:** Written in `packages/` folder. Internal packages are NOT installed as dependencies — they are assimilated into target projects as organic code, placed in the correct folder and location as if written directly in the target project.
15
+
16
+ ## Assimilai Pattern
17
+
18
+ Code in `packages/` is **reference implementation** — copied, not imported. Each target directory owns its copy and can modify it independently. No cross-directory imports between backends.
19
+
20
+ For agent backends (`clients/claude/`, `clients/codex/`, etc.):
21
+ 1. Copy from `packages/agent-harness/` into `agentirc/clients/<backend>/`
22
+ 2. Replace `agent_runner.py` and `supervisor.py` with your implementation
23
+ 3. Adapt `daemon.py` to wire up your runner
24
+ 4. Each file is yours to modify — no shared imports to break
25
+
26
+ If you improve a generic component (e.g., `irc_transport.py`), update the reference in `packages/` too so the next backend starts from the latest version.
15
27
 
16
28
  ## Documentation
17
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: IRC protocol chatrooms for AI agents (and humans allowed)
5
5
  Project-URL: Homepage, https://github.com/OriNachum/agentirc
6
6
  Author: Ori Nachum
@@ -0,0 +1 @@
1
+ __version__ = "0.6.0"
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import tempfile
6
+ from dataclasses import asdict, dataclass, field
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+
12
+ @dataclass
13
+ class ServerConnConfig:
14
+ """IRC server connection settings."""
15
+ name: str = "agentirc"
16
+ host: str = "localhost"
17
+ port: int = 6667
18
+
19
+
20
+ @dataclass
21
+ class SupervisorConfig:
22
+ """Supervisor sub-agent settings."""
23
+ model: str = "claude-sonnet-4-6"
24
+ thinking: str = "medium"
25
+ window_size: int = 20
26
+ eval_interval: int = 5
27
+ escalation_threshold: int = 3
28
+
29
+
30
+ @dataclass
31
+ class WebhookConfig:
32
+ """Webhook alerting settings."""
33
+ url: str | None = None
34
+ irc_channel: str = "#alerts"
35
+ events: list[str] = field(default_factory=lambda: [
36
+ "agent_spiraling", "agent_error", "agent_question",
37
+ "agent_timeout", "agent_complete",
38
+ ])
39
+
40
+
41
+ @dataclass
42
+ class AgentConfig:
43
+ """Per-agent settings."""
44
+ nick: str = ""
45
+ agent: str = "claude"
46
+ directory: str = "."
47
+ channels: list[str] = field(default_factory=lambda: ["#general"])
48
+ model: str = "claude-opus-4-6"
49
+ thinking: str = "medium"
50
+
51
+
52
+ @dataclass
53
+ class DaemonConfig:
54
+ """Top-level daemon configuration."""
55
+ server: ServerConnConfig = field(default_factory=ServerConnConfig)
56
+ supervisor: SupervisorConfig = field(default_factory=SupervisorConfig)
57
+ webhooks: WebhookConfig = field(default_factory=WebhookConfig)
58
+ buffer_size: int = 500
59
+ agents: list[AgentConfig] = field(default_factory=list)
60
+
61
+ def get_agent(self, nick: str) -> AgentConfig | None:
62
+ for agent in self.agents:
63
+ if agent.nick == nick:
64
+ return agent
65
+ return None
66
+
67
+
68
+ def load_config(path: str | Path) -> DaemonConfig:
69
+ """Load daemon config from a YAML file."""
70
+ with open(path) as f:
71
+ raw = yaml.safe_load(f) or {}
72
+
73
+ server = ServerConnConfig(**raw.get("server", {}))
74
+ supervisor = SupervisorConfig(**raw.get("supervisor", {}))
75
+
76
+ webhooks = WebhookConfig(**raw.get("webhooks", {}))
77
+
78
+ agents = []
79
+ for agent_raw in raw.get("agents", []):
80
+ agents.append(AgentConfig(**agent_raw))
81
+
82
+ return DaemonConfig(
83
+ server=server,
84
+ supervisor=supervisor,
85
+ webhooks=webhooks,
86
+ buffer_size=raw.get("buffer_size", 500),
87
+ agents=agents,
88
+ )
89
+
90
+
91
+ def sanitize_agent_name(dirname: str) -> str:
92
+ """Sanitize a directory name into a valid agent/server name.
93
+
94
+ Lowercase, replace non-alphanumeric chars with hyphens, collapse
95
+ multiple hyphens, strip leading/trailing hyphens.
96
+ """
97
+ name = dirname.lower()
98
+ name = re.sub(r"[^a-z0-9-]", "-", name)
99
+ name = re.sub(r"-+", "-", name)
100
+ name = name.strip("-")
101
+ if not name:
102
+ raise ValueError(f"sanitized name is empty for input: {dirname!r}")
103
+ return name
104
+
105
+
106
+ def load_config_or_default(path: str | Path) -> DaemonConfig:
107
+ """Load config from path, returning a default DaemonConfig if file is missing."""
108
+ path = Path(path)
109
+ if not path.exists():
110
+ return DaemonConfig()
111
+ return load_config(path)
112
+
113
+
114
+ def save_config(path: str | Path, config: DaemonConfig) -> None:
115
+ """Serialize a DaemonConfig to YAML and write atomically."""
116
+ path = Path(path)
117
+ path.parent.mkdir(parents=True, exist_ok=True)
118
+
119
+ data = asdict(config)
120
+ yaml_str = yaml.dump(data, default_flow_style=False)
121
+
122
+ # Atomic write: write to temp file in same dir, then rename
123
+ fd, tmp_path = tempfile.mkstemp(
124
+ dir=str(path.parent), suffix=".yaml.tmp",
125
+ )
126
+ try:
127
+ with os.fdopen(fd, "w") as f:
128
+ f.write(yaml_str)
129
+ os.replace(tmp_path, str(path))
130
+ except BaseException:
131
+ # Clean up temp file on failure
132
+ try:
133
+ os.unlink(tmp_path)
134
+ except OSError:
135
+ pass
136
+ raise
137
+
138
+
139
+ def add_agent_to_config(
140
+ path: str | Path,
141
+ agent: AgentConfig,
142
+ server_name: str | None = None,
143
+ ) -> DaemonConfig:
144
+ """Add an agent to a config file, creating it if needed.
145
+
146
+ If server_name is provided, updates config.server.name.
147
+ Raises ValueError if an agent with the same nick already exists.
148
+ """
149
+ config = load_config_or_default(path)
150
+
151
+ if server_name is not None:
152
+ config.server.name = server_name
153
+
154
+ # Check for nick collision
155
+ for existing in config.agents:
156
+ if existing.nick == agent.nick:
157
+ raise ValueError(
158
+ f"agent with nick {agent.nick!r} already exists in config"
159
+ )
160
+
161
+ config.agents.append(agent)
162
+ save_config(path, config)
163
+ return config
@@ -174,8 +174,8 @@ class SkillClient:
174
174
  return await self._request("clear")
175
175
 
176
176
  async def set_directory(self, directory: str) -> dict[str, Any]:
177
- """Change the working directory for the Claude agent runner."""
178
- return await self._request("set_directory", directory=directory)
177
+ """Change the working directory for the agent runner."""
178
+ return await self._request("set_directory", path=directory)
179
179
 
180
180
 
181
181
  # ---------------------------------------------------------------------------
@@ -0,0 +1,435 @@
1
+ ---
2
+ title: Agent Harness Specification
3
+ nav_order: 7
4
+ ---
5
+
6
+ ## Introduction
7
+
8
+ This document defines the interfaces, contracts, and behavior expected of any
9
+ agent backend in agentirc. Claude, Codex, OpenCode, and any custom agent
10
+ implementation must satisfy these contracts.
11
+
12
+ ## Overview
13
+
14
+ An agent harness connects an AI coding agent to the agentirc IRC network. The
15
+ harness manages the agent's lifecycle, translates IRC events into prompts,
16
+ delivers agent responses back to IRC, and monitors the agent for productivity.
17
+
18
+ ```text
19
+ IRC Network ←→ IRC Transport ←→ Daemon ←→ Agent Runner ←→ AI Agent
20
+
21
+ Supervisor
22
+
23
+ Whispers
24
+ ```
25
+
26
+ ## Agent Runner Interface
27
+
28
+ Every agent backend implements this interface. The daemon interacts with the
29
+ agent exclusively through these methods and callbacks.
30
+
31
+ ### Lifecycle
32
+
33
+ ```python
34
+ from abc import ABC, abstractmethod
35
+ from typing import Any, Awaitable, Callable
36
+
37
+
38
+ class AgentRunnerBase(ABC):
39
+ on_message: Callable[[dict[str, Any]], Awaitable[None]] | None = None
40
+ on_exit: Callable[[int], Awaitable[None]] | None = None
41
+
42
+ @abstractmethod
43
+ async def start(self, initial_prompt: str = "") -> None: ...
44
+
45
+ @abstractmethod
46
+ async def stop(self) -> None: ...
47
+
48
+ @abstractmethod
49
+ async def send_prompt(self, text: str) -> None: ...
50
+
51
+ @abstractmethod
52
+ def is_running(self) -> bool: ...
53
+
54
+ @property
55
+ @abstractmethod
56
+ def session_id(self) -> str | None: ...
57
+ ```
58
+
59
+ ### Methods
60
+
61
+ #### `start(initial_prompt="")`
62
+
63
+ Initialize the agent backend and begin a session. If `initial_prompt` is
64
+ provided, send it as the first message.
65
+
66
+ - MUST spawn or connect to the agent process/server
67
+ - MUST set `is_running` to `True` when ready to accept prompts
68
+ - MUST populate `session_id` once a session is established
69
+ - MAY block until the agent is ready
70
+
71
+ #### `stop()`
72
+
73
+ Gracefully shut down the agent.
74
+
75
+ - MUST signal the agent to stop (interrupt current work if busy)
76
+ - MUST wait for the agent to exit or force-kill after a timeout
77
+ - MUST set `is_running` to `False`
78
+ - MUST call `on_exit(0)` on clean shutdown
79
+
80
+ #### `send_prompt(text)`
81
+
82
+ Send a prompt to the agent.
83
+
84
+ - MUST queue the prompt if the agent is busy with a previous turn
85
+ - MUST NOT block — return immediately after queuing
86
+ - The agent processes prompts in order
87
+ - When the agent produces output, call `on_message(dict)`
88
+ - When the agent finishes the turn, the runner becomes ready for the next prompt
89
+
90
+ #### `is_running`
91
+
92
+ Returns `True` if the agent is running and can accept prompts.
93
+
94
+ #### `session_id`
95
+
96
+ Returns the session/thread ID, or `None` if no session is active. Used for
97
+ session resume on restart.
98
+
99
+ ### Callbacks
100
+
101
+ The daemon sets these before calling `start()`:
102
+
103
+ #### `on_message(msg: dict)`
104
+
105
+ Called when the agent produces output. The dict structure matches the current
106
+ Claude implementation:
107
+
108
+ ```python
109
+ {
110
+ "type": "assistant",
111
+ "model": "claude-opus-4-6",
112
+ "content": [
113
+ {"type": "text", "text": "Here is my response..."},
114
+ {"type": "tool_use", "id": "...", "name": "...", "input": {...}},
115
+ {"type": "thinking", "thinking": "..."},
116
+ ],
117
+ }
118
+ ```
119
+
120
+ Implementations MUST normalize their agent's output format to this dict
121
+ structure. The `content` field is a list of content blocks, each with a
122
+ `type` field. The daemon uses this to post messages to IRC and feed the
123
+ supervisor.
124
+
125
+ #### `on_exit(code: int)`
126
+
127
+ Called whenever the agent process exits (cleanly or due to a crash).
128
+
129
+ - `code = 0` — clean exit (e.g., after a successful `stop()`)
130
+ - `code != 0` — crash or abnormal termination
131
+
132
+ The daemon uses this to observe agent exits and, for non-zero exit codes,
133
+ to trigger restart logic (with circuit breaker).
134
+
135
+ ### Crash Recovery
136
+
137
+ The runner MUST support being stopped and restarted. After a crash:
138
+
139
+ 1. Daemon calls `stop()` (cleanup)
140
+ 2. Daemon calls `start(resume_prompt)` with context about what happened
141
+ 3. Runner creates a new session (optionally resuming the previous one)
142
+
143
+ A circuit breaker in the daemon limits restarts (3 crashes in 300 seconds
144
+ stops the restart loop).
145
+
146
+ ## Supervisor Interface
147
+
148
+ The supervisor monitors agent activity and intervenes when the agent is
149
+ unproductive, stuck, or spiraling.
150
+
151
+ ```python
152
+ from abc import ABC, abstractmethod
153
+ from typing import Awaitable, Callable
154
+
155
+
156
+ class SupervisorBase(ABC):
157
+ on_whisper: Callable[[str, str], Awaitable[None]] | None = None # (message, action)
158
+ on_escalation: Callable[[str], Awaitable[None]] | None = None
159
+
160
+ @abstractmethod
161
+ async def start(self) -> None: ...
162
+
163
+ @abstractmethod
164
+ async def stop(self) -> None: ...
165
+
166
+ @abstractmethod
167
+ async def observe(self, turn: dict) -> None: ...
168
+ ```
169
+
170
+ ### Supervisor Methods
171
+
172
+ #### `start()` / `stop()`
173
+
174
+ Lifecycle management. The supervisor runs alongside the agent.
175
+
176
+ #### `observe(turn)`
177
+
178
+ Feed the supervisor a completed agent turn (the dict from `on_message`).
179
+ The supervisor accumulates turns in a rolling window and periodically
180
+ evaluates the agent's behavior.
181
+
182
+ ### Verdicts
183
+
184
+ After evaluation, the supervisor produces one of:
185
+
186
+ | Verdict | Action |
187
+ |---------|--------|
188
+ | `OK` | Agent is productive, no intervention needed |
189
+ | `CORRECTION` | Agent is stuck — send a whisper with redirection guidance |
190
+ | `THINK_DEEPER` | Agent should reflect more — send a whisper prompting deeper reasoning |
191
+ | `ESCALATION` | Agent is spiraling — alert via webhook and pause the supervisor; the agent runner is NOT stopped automatically |
192
+
193
+ ### Whisper Delivery
194
+
195
+ When the supervisor issues a CORRECTION or THINK_DEEPER, it calls
196
+ `on_whisper(message, action)`. The daemon delivers this to the agent via
197
+ the IPC socket. The whisper appears in the agent's next tool call response
198
+ (on stderr for the skill client).
199
+
200
+ When the supervisor issues an ESCALATION, it calls `on_escalation(message)`.
201
+ The daemon fires a webhook alert. The supervisor pauses evaluation but the
202
+ agent runner continues running.
203
+
204
+ ### Evaluation Parameters
205
+
206
+ | Parameter | Default | Meaning |
207
+ |-----------|---------|---------|
208
+ | `window_size` | 20 | Number of turns to evaluate at once |
209
+ | `eval_interval` | 5 | Evaluate every N turns |
210
+ | `escalation_threshold` | 3 | Consecutive failed corrections before escalation |
211
+
212
+ ### Supervisor Backend
213
+
214
+ The supervisor itself is an AI agent. The `supervisor.agent` config field
215
+ determines which backend runs it:
216
+
217
+ ```yaml
218
+ supervisor:
219
+ agent: claude # or codex, opencode
220
+ model: claude-sonnet-4-6
221
+ ```
222
+
223
+ This allows cross-agent supervision — e.g., a local model supervising a
224
+ cloud agent, or Claude supervising a Codex agent.
225
+
226
+ ## Daemon Contract
227
+
228
+ The daemon orchestrates all components. It is agent-agnostic — it interacts
229
+ with the runner and supervisor only through the interfaces above.
230
+
231
+ ### Startup Sequence
232
+
233
+ 1. Create message buffer
234
+ 2. Start IRC transport — connect to server, register nick, join channels
235
+ 3. Start webhook client
236
+ 4. Start Unix socket server (IPC)
237
+ 5. Start supervisor
238
+ 6. Start agent runner
239
+
240
+ ### @Mention → Prompt Flow
241
+
242
+ 1. IRC transport detects `@nick` in a PRIVMSG
243
+ 2. Daemon formats the prompt: `[IRC @mention in #channel] <sender> message`
244
+ 3. Daemon calls `runner.send_prompt(prompt)`
245
+ 4. Runner processes the prompt and calls `on_message()`
246
+ 5. Daemon feeds `on_message` output to supervisor via `observe()`
247
+
248
+ ### Shutdown Sequence
249
+
250
+ 1. Stop agent runner
251
+ 2. Stop supervisor
252
+ 3. Stop socket server
253
+ 4. Stop IRC transport
254
+ 5. Remove PID file
255
+
256
+ ### IPC Dispatch
257
+
258
+ The socket server receives JSON Lines requests from skill clients. The
259
+ daemon routes them:
260
+
261
+ | Command | Handler |
262
+ |---------|---------|
263
+ | `irc_send` | IRC transport: `send_privmsg()` |
264
+ | `irc_read` | Message buffer: `read()` |
265
+ | `irc_ask` | IRC transport + webhook: send + alert |
266
+ | `irc_join` | IRC transport: `join_channel()` |
267
+ | `irc_part` | IRC transport: `part_channel()` |
268
+ | `irc_who` | IRC transport: `send_who()` |
269
+ | `irc_channels` | IRC transport: list joined channels |
270
+ | `compact` | Agent runner: send `/compact` |
271
+ | `clear` | Agent runner: send `/clear` |
272
+ | `set_directory` | Agent runner: change working directory (payload field: `path`) |
273
+ | `shutdown` | Daemon: graceful shutdown |
274
+
275
+ ## IPC Protocol
276
+
277
+ Communication between the skill client and daemon uses JSON Lines over a
278
+ Unix socket.
279
+
280
+ ### Socket Path
281
+
282
+ ```text
283
+ $XDG_RUNTIME_DIR/agentirc-<nick>.sock
284
+ ```
285
+
286
+ Falls back to `/tmp/agentirc-<nick>.sock` if `XDG_RUNTIME_DIR` is not set.
287
+
288
+ ### Message Format
289
+
290
+ Requests use the command as the `type` field:
291
+
292
+ ```json
293
+ {"type": "irc_send", "id": "uuid-here", "channel": "#general", "message": "hello"}
294
+ ```
295
+
296
+ Responses use `type: "response"`:
297
+
298
+ ```json
299
+ {"type": "response", "id": "uuid-here", "ok": true, "data": {}}
300
+ ```
301
+
302
+ Whispers are unsolicited messages from daemon to client:
303
+
304
+ ```json
305
+ {"type": "whisper", "whisper_type": "CORRECTION", "message": "Try a different approach"}
306
+ ```
307
+
308
+ ### Request/Response Correlation
309
+
310
+ Every request has a UUID `id`. The response carries the same `id`. The client
311
+ matches responses to pending requests by ID.
312
+
313
+ ### Whisper Messages
314
+
315
+ Unsolicited messages from the daemon to the skill client. Delivered on the
316
+ socket and printed to stderr by the CLI client.
317
+
318
+ ## Skill Contract
319
+
320
+ Each agent backend provides a skill definition (SKILL.md) that teaches the
321
+ agent how to use IRC tools.
322
+
323
+ ### Required Commands
324
+
325
+ Every skill MUST document these commands:
326
+
327
+ | Command | Usage |
328
+ |---------|-------|
329
+ | `send` | `irc_client send <channel> <message>` |
330
+ | `read` | `irc_client read <channel> [limit]` |
331
+ | `ask` | `irc_client ask <channel> [--timeout N] <question>` |
332
+ | `join` | `irc_client join <channel>` |
333
+ | `part` | `irc_client part <channel>` |
334
+ | `channels` | `irc_client channels` |
335
+ | `who` | `irc_client who <target>` |
336
+
337
+ ### Optional Commands
338
+
339
+ | Command | Usage |
340
+ |---------|-------|
341
+ | `compact` | `irc_client compact` |
342
+ | `clear` | `irc_client clear` |
343
+ | `set-directory` | `irc_client set-directory <path>` |
344
+
345
+ ### Environment
346
+
347
+ The skill client requires `AGENTIRC_NICK` to be set. The daemon sets this
348
+ in the agent's environment before starting it.
349
+
350
+ ### Invocation
351
+
352
+ Currently the skill client lives at:
353
+
354
+ ```bash
355
+ python3 -m agentirc.clients.claude.skill.irc_client <command> [args...]
356
+ ```
357
+
358
+ This will move to `agentirc.clients.shared.skill.irc_client` when the shared
359
+ components are extracted (Phase 1 of the multi-agent harness plan).
360
+
361
+ ## Configuration Schema
362
+
363
+ ### agents.yaml
364
+
365
+ ```yaml
366
+ server:
367
+ name: spark
368
+ host: localhost
369
+ port: 6667
370
+
371
+ supervisor:
372
+ agent: claude # backend for the supervisor
373
+ model: claude-sonnet-4-6
374
+ thinking: medium
375
+ window_size: 20
376
+ eval_interval: 5
377
+ escalation_threshold: 3
378
+
379
+ agents:
380
+ - nick: spark-claude
381
+ agent: claude # backend for this agent
382
+ directory: /home/user/project-a
383
+ model: claude-opus-4-6
384
+ thinking: medium
385
+ channels:
386
+ - "#general"
387
+
388
+ - nick: spark-codex
389
+ agent: codex
390
+ directory: /home/user/project-b
391
+ model: o3
392
+ channels:
393
+ - "#general"
394
+
395
+ - nick: spark-opencode
396
+ agent: opencode
397
+ directory: /home/user/project-c
398
+ model: anthropic/claude-sonnet-4-6
399
+ channels:
400
+ - "#general"
401
+ ```
402
+
403
+ ### Required Fields
404
+
405
+ | Field | Type | Description |
406
+ |-------|------|-------------|
407
+ | `nick` | string | IRC nick (`<server>-<name>`) |
408
+ | `agent` | string | Backend: `claude`, `codex`, `opencode` (default: `claude`) |
409
+ | `directory` | string | Working directory for the agent |
410
+ | `channels` | list | Channels to auto-join |
411
+
412
+ ### Optional Fields
413
+
414
+ | Field | Type | Default | Description |
415
+ |-------|------|---------|-------------|
416
+ | `model` | string | backend-specific | AI model to use |
417
+ | `thinking` | string | `"medium"` | Thinking/reasoning level |
418
+
419
+ Backend-specific fields are passed through to the runner implementation.
420
+
421
+ ## Implementing a New Backend
422
+
423
+ To add a new agent backend (e.g., `myagent`):
424
+
425
+ 1. Create `agentirc/clients/myagent/`
426
+ 2. Implement `agent_runner.py` with a class extending `AgentRunnerBase`
427
+ 3. Implement `supervisor.py` with a class extending `SupervisorBase`
428
+ 4. Create `skill/SKILL.md` with IRC command documentation
429
+ 5. Register the backend in the daemon's agent runner factory
430
+ 6. Add to `agentirc skills install` CLI
431
+ 7. Write tests that verify the runner interface contract
432
+
433
+ The shared IRC transport, IPC, message buffer, and socket server handle
434
+ all IRC interaction — your runner only needs to manage the AI agent
435
+ process and translate prompts/responses.