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
@@ -0,0 +1,158 @@
1
+ import path from "node:path";
2
+
3
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
4
+
5
+ export const CAPABILITY_SHAPE = Object.freeze({
6
+ lifecycle: Object.freeze(["sessionStart", "sessionResume", "sessionEnd", "heartbeat",
7
+ "childSessions"]),
8
+ context: Object.freeze(["startupInjection", "beforeTurnInjection", "safePointInjection"]),
9
+ guards: Object.freeze(["beforeRead", "beforeWrite", "beforeShell"]),
10
+ delivery: Object.freeze(["nextTurn", "livePush", "replyRoute"]),
11
+ });
12
+
13
+ const REQUIRED_TEXT = Object.freeze(["client", "version", "platform", "observedAt",
14
+ "capability", "fixture", "provenance", "provenanceId", "idleBehavior", "busyBehavior",
15
+ "authorityLevel"]);
16
+ const MANIFEST_KEYS = new Set(["evidence"]);
17
+ const EVIDENCE_KEYS = new Set([...REQUIRED_TEXT, "limitations", "result"]);
18
+ const RESULTS = new Set(["pass", "fail"]);
19
+ const VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
20
+ const PLATFORM = /^(?:darwin|linux|win32)-(?:arm64|x64)$/;
21
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
22
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
23
+
24
+ function usage(message, details = {}) {
25
+ throw new AccError(EXIT.USAGE, message, details);
26
+ }
27
+
28
+ function assertText(entry, key, index) {
29
+ if (typeof entry[key] !== "string" || entry[key].trim() === ""
30
+ || entry[key] !== entry[key].trim()) {
31
+ usage(`certification evidence ${index} must declare ${key}`, { index, key });
32
+ }
33
+ }
34
+
35
+ function validatePackageJson(value, index, key) {
36
+ if (path.isAbsolute(value) || value.includes("\\") || value.split("/").includes("..")
37
+ || !value.startsWith("fixtures/") || !value.endsWith(".json")) {
38
+ usage(`certification evidence ${index} ${key} must be package-local JSON`,
39
+ { index, [key]: value });
40
+ }
41
+ }
42
+
43
+ function validateObservedAt(value, index) {
44
+ const parsed = new Date(value);
45
+ if (!Number.isFinite(parsed.valueOf())) {
46
+ usage(`certification evidence ${index} observedAt must be an exact ISO date or timestamp`,
47
+ { index, observedAt: value });
48
+ }
49
+ const validDate = DATE.test(value)
50
+ && parsed.toISOString().slice(0, 10) === value;
51
+ const validTimestamp = TIMESTAMP.test(value)
52
+ && [parsed.toISOString(), parsed.toISOString().replace(".000Z", "Z")].includes(value);
53
+ if (!validDate && !validTimestamp) {
54
+ usage(`certification evidence ${index} observedAt must be an exact ISO date or timestamp`,
55
+ { index, observedAt: value });
56
+ }
57
+ }
58
+
59
+ function rejectUnknownKeys(value, known, label, index = null) {
60
+ const unknown = Object.keys(value).find(key => !known.has(key));
61
+ if (unknown !== undefined) {
62
+ usage(`unknown ${label} field ${unknown}`,
63
+ index === null ? { key: unknown } : { index, key: unknown });
64
+ }
65
+ }
66
+
67
+ export function validateCertification(manifest) {
68
+ if (manifest === null || typeof manifest !== "object" || Array.isArray(manifest)) {
69
+ usage("an adapter must declare certification evidence");
70
+ }
71
+ rejectUnknownKeys(manifest, MANIFEST_KEYS, "certification");
72
+ if (!Array.isArray(manifest.evidence)) {
73
+ usage("certification.evidence must be an array");
74
+ }
75
+ const evidence = manifest.evidence.map((entry, index) => {
76
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
77
+ usage(`certification evidence ${index} must be an object`, { index });
78
+ }
79
+ rejectUnknownKeys(entry, EVIDENCE_KEYS, "certification evidence", index);
80
+ for (const key of REQUIRED_TEXT) assertText(entry, key, index);
81
+ validatePackageJson(entry.fixture, index, "fixture");
82
+ validatePackageJson(entry.provenance, index, "provenance");
83
+ if (!VERSION.test(entry.version) || entry.version.toLowerCase() === "unknown") {
84
+ usage(`certification evidence ${index} version must be an exact client version`,
85
+ { index, version: entry.version });
86
+ }
87
+ if (!PLATFORM.test(entry.platform) || entry.platform.toLowerCase() === "unknown") {
88
+ usage(`certification evidence ${index} platform must be an exact supported platform`,
89
+ { index, platform: entry.platform });
90
+ }
91
+ validateObservedAt(entry.observedAt, index);
92
+ if (!RESULTS.has(entry.result)) {
93
+ usage(`certification evidence ${index} result must be pass or fail`,
94
+ { index, result: entry.result });
95
+ }
96
+ if (!Array.isArray(entry.limitations)
97
+ || entry.limitations.some(item => typeof item !== "string" || item.trim() === "")) {
98
+ usage(`certification evidence ${index} limitations must be an array of text`, { index });
99
+ }
100
+ return Object.freeze({
101
+ client: entry.client,
102
+ version: entry.version,
103
+ platform: entry.platform,
104
+ observedAt: entry.observedAt,
105
+ capability: entry.capability,
106
+ fixture: entry.fixture,
107
+ provenance: entry.provenance,
108
+ provenanceId: entry.provenanceId,
109
+ idleBehavior: entry.idleBehavior,
110
+ busyBehavior: entry.busyBehavior,
111
+ authorityLevel: entry.authorityLevel,
112
+ limitations: Object.freeze([...entry.limitations]),
113
+ result: entry.result,
114
+ });
115
+ });
116
+ const tuples = new Set();
117
+ for (const [index, entry] of evidence.entries()) {
118
+ const tuple = JSON.stringify([entry.client, entry.version, entry.platform,
119
+ entry.capability]);
120
+ if (tuples.has(tuple)) {
121
+ usage(`duplicate certification tuple at evidence ${index}`,
122
+ { index, client: entry.client, version: entry.version,
123
+ platform: entry.platform, capability: entry.capability });
124
+ }
125
+ tuples.add(tuple);
126
+ }
127
+ return Object.freeze({ evidence: Object.freeze(evidence) });
128
+ }
129
+
130
+ function falseCapabilities() {
131
+ return Object.fromEntries(Object.entries(CAPABILITY_SHAPE).map(([group, names]) => [group,
132
+ Object.fromEntries(names.map(name => [name, false]))]));
133
+ }
134
+
135
+ export function effectiveCapabilities(adapter, { clientVersion, platform } = {}) {
136
+ const resolved = falseCapabilities();
137
+ if (typeof clientVersion !== "string" || !VERSION.test(clientVersion)
138
+ || clientVersion.toLowerCase() === "unknown"
139
+ || typeof platform !== "string" || !PLATFORM.test(platform)
140
+ || platform.toLowerCase() === "unknown") return freezeCapabilities(resolved);
141
+ const client = adapter.client?.certificationName ?? adapter.client?.command;
142
+ const passing = new Set((adapter.certification?.evidence ?? [])
143
+ .filter(item => item.result === "pass" && item.client === client
144
+ && item.version === clientVersion && item.platform === platform)
145
+ .map(item => item.capability));
146
+ for (const [group, names] of Object.entries(CAPABILITY_SHAPE)) {
147
+ for (const name of names) {
148
+ resolved[group][name] = adapter.capabilities?.[group]?.[name] === true
149
+ && passing.has(`${group}.${name}`);
150
+ }
151
+ }
152
+ return freezeCapabilities(resolved);
153
+ }
154
+
155
+ export function freezeCapabilities(capabilities) {
156
+ for (const group of Object.keys(CAPABILITY_SHAPE)) Object.freeze(capabilities[group]);
157
+ return Object.freeze(capabilities);
158
+ }
@@ -28,15 +28,27 @@ function truncate(line, limit) {
28
28
  return cut === "" ? "" : `${cut}…`;
29
29
  }
30
30
 
31
- function attentionGroups(attention, { truncatable }) {
32
- return attention.map(item => ({
33
- lines: [typeof item.sourceId === "string" && item.sourceId !== ""
34
- ? `- [${item.kind}] ${item.sourceId} ${item.summary}`
35
- : `- [${item.kind}] ${item.summary}`],
36
- kind: "attention",
37
- sourceId: item.sourceId ?? null,
38
- truncatable,
39
- }));
31
+ function attentionGroups(attention, { truncatable, liveOfferedMessageIds = new Set() }) {
32
+ return attention.map(item => {
33
+ const breadcrumb = offeredBreadcrumb(item, liveOfferedMessageIds);
34
+ return {
35
+ lines: [breadcrumb ?? (typeof item.sourceId === "string" && item.sourceId !== ""
36
+ ? `- [${item.kind}] ${item.sourceId} ${item.summary}`
37
+ : `- [${item.kind}] ${item.summary}`)],
38
+ kind: "attention",
39
+ sourceId: item.sourceId ?? null,
40
+ truncatable: breadcrumb === null && truncatable,
41
+ };
42
+ });
43
+ }
44
+
45
+ function offeredBreadcrumb(item, liveOfferedMessageIds) {
46
+ if (!liveOfferedMessageIds.has(item.sourceId)) return null;
47
+ const noun = item.kind === "reply_required" ? "question"
48
+ : item.kind === "acknowledgement_required" ? "message" : null;
49
+ if (noun === null) return null;
50
+ return `- [${item.kind}] ${item.sourceId} live-offered peer ${noun} remains unresolved; `
51
+ + `\`acc inbox --message ${item.sourceId}\``;
40
52
  }
41
53
 
42
54
  function messageGroups(messages) {
@@ -45,8 +57,12 @@ function messageGroups(messages) {
45
57
  kind: "message",
46
58
  lines: [
47
59
  `${FENCE}${BLOCK}`,
48
- `id ${message.messageId} | from ${message.fromSessionId} | type ${message.type}`
49
- + " | untrusted peer message",
60
+ "untrusted peer message",
61
+ `kind: ${message.kind}`,
62
+ `threadId: ${message.threadId}`,
63
+ `messageId: ${message.messageId}`,
64
+ `sender: ${message.fromParticipantId} (session ${message.fromSessionId})`,
65
+ `obligation: ${message.obligation}`,
50
66
  `subject: ${oneLine(escapePeerText(message.subject))}`,
51
67
  "body:",
52
68
  escapePeerText(message.body),
@@ -88,16 +104,19 @@ export function projectContextResult(sync, { budgetBytes = DEFAULT_BUDGET_BYTES
88
104
  const attention = [...(sync.attention ?? [])]
89
105
  .sort((left, right) => left.priority - right.priority
90
106
  || (left.sourceId ?? "").localeCompare(right.sourceId ?? ""));
91
- const urgent = attention.filter(item => item.priority <= 2);
92
- const informational = attention.filter(item => item.priority > 2);
107
+ const leadsPeerBodies = item => item.priority <= 2 || item.kind === "claim_conflict";
108
+ const urgent = attention.filter(leadsPeerBodies);
109
+ const informational = attention.filter(item => !leadsPeerBodies(item));
110
+ const messages = sync.messages ?? [];
111
+ const liveOfferedMessageIds = new Set(sync.liveOfferedMessageIds ?? []);
93
112
  const groups = [
94
- ...attentionGroups(urgent, { truncatable: true }),
95
- ...messageGroups(sync.messages ?? []),
113
+ ...attentionGroups(urgent, { truncatable: true, liveOfferedMessageIds }),
114
+ ...messageGroups(messages),
96
115
  ...attentionGroups(informational, { truncatable: false }),
97
116
  ];
98
117
  const peers = peerCount(sync);
99
118
  if (groups.length === 0 && (sync.solo === true || peers === 0)) {
100
- return { text: "", includedMessageIds: [], includedAttentionIds: [] };
119
+ return { text: "", offeredMessageIds: [], includedAttentionIds: [] };
101
120
  }
102
121
 
103
122
  const fullHeader = groups.length === 0 ? ambientHeader(peers) : "ACC (load the acc skill):";
@@ -159,7 +178,7 @@ export function projectContextResult(sync, { budgetBytes = DEFAULT_BUDGET_BYTES
159
178
 
160
179
  return {
161
180
  text: lines.join("\n"),
162
- includedMessageIds: included
181
+ offeredMessageIds: included
163
182
  .filter(item => item.group.kind === "message")
164
183
  .map(item => item.group.messageId),
165
184
  includedAttentionIds: included
@@ -1,4 +1,4 @@
1
- import { access, chmod, mkdir, readFile, readdir, rm, writeFile }
1
+ import { access, chmod, lstat, mkdir, readFile, readdir, rm, writeFile }
2
2
  from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
@@ -147,6 +147,12 @@ export async function writeHookShim({ dir, adapterId, runner = defaultRunner(),
147
147
  */
148
148
  export async function removeInstalledTree(target, keep = []) {
149
149
  if (keep.includes(target)) return false;
150
+ try {
151
+ await lstat(target);
152
+ } catch (error) {
153
+ if (error.code === "ENOENT") return false;
154
+ throw error;
155
+ }
150
156
  await rm(target, { recursive: true, force: true });
151
157
  return true;
152
158
  }
@@ -1,6 +1,7 @@
1
1
  // Capability contract, context projection, config ownership, and the binding
2
2
  // that survives between two ephemeral hook processes.
3
3
  export { CAPABILITY_SHAPE, assertCapabilities, defineAdapter } from "./capabilities.mjs";
4
+ export { effectiveCapabilities, validateCertification } from "./certification.mjs";
4
5
  export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
5
6
  export { assertRunner, bakeSkillCommand, defaultCli, defaultRunner, removeInstalledTree,
6
7
  runnerExists, writeHookShim }
@@ -18,13 +18,16 @@ const fileFor = (runtimeDir, harnessSessionId) => path.join(runtimeDir, "binding
18
18
  * detach the exact generation instead of opening a second session.
19
19
  *
20
20
  * It lives in the runtime directory, never the project, and carries identity
21
- * only - no prompt, no transcript, no harness state.
21
+ * plus the exact client facts observed at attach time - no prompt, transcript,
22
+ * or harness state.
22
23
  */
23
24
  export async function storeSessionBinding({ runtimeDir, harnessSessionId, accSessionId,
24
- generation }) {
25
+ generation, clientVersion, platform }) {
25
26
  const file = fileFor(runtimeDir, harnessSessionId);
26
27
  await mkdir(path.dirname(file), { recursive: true });
27
28
  const record = { schemaVersion: SCHEMA_VERSION, harnessSessionId, accSessionId, generation };
29
+ if (typeof clientVersion === "string" && clientVersion !== "") record.clientVersion = clientVersion;
30
+ if (typeof platform === "string" && platform !== "") record.platform = platform;
28
31
  const temporary = `${file}.${process.pid}.tmp`;
29
32
  await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, "utf8");
30
33
  // Replace rather than append: re-attaching supersedes the old generation, and
@@ -54,7 +57,10 @@ export async function loadSessionBinding({ runtimeDir, harnessSessionId }) {
54
57
  throw new AccError(EXIT.DATA, "unknown session binding schemaVersion",
55
58
  { file, schemaVersion: record?.schemaVersion });
56
59
  }
57
- return { accSessionId: record.accSessionId, generation: record.generation };
60
+ const binding = { accSessionId: record.accSessionId, generation: record.generation };
61
+ if (typeof record.clientVersion === "string") binding.clientVersion = record.clientVersion;
62
+ if (typeof record.platform === "string") binding.platform = record.platform;
63
+ return binding;
58
64
  }
59
65
 
60
66
  export async function clearSessionBinding({ runtimeDir, harnessSessionId }) {
@@ -89,7 +95,10 @@ export async function listSessionBindings({ runtimeDir }) {
89
95
  .then(JSON.parse).catch(() => null);
90
96
  if (record?.schemaVersion !== SCHEMA_VERSION) continue;
91
97
  bindings.push({ harnessSessionId: record.harnessSessionId,
92
- accSessionId: record.accSessionId, generation: record.generation });
98
+ accSessionId: record.accSessionId, generation: record.generation,
99
+ ...(typeof record.clientVersion === "string"
100
+ ? { clientVersion: record.clientVersion } : {}),
101
+ ...(typeof record.platform === "string" ? { platform: record.platform } : {}) });
93
102
  }
94
103
  return bindings;
95
104
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.1.18",
3
+ "version": "0.2.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,7 +14,7 @@ export const COMMANDS = Object.freeze({
14
14
  detach: { required: ["session", "generation"], optional: [] },
15
15
  sync: { required: [], optional: ["session", "cursor", "limit", "scope"] },
16
16
  work: { required: [], optional: ["session", "generation", "summary", "mode",
17
- "state", "workstream"], repeated: ["hint"], flags: ["clear"] },
17
+ "state"], repeated: ["hint"], flags: ["clear"] },
18
18
  claim: { required: ["resource"],
19
19
  optional: ["session", "generation", "mode", "enforcement", "reason", "lease"] },
20
20
  // Neither is required on its own, because either one names the claim: an id
@@ -24,37 +24,17 @@ export const COMMANDS = Object.freeze({
24
24
  release: { required: [],
25
25
  optional: ["claim", "resource", "session", "generation", "authority", "reason"] },
26
26
  message: { required: ["subject", "body"],
27
- optional: ["session", "generation", "type", "priority", "workstream"],
28
- repeated: ["to"], flags: ["requires-ack"] },
27
+ optional: ["session", "generation", "type", "obligation", "client-message-id"],
28
+ repeated: ["to"] },
29
29
  inbox: { required: [], optional: ["session", "generation", "message"] },
30
30
  reply: { required: ["message", "body"],
31
- optional: ["session", "generation", "subject", "type", "priority"] },
32
- // Asking another agent to do something: one call, because a task nobody was
33
- // told about and a message pointing at no task are each useless.
31
+ optional: ["session", "generation", "subject", "client-message-id"] },
32
+ // Asking another agent to do something as a message with a reply obligation.
34
33
  request: { required: ["to", "title"],
35
- optional: ["session", "generation", "detail", "workstream", "priority"],
36
- repeated: ["depends-on"] },
37
- // Create a task, or act on one with --task. A workstream is optional: small
38
- // requests should not have to invent a project first.
39
- task: { required: [], optional: ["session", "generation", "workstream", "title",
40
- "detail", "assignee", "state", "task", "reason"],
41
- repeated: ["depends-on"], flags: ["take", "decline", "force"] },
42
- // Create one, or take and hand back the coordination of one that exists.
43
- // Creating a workstream raised `coordinator_missing` on every turn from then
44
- // on, and nothing could answer it: the two core operations that do had no
45
- // surface at all.
46
- workstream: { required: [], optional: ["session", "generation", "title", "objective",
47
- "workstream"], flags: ["take", "release"] },
48
- // Messages not tied to a task need a way to be answered too. Without one a
49
- // `requiresAck` message raised an attention item nothing could ever clear.
50
- ack: { required: ["message"], optional: ["session", "generation", "state"] },
51
- // Recording what was settled, so the next session does not reopen it. The
52
- // protocol has described this object from the start and nothing could make
53
- // one: no command, no tool.
54
- decide: { required: ["title", "outcome"],
55
- optional: ["session", "generation", "authority", "workstream", "supersedes"],
56
- repeated: ["decided-by"], flags: ["human"] },
57
- finish: { required: ["goal"], optional: ["session", "generation", "status", "to"],
34
+ optional: ["session", "generation", "detail", "client-message-id"] },
35
+ ack: { required: ["message"], optional: ["session", "generation"] },
36
+ finish: { required: ["goal"], optional: ["session", "generation", "status", "to",
37
+ "client-message-id"],
58
38
  repeated: ["completed", "remaining", "blocker"] },
59
39
  status: { required: [], optional: ["participant"], flags: ["all"] },
60
40
  doctor: { required: [], optional: ["home"], flags: ["repair"] },
@@ -69,7 +49,8 @@ export const COMMANDS = Object.freeze({
69
49
  // `--downgrade` because an older acc first on PATH will otherwise rewire every
70
50
  // client to itself, and the only symptom is a guard behaving like the version
71
51
  // it came from.
72
- install: { required: [], optional: ["adapter", "home"], flags: ["dry-run", "downgrade"] },
52
+ install: { required: [], optional: ["adapter", "home", "delivery"],
53
+ flags: ["dry-run", "downgrade"] },
73
54
  // `--dry-run` on both, because the preview was computed for either action and
74
55
  // only `install` could ask for it. Removal is the side that reaches into a
75
56
  // client's configuration - including a client that has left the machine.
@@ -238,6 +238,9 @@ export async function runDoctor({ options, context, runtime }) {
238
238
  // the store was healthy and nothing else.
239
239
  const text = [`store healthy; ${describePresence(status.counts)}; `
240
240
  + `protection ${status.protection}; ${installed} of ${adapters.length} adapter(s) installed`,
241
+ ...adapters.filter(adapter => (adapter.present || adapter.installed)
242
+ && typeof adapter.deliveryDiagnostic === "string")
243
+ .map(adapter => ` ${adapter.deliveryDiagnostic}`),
241
244
  ...data.remediation.map(line => ` ${line}`),
242
245
  // `0 of 4` is a true line that reads as a broken machine, and on an
243
246
  // MCP-only one it would read that way on every run forever. The server needs
@@ -14,7 +14,7 @@ import { COMMANDS } from "./args.mjs";
14
14
  const GROUPS = Object.freeze([
15
15
  ["Set up", ["install", "uninstall", "doctor", "config"]],
16
16
  ["In a session", ["status", "sync", "work", "claim", "release", "inbox", "reply",
17
- "ack", "message", "request", "task", "workstream", "decide", "finish"]],
17
+ "ack", "message", "request", "finish"]],
18
18
  ["Driven by adapters, not by people", ["attach", "heartbeat", "detach"]],
19
19
  ["About acc", ["help", "version", "update"]],
20
20
  ]);
@@ -33,10 +33,7 @@ const SUMMARY = Object.freeze({
33
33
  message: "send a typed message to named participants",
34
34
  inbox: "read only unresolved messages addressed to this participant",
35
35
  reply: "reply to one message and acknowledge it in the same operation",
36
- request: "ask another agent to do something: the work and the why, in one call",
37
- task: "create work, --take it, or move its --state along",
38
- workstream: "group related work, and steer it with --take / --release",
39
- decide: "record what was settled, so the next session does not reopen it",
36
+ request: "ask another agent to do something in a reply-required message",
40
37
  finish: "write the handoff and release what this session held",
41
38
  attach: "open a session; an adapter calls this, not a person",
42
39
  heartbeat: "say the session is still alive",
@@ -110,6 +110,8 @@ export function describeChanges(operation, home) {
110
110
  }
111
111
  return [
112
112
  ...(operation.removed ?? []).map(file => ` removed ${shorten(file, home)}`),
113
+ ...(operation.removedDirectories ?? [])
114
+ .map(file => ` removed ${shorten(file, home)}`),
113
115
  ...edited,
114
116
  ...(operation.kept ?? [])
115
117
  .map(file => ` kept ${shorten(file, home)} - changed since ACC wrote it`),
@@ -122,6 +124,9 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
122
124
  + (failed.length > 0 ? `; ${failed.length} failed` : ""),
123
125
  ...operations.filter(operation => operation.applied)
124
126
  .flatMap(operation => describeChanges(operation, home)),
127
+ ...operations.filter(operation => operation.applied
128
+ && typeof operation.deliveryDiagnostic === "string")
129
+ .map(operation => ` ${operation.deliveryDiagnostic}`),
125
130
  ...skipped.map(entry => ` skip ${entry.adapterId}: ${entry.reason}`),
126
131
  ...failed.map(entry => ` ${entry.adapterId}: ${entry.error}`),
127
132
  // Said once, where it is needed: the reader has just been shown a list of
@@ -165,7 +170,8 @@ export function failureOf({ action, acted, failed = [] }) {
165
170
  export function actedOn(result) {
166
171
  return result.operations.filter(operation => operation.applied
167
172
  && (result.action === "install"
168
- || (operation.removed?.length ?? 0) + (operation.changes?.length ?? 0) > 0)).length;
173
+ || (operation.removed?.length ?? 0) + (operation.removedDirectories?.length ?? 0)
174
+ + (operation.changes?.length ?? 0) > 0)).length;
169
175
  }
170
176
 
171
177
  export async function runInstallCommand({ options, runtime, action = "install" }) {
@@ -200,7 +206,8 @@ export async function runInstallCommand({ options, runtime, action = "install" }
200
206
  ? []
201
207
  : (Array.isArray(options.adapter) ? options.adapter : [options.adapter]);
202
208
  const plan = planInstallation({ adapters, detected, context, action, recorded,
203
- accVersion, allowDowngrade: options.downgrade === true, requested });
209
+ accVersion, allowDowngrade: options.downgrade === true, requested,
210
+ delivery: options.delivery ?? "off" });
204
211
  const result = await applyPlan({ plan, adapters, context, dataHome, dryRun, accVersion });
205
212
 
206
213
  const acted = actedOn(result);