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.
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/CHANGELOG.md +25 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/CLAUDE.md +13 -1
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/PKG-INFO +1 -1
- agentirc_cli-0.6.0/agentirc/__init__.py +1 -0
- agentirc_cli-0.6.0/agentirc/clients/claude/config.py +163 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/skill/irc_client.py +2 -2
- agentirc_cli-0.6.0/docs/agent-harness-spec.md +435 -0
- agentirc_cli-0.6.0/packages/agent-harness/README.md +45 -0
- agentirc_cli-0.6.0/packages/agent-harness/daemon.py +212 -0
- agentirc_cli-0.6.0/packages/agent-harness/ipc.py +38 -0
- agentirc_cli-0.6.0/packages/agent-harness/irc_transport.py +147 -0
- agentirc_cli-0.6.0/packages/agent-harness/message_buffer.py +46 -0
- agentirc_cli-0.6.0/packages/agent-harness/skill/SKILL.md +50 -0
- agentirc_cli-0.6.0/packages/agent-harness/skill/irc_client.py +282 -0
- agentirc_cli-0.6.0/packages/agent-harness/socket_server.py +107 -0
- agentirc_cli-0.6.0/packages/agent-harness/webhook.py +60 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/pyproject.toml +1 -1
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/uv.lock +1 -1
- agentirc_cli-0.4.0/agentirc/__init__.py +0 -1
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.gitignore +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/.pr_agent.toml +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/CNAME +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/Gemfile +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/Gemfile.lock +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/LICENSE +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/README.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/_config.yml +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/cli.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/__main__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/daemon.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/ipc.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/socket_server.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/supervisor.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/claude/webhook.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/codex/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/observer.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/pidfile.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/commands.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/extensions/federation.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/extensions/history.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/message.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/protocol-index.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/protocol/replies.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/__main__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/channel.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/client.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/config.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/ircd.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/remote_client.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/server_link.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/skill.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/skills/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/agentirc/server/skills/history.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/agent-client.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/ci.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/cli.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/design.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/docs-site.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/getting-started.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer1-core-irc.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer2-attention.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer3-skills.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer4-federation.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/layer5-agent-harness.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/publishing.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/server-architecture.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/03-research-deep-dive.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/04-agent-delegation.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/05-benchmark-swarm.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/07-knowledge-pipeline.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/08-supervisor-intervention.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases/09-apps-as-agents.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/index.md +0 -0
- {agentirc_cli-0.4.0/agentirc/clients/claude → agentirc_cli-0.6.0/packages/agent-harness}/config.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/__init__.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/conftest.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_channel.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_connection.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_federation.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_history.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_message.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_modes.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_skills.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-0.4.0 → agentirc_cli-0.6.0}/tests/test_supervisor.py +0 -0
- {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
|
|
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
|
|
|
@@ -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
|
|
178
|
-
return await self._request("set_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.
|