agents-can-communicate 0.1.18 → 0.2.0

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 (134) hide show
  1. package/README.md +78 -70
  2. package/SECURITY.md +31 -0
  3. package/bin/acc-hook.mjs +94 -12
  4. package/bin/acc-mcp.mjs +6 -2
  5. package/bin/acc.mjs +6 -1
  6. package/docs/ADAPTER_AUTHORING.md +172 -0
  7. package/docs/ARCHITECTURE.md +131 -0
  8. package/docs/CAPABILITIES.md +102 -214
  9. package/docs/CLI.md +157 -0
  10. package/docs/CONCEPTS.md +134 -0
  11. package/docs/CONFIGURATION.md +143 -0
  12. package/docs/DESIGN_DECISIONS.md +89 -0
  13. package/docs/GETTING_STARTED.md +145 -0
  14. package/docs/GLOSSARY.md +26 -0
  15. package/docs/MCP.md +94 -0
  16. package/docs/PROTOCOL.md +200 -0
  17. package/docs/RELEASING.md +109 -0
  18. package/docs/SECURITY_MODEL.md +131 -0
  19. package/docs/TROUBLESHOOTING.md +102 -0
  20. package/docs/WHY_ACC.md +61 -0
  21. package/docs/index.md +42 -0
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +20 -22
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +12 -4
  33. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
  34. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
  35. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
  36. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
  37. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
  38. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
  41. package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
  42. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +20 -22
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +18 -11
  44. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
  45. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  46. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +20 -22
  47. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
  48. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
  49. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
  50. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
  51. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
  52. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
  53. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +7 -3
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
  56. package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
  57. package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
  58. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +20 -22
  59. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +9 -9
  60. package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
  61. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
  62. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
  63. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
  64. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
  65. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
  66. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
  67. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
  68. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +20 -22
  69. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
  70. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  71. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
  72. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
  73. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
  77. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  78. package/node_modules/@agents-can-communicate/cli/src/args.mjs +11 -30
  79. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
  80. package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
  81. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +9 -2
  82. package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
  83. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  84. package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
  85. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
  86. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
  87. package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
  88. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
  89. package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
  90. package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
  91. package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
  92. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
  93. package/node_modules/@agents-can-communicate/core/src/service.mjs +11 -10
  94. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
  95. package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
  96. package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
  97. package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
  98. package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
  99. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
  100. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  101. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
  102. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +115 -63
  103. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  104. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
  105. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
  106. package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
  107. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
  108. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
  109. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  110. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
  111. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
  112. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
  113. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
  114. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  115. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
  116. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
  117. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
  118. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
  119. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
  120. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  121. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
  122. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
  123. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
  124. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
  125. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
  129. package/package.json +19 -1
  130. package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
  131. package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
  132. package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
  133. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
  134. package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
package/README.md CHANGED
@@ -4,101 +4,109 @@
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![Node](https://img.shields.io/badge/node-%E2%89%A524-brightgreen.svg)](https://nodejs.org)
6
6
 
7
- **Give the agent sessions you already opened a shared room and stop being the wire
8
- between them.**
7
+ ACC connects independently opened AI sessions so they can discover, ask, answer, acknowledge, and hand off without becoming one managed agent team.
9
8
 
10
- ACC is a local-first coordination layer for independent AI coding sessions on one repo.
11
- They see who else is here, claim the files they touch, message each other directly, and
12
- hand work across while each keeps its own client, checkout, permissions, and human. It
13
- runs entirely on your machine, on Node's standard library with **zero runtime
14
- dependencies**, and your transcripts never leave the client.
9
+ It is a local-first communication layer for sessions you already run. Each session keeps
10
+ its own client, model, checkout, permissions, context, and human direction. ACC supplies a
11
+ shared room with presence, intent, narrow file claims, durable conversation threads, and
12
+ truthful per-recipient receipts. It does not launch, steer, supervise, or terminate agents.
15
13
 
16
- ## The wall is you
14
+ The project uses Node's standard library with **zero runtime dependencies**. Coordination
15
+ state stays in platform app data outside the repository, and ACC never collects raw
16
+ transcripts.
17
17
 
18
- One session refactors and is about to remove a field called `item.drive`. Another, in a
19
- file you're not looking at, still reads it. Neither terminal knows the other exists — so
20
- either you carry the warning across by hand, or it ships broken.
18
+ ## Stop relaying between windows
21
19
 
22
- **Without ACC**, every warning, question, and handoff between sessions stops at you and
23
- starts again from you. **With ACC**, the sessions carry them to each other: the second
24
- agent sees the claim, gets the measured impact as a message, and the change lands in one
25
- piece. You go back to directing.
20
+ One session finds that removing `item.drive` will break another area. A second session is
21
+ working there, but neither client knows the other exists. Without ACC, the warning stops at
22
+ you: copy it to the other window, copy the answer back, and repeat for every question.
23
+
24
+ With ACC, the first session sends an attributed question. The second retrieves it, replies
25
+ in the same thread, and thereby acknowledges the original. The first retrieves the answer.
26
+ The message is durable throughout; neither model gains authority over the other.
26
27
 
27
28
  ```mermaid
28
- flowchart TB
29
- Y["Youset direction"]
30
- A["session A"]
31
- B["session B"]
32
- C["session C"]
33
- R["ACC shared room<br/>presence · claims · messages · handoffs"]
34
- Y --> A
35
- Y --> B
36
- Y --> C
37
- A <--> R
38
- B <--> R
39
- C <--> R
29
+ flowchart LR
30
+ A["session A independently opened"] <--> R["ACC room<br/>presence · messages · receipts · claims"]
31
+ B["session B — independently opened"] <--> R
32
+ U["You — direction and authority"] --> A
33
+ U --> B
40
34
  ```
41
35
 
36
+ The product's canonical activation event is simple: a second independently opened session
37
+ completes a useful acknowledged interaction without the human copying peer message content.
38
+
42
39
  ## Install
43
40
 
44
- Two commands, once per macOS or Linux machine where your clients run:
41
+ On macOS or Linux with Node 24 or newer:
45
42
 
46
43
  ```bash
47
44
  npm install -g agents-can-communicate
48
45
  acc install
49
46
  ```
50
47
 
51
- `acc install` wires ACC into the clients you have Codex, Claude Code, Gemini CLI, Grok,
52
- Kimi Code and names everything it changed. **Restart the client afterwards** (hooks load
53
- at startup); Codex also needs you to trust the plugin. Then just open a session in a
54
- project and it joins that project's room by itself; open a second and they coordinate.
55
- `acc doctor` shows what's active, `acc update --apply` keeps it current, `acc uninstall`
56
- removes only what ACC wrote.
48
+ Restart the clients whose hooks were installed; Codex also requires trusting the plugin.
49
+ Then open two sessions in the same repository or plain directory as usual. They remain
50
+ independent and join the same ACC workspace. Use `acc doctor` to see exact versions,
51
+ installation health, and delivery downgrades.
57
52
 
58
- Coordination data lives in the platform app-data directory (`~/Library/Application
59
- Support/acc`, `~/.local/share/acc`; override with `ACC_DATA_HOME`), never in your repo.
60
- Every worktree of a repo shares one room; a plain folder works the same.
53
+ Runtime state lives in `~/Library/Application Support/acc` on macOS or the XDG data
54
+ directory on Linux. `ACC_DATA_HOME` can override it, but ACC refuses a location inside a
55
+ workspace. Git is optional; worktrees of one repository share awareness.
61
56
 
62
- ## What the room holds
57
+ ## The communication loop
63
58
 
64
- | | |
65
- |---|---|
66
- | **presence** | who's live, their client and branch, and what each intends — one `acc status`. |
67
- | **claims** | the files each session is touching; on guardable clients a clashing edit is refused and the owner named. |
68
- | **messages** | questions, decisions, notes — attributed, delivered on the recipient's next turn, and data they weigh rather than orders. |
69
- | **requests & handoffs** | work addressed to a participant, so it waits across restarts and carries the answer back. |
59
+ ```bash
60
+ # Session A publishes awareness and reserves only what it will edit.
61
+ acc work --summary "changing the item schema" --mode edit --hint 'file:src/item.mjs'
62
+ acc claim --resource 'file:src/item.mjs' --reason "changing the item schema"
70
63
 
71
- A targeted `acc inbox` survives context compaction, and `acc reply` answers and
72
- acknowledges in one operation. A session alone in a repo pays nothing — no banner, no
73
- protocol, nothing left behind.
64
+ # Session A asks a participant named by `acc status`.
65
+ acc message --to models --type question --subject "item.drive" \
66
+ --body "Can your code stop reading item.drive before I remove it?"
74
67
 
75
- ## Everyday controls
68
+ # Session B reads and answers the exact message.
69
+ acc inbox --message message_x
70
+ acc reply --message message_x --body "Yes. Commit abc123 removes the final read."
76
71
 
77
- The installed guidance teaches your agents to claim, ask, request, and hand off on their
78
- own. These give you a direct view when you want one:
72
+ # Either session records a handoff while context is still available.
73
+ acc finish --goal "remove item.drive" --status complete \
74
+ --completed "schema and reader updated" --remaining "none"
75
+ ```
79
76
 
80
- | Command | For |
81
- |---|---|
82
- | `acc status` | who's here, what they claim, and the room's protection level |
83
- | `acc inbox` | recover an addressed message without a workspace dump |
84
- | `acc reply` | answer and acknowledge in one operation |
85
- | `acc doctor` | confirm which client integrations are active and current |
86
- | `acc update --apply` | install the latest release and refresh integrations |
87
- | `acc uninstall` | remove ACC's integrations — settings you changed stay yours |
77
+ Messages commit before any delivery attempt. `queued`, `offered`, `retrieved`, and
78
+ `acknowledged` describe different observable facts: an offer is not a read, retrieval is
79
+ not proof of model attention, and a reply resolves communication rather than proving a
80
+ requested task finished.
88
81
 
89
- ## Documentation
82
+ Durable inbox recovery is the baseline for every client. Certified next-turn injection can
83
+ reduce the polling, but only for the exact client version and platform captured by the
84
+ adapter. Native live push is opt-in and would also require a current reachable binding;
85
+ **no shipped adapter currently has passing live-push certification**. The failed Codex
86
+ 0.152.0 and Claude Code 2.1.252 captures are shipped as evidence, so ACC reports fallback
87
+ instead of claiming realtime delivery.
88
+
89
+ ## What ACC owns
90
90
 
91
- Start at the **[documentation map](docs/index.md)** it lays out a path for whatever
92
- brought you here: [why ACC](docs/WHY_ACC.md) and [concepts](docs/CONCEPTS.md) to evaluate
93
- it, [getting started](docs/GETTING_STARTED.md) to run it, the [CLI](docs/CLI.md) /
94
- [protocol](docs/PROTOCOL.md) / [capabilities](docs/CAPABILITIES.md) reference, and
95
- [adapter authoring](docs/ADAPTER_AUTHORING.md) to extend it. New to the vocabulary? The
96
- [glossary](docs/GLOSSARY.md) defines every term in one line.
91
+ | ACC owns | ACC does not own |
92
+ |---|---|
93
+ | participant and session presence | process or model lifecycle |
94
+ | current intent and resource claims | prompts, permissions, or token budgets |
95
+ | messages, threads, replies, acknowledgements | work queues or execution state |
96
+ | delivery evidence and visible fallback | raw transcripts or shared model memory |
97
97
 
98
- See it run: [three workstreams](examples/three-workstreams.md) · [research in a plain
99
- folder](examples/non-git-research.md). Contributing starts with [AGENTS.md](AGENTS.md).
98
+ A peer message is untrusted input, never system authority. A guarded claim can stop only
99
+ the write paths a client actually exposes; `acc status` reports `advisory` when that cannot
100
+ be guaranteed.
101
+
102
+ ## Documentation
100
103
 
101
- ## Requirements & license
104
+ Start with [Getting started](docs/GETTING_STARTED.md), then use the
105
+ [documentation map](docs/index.md). The exact surfaces are in the [CLI](docs/CLI.md),
106
+ [MCP](docs/MCP.md), [Protocol](docs/PROTOCOL.md), and
107
+ [Capabilities](docs/CAPABILITIES.md) references. Adapter evidence lives beside each
108
+ adapter in its `COMPATIBILITY.md` and `certification.json`.
102
109
 
103
- Node 24+, macOS or Linux, Git optional. MIT — use it, fork it, keep it
104
- ([LICENSE](LICENSE)).
110
+ Contributing starts with the repository's
111
+ [AGENTS.md](https://github.com/automatis-tools/agents-can-communicate/blob/main/AGENTS.md).
112
+ Node 24+, Git optional, MIT licensed.
package/SECURITY.md ADDED
@@ -0,0 +1,31 @@
1
+ # Security policy
2
+
3
+ ## Reporting
4
+
5
+ Report privately via [GitHub security advisories](https://github.com/automatis-tools/agents-can-communicate/security/advisories/new).
6
+
7
+ Please do not open a public issue first.
8
+
9
+ Include: what you ran, what happened, what you expected, and the client and
10
+ version. A reproduction against a throwaway `ACC_DATA_HOME` is ideal.
11
+
12
+ ## Scope
13
+
14
+ | In scope | Out of scope |
15
+ |---|---|
16
+ | Peer text escaping its quoted block | An attacker who already has write access to your data home |
17
+ | Any path escaping the managed root | A model choosing to obey persuasive peer text |
18
+ | Uninstall deleting files ACC did not write | Vulnerabilities in Codex, Claude Code, Gemini, Grok, or Kimi themselves |
19
+ | ACC writing into a repository | Denial of service by a trusted peer |
20
+ | Session impersonation across MCP | |
21
+
22
+ Reasoning behind each: [docs/SECURITY_MODEL.md](docs/SECURITY_MODEL.md).
23
+
24
+ ## Supported versions
25
+
26
+ Pre-1.0: the latest release only.
27
+
28
+ ## What we will do
29
+
30
+ Acknowledge, reproduce, and tell you whether it is in scope. If it is, the fix
31
+ ships with a test in `tests/security/` so it cannot come back quietly.
package/bin/acc-hook.mjs CHANGED
@@ -6,6 +6,8 @@
6
6
  // someone's session stops working. Unknown adapter, malformed payload, broken
7
7
  // store, missing binding - all of them end in "allow, exit 0".
8
8
  import { randomBytes } from "node:crypto";
9
+ import { realpathSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
9
11
 
10
12
  import { createId } from "@agents-can-communicate/protocol";
11
13
  import { runHook } from "@agents-can-communicate/hook-runner";
@@ -36,20 +38,100 @@ const readStdin = () => new Promise(resolve => {
36
38
  return undefined;
37
39
  });
38
40
 
39
- const [adapterId] = process.argv.slice(2);
41
+ export const writeOutput = (stream, output, { deadlineAt } = {}) => {
42
+ if (output === "") return Promise.resolve();
43
+ return new Promise((resolve, reject) => {
44
+ let settled = false;
45
+ let timer;
46
+ const finish = error => {
47
+ if (settled) return;
48
+ settled = true;
49
+ if (timer !== undefined) clearTimeout(timer);
50
+ if (error === undefined || error === null) resolve();
51
+ else reject(error);
52
+ };
53
+ if (deadlineAt !== undefined) {
54
+ const remaining = deadlineAt - Date.now();
55
+ if (remaining <= 0) {
56
+ finish(new Error("hook budget exhausted before stdout write"));
57
+ return;
58
+ }
59
+ timer = setTimeout(() => finish(
60
+ new Error("hook budget exhausted waiting for stdout callback")), remaining);
61
+ }
62
+ try {
63
+ stream.write(output, finish);
64
+ } catch (error) {
65
+ finish(error);
66
+ }
67
+ });
68
+ };
69
+
70
+ const DIAGNOSTIC_BYTES = 512;
71
+
72
+ function boundedDiagnostic(label, error) {
73
+ const detail = String(error?.message ?? error).replace(/[\u0000-\u001f\u007f]/g, " ");
74
+ let line = `acc: ${label}: ${detail}`;
75
+ while (Buffer.byteLength(`${line}\n`, "utf8") > DIAGNOSTIC_BYTES && line.length > 0) {
76
+ line = line.slice(0, -1);
77
+ }
78
+ return `${line}\n`;
79
+ }
40
80
 
41
- let payload = null;
81
+ function tryWrite(stream, output) {
82
+ if (output === "") return;
83
+ try {
84
+ stream.write(output, () => {});
85
+ } catch {
86
+ // A broken diagnostic stream must not turn a failed-open hook into a crash.
87
+ }
88
+ }
89
+
90
+ export async function completeHookOutput(result,
91
+ { stdout = process.stdout, stderr = process.stderr } = {}) {
92
+ try {
93
+ await writeOutput(stdout, result.stdout ?? "", { deadlineAt: result.deadlineAt });
94
+ } catch (error) {
95
+ tryWrite(stderr, boundedDiagnostic("stdout write failed", error));
96
+ return { exitCode: 0, wroteStdout: false, committedOffers: false };
97
+ }
98
+
99
+ let committedOffers = true;
100
+ try {
101
+ await result.commitOffers?.();
102
+ } catch (error) {
103
+ committedOffers = false;
104
+ tryWrite(stderr, boundedDiagnostic("offer commit failed", error));
105
+ }
106
+ if (result.stderr) tryWrite(stderr, `${result.stderr}\n`);
107
+ return { exitCode: 0, wroteStdout: true, committedOffers };
108
+ }
109
+
110
+ async function main() {
111
+ const [adapterId] = process.argv.slice(2);
112
+
113
+ let payload = null;
114
+ try {
115
+ payload = JSON.parse(await readStdin());
116
+ } catch {
117
+ payload = null;
118
+ }
119
+
120
+ const result = await runHook({ adapterId, payload, adapters,
121
+ runtime: { clock: { now: () => new Date().toISOString() },
122
+ ids: { next: kind => createId(kind, randomBytes) } },
123
+ env: process.env });
124
+
125
+ const completed = await completeHookOutput(result);
126
+ process.exitCode = result.exitCode ?? completed.exitCode;
127
+ }
128
+
129
+ let isMain = false;
42
130
  try {
43
- payload = JSON.parse(await readStdin());
131
+ isMain = process.argv[1] !== undefined
132
+ && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
44
133
  } catch {
45
- payload = null;
134
+ isMain = false;
46
135
  }
47
136
 
48
- const result = await runHook({ adapterId, payload, adapters,
49
- runtime: { clock: { now: () => new Date().toISOString() },
50
- ids: { next: kind => createId(kind, randomBytes) } },
51
- env: process.env });
52
-
53
- if (result.stdout !== "") process.stdout.write(result.stdout);
54
- if (result.stderr) process.stderr.write(`${result.stderr}\n`);
55
- process.exitCode = result.exitCode ?? 0;
137
+ if (isMain) await main();
package/bin/acc-mcp.mjs CHANGED
@@ -3,8 +3,9 @@ import { randomBytes } from "node:crypto";
3
3
 
4
4
  import { EXIT, createId } from "@agents-can-communicate/protocol";
5
5
  import { createCoordinationService } from "@agents-can-communicate/core";
6
+ import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
6
7
  import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
7
- import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
8
+ import { ALL_ADAPTERS, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
8
9
  from "@agents-can-communicate/cli";
9
10
  import { serve } from "@agents-can-communicate/mcp-server";
10
11
 
@@ -40,13 +41,16 @@ const paths = runtimePaths({
40
41
  });
41
42
  const store = await openFilesystemStore({ root: paths.root, clock, ids,
42
43
  workspaceId: descriptor.id });
44
+ const service = createCoordinationService({ store, clock, ids });
45
+ const adapters = Object.fromEntries(ALL_ADAPTERS().map(adapter => [adapter.id, adapter]));
43
46
 
44
47
  await serve({
45
48
  input: process.stdin,
46
49
  output: process.stdout,
47
50
  log: message => process.stderr.write(`acc-mcp: ${message}\n`),
48
51
  context: {
49
- service: createCoordinationService({ store, clock, ids }),
52
+ service,
53
+ deliveryRouter: createDeliveryRouter({ service, adapters, clock }),
50
54
  workspaceId: descriptor.id,
51
55
  participantId,
52
56
  descriptor,
package/bin/acc.mjs CHANGED
@@ -3,7 +3,10 @@ import { randomBytes } from "node:crypto";
3
3
  import { readFile } from "node:fs/promises";
4
4
 
5
5
  import { createId } from "@agents-can-communicate/protocol";
6
- import { askConfirmation, main } from "@agents-can-communicate/cli";
6
+ import { createDeliveryRouter } from "@agents-can-communicate/delivery-router";
7
+ import { ALL_ADAPTERS, askConfirmation, main } from "@agents-can-communicate/cli";
8
+
9
+ const adapters = Object.fromEntries(ALL_ADAPTERS().map(adapter => [adapter.id, adapter]));
7
10
 
8
11
  // The composition root is the only place allowed to reach for ambient time and
9
12
  // randomness; everything below it receives them as ports.
@@ -15,6 +18,8 @@ const runtime = {
15
18
  stderr: process.stderr,
16
19
  clock: { now: () => new Date().toISOString() },
17
20
  ids: { next: kind => createId(kind, randomBytes) },
21
+ createDeliveryRouter: ({ service, clock }) =>
22
+ createDeliveryRouter({ service, adapters, clock }),
18
23
  // Asked only by `acc config init`, and only when stdout is a terminal. There
19
24
  // was no port here at all, so the question went to the fallback that always
20
25
  // answers no: in a real terminal the command printed "not written" and never
@@ -0,0 +1,172 @@
1
+ # Writing an adapter
2
+
3
+ An adapter teaches ACC one client. Nothing else in ACC knows that client exists. This page
4
+ assumes the vocabulary in [Protocol](PROTOCOL.md#identity-hierarchy) — session, participant,
5
+ claim — and points to [Capabilities](CAPABILITIES.md) for what was actually measured per
6
+ client; see the [documentation map](index.md) for where both fit among the rest.
7
+
8
+ ```mermaid
9
+ graph LR
10
+ H[client hook] --> R[acc-hook runtime]
11
+ R -->|normalizeHook| E[normalised event]
12
+ E --> CO[core: attach, claims, sync]
13
+ CO -->|denyOutcome / injectOutcome| R
14
+ R --> H
15
+ ```
16
+
17
+ ## The manifest
18
+
19
+ ```js
20
+ export function createExampleAdapter() {
21
+ return defineAdapter({
22
+ id: "example", // portable id
23
+ displayName: "Example CLI",
24
+ client: { command: "example", certificationName: "example-cli",
25
+ versionArgs: ["--version"] },
26
+ certification, // imported package-local certification.json
27
+ capabilities: { delivery: { nextTurn: true } },
28
+
29
+ detect, install, uninstall, doctor,
30
+ planInstall, // what install would write
31
+ normalizeHook, // client payload -> normalised event
32
+ renderContext, // SyncResult -> text
33
+ renderContextResult, // text + ids of complete rendered groups
34
+ denyOutcome, injectOutcome, // how this client is answered
35
+ });
36
+ }
37
+ ```
38
+
39
+ `renderContextResult` is required wherever an adapter renders peer messages. It returns
40
+ `{ text, includedMessageIds, includedAttentionIds }`, and the [delivery
41
+ lifecycle](PROTOCOL.md#delivery-lifecycle) advances only from those ids — never by searching
42
+ `text` for one, because peer text is untrusted and can imitate another message's header.
43
+ `projectContextResult()` implements this contract; `projectContext()` remains the text-only
44
+ convenience API for adapters that don't need it. An adapter with only the older
45
+ `renderContext()` gets pending bodies withheld and a visible `acc inbox` degradation warning
46
+ instead: repeating an untracked body every turn would be quieter in code and dishonest about
47
+ what was actually delivered.
48
+
49
+ `client.command` does double duty. `detect.mjs` uses it as the version-probe binary, and
50
+ presence liveness separately walks the hook's process ancestry for the first ancestor whose
51
+ executable basename matches it, to learn the client's own pid. Declare the binary the client
52
+ actually runs as — `command: "claude"` for a client that really runs as `node` resolves
53
+ nothing, and the failure is silent: the session gets `pid: null` and falls back to reading
54
+ presence by age alone, with nothing telling you why.
55
+
56
+ ## Capabilities
57
+
58
+ Fourteen booleans in four groups, declared in the manifest's `capabilities` object:
59
+
60
+ | Group | Entries |
61
+ |---|---|
62
+ | `lifecycle` | `sessionStart` `sessionResume` `sessionEnd` `heartbeat` `childSessions` |
63
+ | `context` | `startupInjection` `beforeTurnInjection` `safePointInjection` |
64
+ | `guards` | `beforeRead` `beforeWrite` `beforeShell` |
65
+ | `delivery` | `nextTurn` `livePush` `replyRoute` |
66
+
67
+ **False by default. `true` requires a backing method *and* an observed capture.**
68
+ `defineAdapter` enforces the method — declaring `guards.beforeWrite: true` without
69
+ `guardWrite()` is a usage error at construction. It also requires a passing entry in the
70
+ validated `certification.json`; method existence is never evidence. What each shipped
71
+ client was actually observed doing against this list is [Capabilities](CAPABILITIES.md#matrix).
72
+
73
+ `lifecycle.heartbeat` is deliberately not a flavour of `delivery.nextTurn`. Next-turn
74
+ delivery happens only when the client reaches a normal turn boundary; heartbeat fires on a
75
+ timer even while the session is idle.
76
+
77
+ ### Certification evidence
78
+
79
+ Every adapter ships `certification.json` and every referenced capture under `fixtures/`.
80
+ Each evidence entry contains `client`, exact `version`, exact `platform`, `observedAt`,
81
+ `capability`, package-relative `fixture`, `idleBehavior`, `busyBehavior`,
82
+ `authorityLevel`, `limitations`, and `result` (`pass` or `fail`). A copied documentation
83
+ example is not a capture. Failed experiments stay in the manifest as `fail`; they explain
84
+ the false value and can never enable it.
85
+
86
+ `effectiveCapabilities(adapter, { clientVersion, platform })` returns the full boolean
87
+ shape for the installed client. Only an exact passing version/platform match remains true.
88
+ Unreadable, unknown, or mismatched clients degrade every uncertified row to false.
89
+
90
+ The backing methods for delivery are `renderContextResult()` for `nextTurn`,
91
+ `offerMessage()` for `livePush`, and `routeReply()` for `replyRoute`.
92
+
93
+ ## How far you can get
94
+
95
+ | Tier | You register | You get | You do not get |
96
+ |---|---|---|---|
97
+ | 0 | nothing — humans run `acc` | durable messages, status, claims | anything automatic |
98
+ | 1 | the MCP server | attach on first call, read, claim, message | guards, session end |
99
+ | 2 | hooks + skill | automatic attach, turn context, write guards, cleanup | realtime |
100
+ | 3 | + realtime surface | delivery receipts, safe-point injection, child sessions | — |
101
+
102
+ Installed hook wiring may reach tier 2, but the effective capability is still limited to
103
+ an exact certified client/version/platform. No client currently certifies native live push
104
+ or native reply routing.
105
+
106
+ ## normalizeHook
107
+
108
+ Whitelist, never a filter. Every client hands hooks the prompt, the transcript path, or the
109
+ tool output; none of it may survive.
110
+
111
+ ```js
112
+ return normalizedEvent({
113
+ kind, sessionId, cwd, model, parentSessionId, tool,
114
+ targets, // paths this call would WRITE. For a shell call, pass the command to
115
+ // shellWriteTargets() — it reads write positions only, never reads.
116
+ });
117
+ ```
118
+
119
+ Refuse an unrecognised payload. Inventing a session attaches the wrong one, or a new one
120
+ every hook, and looks like it is working.
121
+
122
+ ## Response contracts do not port
123
+
124
+ Measure them. Every client differs, and a wrong shape fails **silently**:
125
+
126
+ | | deny | inject |
127
+ |---|---|---|
128
+ | Codex | exit 2 + stderr | plain stdout (`developer` message) |
129
+ | Claude Code | `hookSpecificOutput.permissionDecision` | same envelope |
130
+ | Gemini CLI | `{"decision":"block"}` | `hookSpecificOutput` envelope |
131
+ | Grok | `{"decision":"deny","reason"}` (documented; deny not yet captured) | UserPromptSubmit stdout discarded on 1.0.13 |
132
+ | Kimi Code | `hookSpecificOutput.permissionDecision` | plain stdout |
133
+
134
+ `denyOutcome(reason)` returns `{ stdout, stderr, exitCode }`, so the runtime never has to
135
+ know which client it is talking to. This table is only the shape each shipped adapter
136
+ actually uses; the full experimental grid — every candidate shape tried against every
137
+ client, including which ones are silently ignored — is measured in
138
+ [Capabilities](CAPABILITIES.md#response-contracts-which-do-not-port).
139
+
140
+ ## Install and ownership
141
+
142
+ ```mermaid
143
+ graph TB
144
+ P[planInstall] -->|artifacts| K{kind}
145
+ K -->|tree| T[a directory ACC creates<br/>removable if unchanged]
146
+ K -->|merge| M[a file the user owns<br/>never deleted]
147
+ ```
148
+
149
+ Rules that are not negotiable:
150
+
151
+ - idempotent — installing twice equals installing once;
152
+ - reversible — uninstall restores the user's file byte for byte;
153
+ - absolute command paths — a hook's environment carries no PATH;
154
+ - honour `keep`: uninstall receives paths the user has since edited.
155
+
156
+ `planInstall` must use the same path helpers as `install`. A conformance test compares
157
+ them, because a plan that drifts makes `--dry-run` a decoration.
158
+
159
+ ## Conformance
160
+
161
+ ```bash
162
+ node --test tests/conformance/*.test.mjs
163
+ node --test tests/process/hook-wiring.test.mjs
164
+ ```
165
+
166
+ The second one *executes* what your install wrote. Three adapters once shipped a hook
167
+ command that did not exist anywhere; every test was green.
168
+
169
+ ## Record what you learned
170
+
171
+ One `COMPATIBILITY.md` per adapter: client version, event names, payload fields, the deny
172
+ matrix, and what you could **not** observe. The next person's alternative is guessing.