agents-can-communicate 0.7.1 → 0.8.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 (85) hide show
  1. package/README.md +3 -1
  2. package/bin/entrypoints/acc-bootstrap.mjs +6 -48
  3. package/bin/entrypoints/acc-claude-channel.mjs +47 -157
  4. package/docs/ADAPTER_AUTHORING.md +41 -10
  5. package/docs/ARCHITECTURE.md +23 -13
  6. package/docs/CAPABILITIES.md +19 -16
  7. package/docs/CLI.md +48 -13
  8. package/docs/CONCEPTS.md +5 -4
  9. package/docs/CONFIGURATION.md +18 -11
  10. package/docs/GETTING_STARTED.md +15 -12
  11. package/docs/GLOSSARY.md +2 -1
  12. package/docs/HOW_IT_WORKS.md +8 -4
  13. package/docs/PROTOCOL.md +25 -6
  14. package/docs/RELEASING.md +3 -2
  15. package/docs/SECURITY_MODEL.md +14 -1
  16. package/docs/TROUBLESHOOTING.md +64 -27
  17. package/docs/UPGRADING.md +49 -8
  18. package/docs/WHY_ACC.md +5 -3
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  21. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +14 -73
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +29 -71
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282-product-evidence.json +161 -0
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.282.json +28 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/inbox-wake-2.1.282.json +44 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +5 -2
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +25 -30
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +6 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-contract.mjs +10 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +254 -0
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +166 -0
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +71 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +109 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +25 -31
  35. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  36. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  37. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  38. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  40. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  41. package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +16 -3
  42. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +4 -3
  43. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
  44. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +4 -14
  45. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +23 -11
  46. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +5 -1
  47. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  48. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +8 -6
  49. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +8 -23
  50. package/node_modules/@agents-can-communicate/cli/src/install-delivery-consent.mjs +3 -4
  51. package/node_modules/@agents-can-communicate/cli/src/main.mjs +25 -3
  52. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -43
  53. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
  54. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +3 -4
  55. package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +2 -2
  56. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  57. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +25 -2
  58. package/node_modules/@agents-can-communicate/core/src/status.mjs +19 -1
  59. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  60. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +24 -7
  61. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  62. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +6 -8
  63. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +24 -11
  64. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +1 -1
  65. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  66. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +19 -5
  67. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +3 -7
  68. package/node_modules/@agents-can-communicate/installer/src/index.mjs +4 -7
  69. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +23 -61
  70. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +7 -1
  71. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +15 -5
  72. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +10 -133
  73. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  75. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  76. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +6 -1
  77. package/package.json +1 -1
  78. package/bin/acc-bootstrap.mjs +0 -7
  79. package/bin/acc-claude-channel.mjs +0 -10
  80. package/bin/entrypoints/claude-channel-binding.mjs +0 -46
  81. package/bin/entrypoints/claude-channel-stdio.mjs +0 -29
  82. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +0 -8
  83. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +0 -381
  84. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +0 -232
  85. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +0 -150
@@ -7,13 +7,14 @@ import path from "node:path";
7
7
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
- import { acccreatedFile, bakeSkillCommand, blankJson, defaultBootstrap, defaultChannel,
10
+ import { acccreatedFile, bakeSkillCommand, blankJson,
11
11
  mergeOwnedEntries, ownedEntries,
12
12
  keepVersions, ownVersion, stampPluginVersion,
13
13
  removeIfEmpty,
14
14
  removeInstalledTree,
15
15
  removeOwnedEntries, writeCliShim, writeForeignJson, writeHookShim }
16
16
  from "@agents-can-communicate/adapter-sdk";
17
+ import { MANAGED_SETTINGS, inboundStatus } from "./inbox-settings.mjs";
17
18
 
18
19
  const bundle = fileURLToPath(new URL("../plugin", import.meta.url));
19
20
  const manifest = fileURLToPath(new URL("../plugin/.claude-plugin/plugin.json",
@@ -22,9 +23,6 @@ const manifest = fileURLToPath(new URL("../plugin/.claude-plugin/plugin.json",
22
23
  const PLUGIN_NAME = "agents-can-communicate";
23
24
  const MARKETPLACE = "acc-local";
24
25
  const QUALIFIED = `${PLUGIN_NAME}@${MARKETPLACE}`;
25
- const CHANNEL_ACTIVATION_CHECK = "Check Claude's Channels startup notice for ACC; "
26
- + "an MCP connection does not verify inbound delivery. If Channels are unavailable "
27
- + "or blocked, resolve that client warning before expecting live messages.";
28
26
 
29
27
  /**
30
28
  * How this client actually installs a plugin.
@@ -147,24 +145,21 @@ const marketplaceManifest = () => ({
147
145
  }],
148
146
  });
149
147
 
150
- // The channel MCP entry Claude Code loads only when the session is started with
151
- // the captured development-channel flag. Written with the pinned Node and the
152
- // installed Channel binary, so the generated file carries no repository path;
153
- // removed entirely for a non-live install so a plain launch spawns nothing.
148
+ // Up to 0.7.x a live install wrote the Channel MCP server here, and Claude
149
+ // Code spawned it for every session that enabled the plugin. The inbox wake
150
+ // needs no MCP server, so no plugin copy ACC keeps may name one.
154
151
  const mcpPath = target => path.join(target, ".mcp.json");
155
- async function writeChannelMcp(target, { node, channel }) {
156
- await writeJson(mcpPath(target), { mcpServers: { "acc-channel": {
157
- command: node, args: [channel] } } });
152
+
153
+ async function removeChannelConfig(versionsRoot) {
154
+ const versions = await readdir(versionsRoot).catch(() => []);
155
+ await Promise.all(versions.map(version => rm(mcpPath(path.join(versionsRoot, version)),
156
+ { force: true })));
158
157
  }
159
158
 
160
159
  /** A plugin tree with the shim written and the skill's command baked in. */
161
- async function layOutPlugin(target, { runner, node, cli, channel, live }) {
160
+ async function layOutPlugin(target, { runner, node, cli }) {
162
161
  await rm(target, { recursive: true, force: true });
163
162
  await cp(bundle, target, { recursive: true });
164
- // The bundle ships a placeholder .mcp.json; the real one is written only for a
165
- // live install, and a non-live tree carries none.
166
- await rm(mcpPath(target), { force: true });
167
- if (live) await writeChannelMcp(target, { node, channel });
168
163
  // The skill ships with a placeholder where the command belongs: `acc` is not
169
164
  // on PATH everywhere, and an agent that cannot run it improvises. The shim
170
165
  // carries the pinning so each example can name one path. No data home is
@@ -181,9 +176,8 @@ async function layOutPlugin(target, { runner, node, cli, channel, live }) {
181
176
  version: await pluginVersion(), io: { readFile, writeFile } });
182
177
  }
183
178
 
184
- export async function installClaudePlugin({ configDir, runner, cli, keepPreviousVersion = null, node = process.execPath,
185
- channel = defaultChannel(), livePolicy = "off", now = new Date() }) {
186
- const live = livePolicy === "actionable" || livePolicy === "all";
179
+ export async function installClaudePlugin({ configDir, runner, cli, keepPreviousVersion = null,
180
+ node = process.execPath, now = new Date() }) {
187
181
  // Everything this will merge into, read before a byte is written. A settings
188
182
  // file that will not parse used to be discovered after the plugin tree was
189
183
  // already on disk, and the install then failed with nineteen files left
@@ -200,12 +194,12 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
200
194
  const source = sourceDir(configDir);
201
195
  const cached = cachePath(configDir, version);
202
196
 
203
- await layOutPlugin(source, { runner, node, cli, channel, live });
197
+ await layOutPlugin(source, { runner, node, cli });
204
198
  await writeJson(marketplaceFile(configDir), marketplaceManifest());
205
199
  // The copy the client runs from. Written here rather than asking the user to
206
200
  // run `claude plugin install`, exactly as the Codex adapter does, because the
207
201
  // command's only effect is this copy plus the two registry entries below.
208
- await layOutPlugin(cached, { runner, node, cli, channel, live });
202
+ await layOutPlugin(cached, { runner, node, cli });
209
203
  // The copy just written, plus the one an upgrade moved off. A client caches a
210
204
  // plugin under its version, so every upgrade would otherwise leave the previous
211
205
  // release's tree beside this one - invisible while the version never moved,
@@ -214,6 +208,10 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
214
208
  // anything older than that holds no session and is litter.
215
209
  await keepVersions({ root: path.dirname(cached), keep: [version, keepPreviousVersion],
216
210
  io: { readdir, rm } });
211
+ // The copy an upgrade moved off stays for sessions already running from it,
212
+ // but its Channel config would still start a Channel for any session that
213
+ // reads it. Those sessions keep what they loaded; nothing new starts one.
214
+ await removeChannelConfig(path.dirname(cached));
217
215
 
218
216
  await writeClientJson(knownMarketplacesPath(configDir), {
219
217
  ...known,
@@ -254,12 +252,9 @@ export async function installClaudePlugin({ configDir, runner, cli, keepPrevious
254
252
 
255
253
  return { ok: true,
256
254
  changes: [source, cached, marketplaceFile(configDir),
257
- knownMarketplacesPath(configDir), installedPluginsPath(configDir), file,
258
- ...(live ? [mcpPath(source), mcpPath(cached)] : [])],
259
- needsAction: live ? [CHANNEL_ACTIVATION_CHECK] : [],
260
- diagnostics: live
261
- ? ["native channel wired; Claude's experimental development-channel warning still applies"]
262
- : [] };
255
+ knownMarketplacesPath(configDir), installedPluginsPath(configDir), file],
256
+ needsAction: [],
257
+ diagnostics: [] };
263
258
  }
264
259
 
265
260
  export async function uninstallClaudePlugin({ configDir, keep = [] }) {
@@ -297,16 +292,15 @@ export async function uninstallClaudePlugin({ configDir, keep = [] }) {
297
292
  return { ok: true, changes, diagnostics: [] };
298
293
  }
299
294
 
300
- export async function detectClaude({ configDir }) {
295
+ export async function detectClaude({ configDir, managedSettingsPath = MANAGED_SETTINGS[process.platform] }) {
301
296
  const settings = await readJson(settingsPath(configDir), null);
302
297
  const enabled = settings?.enabledPlugins?.[QUALIFIED] === true;
303
298
  const registered = Object.hasOwn(
304
299
  (await readJson(installedPluginsPath(configDir), { plugins: {} })).plugins ?? {},
305
300
  QUALIFIED);
306
- const channelConfigured = enabled && registered
307
- && (await readJson(mcpPath(sourceDir(configDir)), null))?.mcpServers?.["acc-channel"] != null;
308
301
  return { ok: true, changes: [],
309
- needsAction: channelConfigured ? [CHANNEL_ACTIVATION_CHECK] : [],
302
+ inboundDelivery: await inboundStatus({ configDir, managedSettingsPath }),
303
+ needsAction: [],
310
304
  diagnostics: [enabled && registered
311
305
  ? "acc plugin registered and enabled"
312
306
  : "acc plugin not registered"] };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-codex",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Coordinate this Gemini CLI session with other AI agent sessions working in the same workspace.",
5
5
  "contextFileName": "skills/acc/SKILL.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-grok",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -4,7 +4,7 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
4
4
  // so the four cannot drift: a consumer that has to ask which adapter produced an
5
5
  // event is a consumer that will eventually get it wrong.
6
6
  export const NORMALIZED_EVENT_KEYS = Object.freeze(["kind", "sessionId", "cwd", "model",
7
- "parentSessionId", "tool", "targets"]);
7
+ "parentSessionId", "tool", "targets", "permissionMode", "endReason"]);
8
8
 
9
9
  export const EVENT_KINDS = Object.freeze(["sessionStart", "sessionEnd", "heartbeat",
10
10
  "beforeTurn", "beforeTool", "afterTool", "turnEnd", "childStart", "childEnd", "other"]);
@@ -34,7 +34,7 @@ export function normalizedEvent(fields) {
34
34
  }
35
35
  }
36
36
  const { kind, sessionId, cwd, model = null, parentSessionId = null, tool = null,
37
- targets = [] } = fields;
37
+ targets = [], permissionMode = null, endReason = null } = fields;
38
38
 
39
39
  if (!EVENT_KINDS.includes(kind)) data("unknown normalised event kind", { kind });
40
40
  if (typeof sessionId !== "string" || sessionId === "") {
@@ -50,6 +50,19 @@ export function normalizedEvent(fields) {
50
50
  }
51
51
  }
52
52
 
53
+ // The client's own name for the session's permission mode, when its hook
54
+ // reports one. Only a native binding reads it, to tell a sender whether the
55
+ // client will hold a wake for approval.
56
+ if (permissionMode !== null && (typeof permissionMode !== "string" || permissionMode === "")) {
57
+ data("normalised event permission mode must be a non-empty string", { kind });
58
+ }
59
+
60
+ // Why the client ended the session, in its own words, when it says. A sender
61
+ // is told a conversation was cleared rather than that its peer vanished.
62
+ if (endReason !== null && (typeof endReason !== "string" || endReason === "")) {
63
+ data("normalised event end reason must be a non-empty string", { kind });
64
+ }
65
+
53
66
  return Object.freeze({ kind, sessionId, cwd, model, parentSessionId, tool,
54
- targets: Object.freeze([...targets]) });
67
+ targets: Object.freeze([...targets]), permissionMode, endReason });
55
68
  }
@@ -58,8 +58,6 @@ const ownBinary = name => {
58
58
  };
59
59
 
60
60
  export const defaultRunner = () => ownBinary("acc-hook.mjs");
61
- export const defaultBootstrap = () => ownBinary("acc-bootstrap.mjs");
62
- export const defaultChannel = () => ownBinary("acc-claude-channel.mjs");
63
61
  export const defaultAntigravityRelay = () => ownBinary("acc-antigravity-relay.mjs");
64
62
 
65
63
  export const runnerExists = async runner => {
@@ -199,7 +197,10 @@ export async function writeCliShim({ dir, cli = defaultCli(), node = process.exe
199
197
  * created, and a recognised path is not a reason to delete it.
200
198
  */
201
199
  export async function removeInstalledTree(target, keep = []) {
202
- if (keep.includes(target)) return false;
200
+ // A kept tree holds someone's own work. Removing a directory inside it, or one
201
+ // that contains it, would delete that work as surely as removing it.
202
+ const within = (inner, outer) => inner === outer || inner.startsWith(`${outer}${path.sep}`);
203
+ if (keep.some(kept => within(target, kept) || within(kept, target))) return false;
203
204
  try {
204
205
  await lstat(target);
205
206
  } catch (error) {
@@ -8,7 +8,7 @@ export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE
8
8
  validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
9
9
  from "./native-delivery.mjs";
10
10
  export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
11
- export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultBootstrap, defaultChannel, defaultCli,
11
+ export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli,
12
12
  defaultRunner, removeInstalledTree, runnerExists, writeCliShim, writeHookShim }
13
13
  from "./hook-shim.mjs";
14
14
  export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { COMMAND_NAME, IDENTIFIER, NATIVE_ACTIVATION_KINDS, SHELL_SOURCE, assertReasonCode,
3
+ import { IDENTIFIER, NATIVE_ACTIVATION_KINDS, SHELL_SOURCE, assertReasonCode,
4
4
  closed, deepFreeze, isPlainObject, isText, usage } from "./native-vocabulary.mjs";
5
5
 
6
6
  // A native activation plan is what an adapter hands the installer: closed
@@ -27,19 +27,9 @@ function validateMechanism(mechanism, index) {
27
27
  usage(`${label} kind must be one of ${NATIVE_ACTIVATION_KINDS.join(", ")}`);
28
28
  }
29
29
  if (mechanism.kind === "shell-bootstrap") {
30
- closed(mechanism, ["kind", "command", "realExecutable", "prefixArgs"], label);
31
- if (!isText(mechanism.command) || !COMMAND_NAME.test(mechanism.command)) {
32
- usage(`${label} command must be a bare command name`);
33
- }
34
- if (!isText(mechanism.realExecutable) || !path.isAbsolute(mechanism.realExecutable)) {
35
- usage(`${label} realExecutable must be an absolute path`);
36
- }
37
- if (!Array.isArray(mechanism.prefixArgs) || mechanism.prefixArgs.some(arg => !isText(arg)
38
- || SHELL_SOURCE.test(arg))) {
39
- usage(`${label} prefixArgs must be plain argument strings, never shell source`);
40
- }
41
- return { kind: mechanism.kind, command: mechanism.command,
42
- realExecutable: mechanism.realExecutable, prefixArgs: [...mechanism.prefixArgs] };
30
+ // ACC 0.7.x put a `claude` shim on PATH this way. Install records of it are
31
+ // still read so it can be retired; a plan can no longer create one.
32
+ usage(`${label}: shell-bootstrap activation was removed; no adapter may plan one`);
43
33
  }
44
34
  if (mechanism.kind === "native-config") {
45
35
  closed(mechanism, ["kind", "artifactIds"], label);
@@ -1,5 +1,5 @@
1
- import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_ACTIVATION_KINDS,
2
- NATIVE_BINDING_MODES, NATIVE_PLATFORMS, PROBE_KEYS, TIMESTAMP, assertModes,
1
+ import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_BINDING_MODES,
2
+ NATIVE_PLATFORMS, PLANNABLE_ACTIVATION_KINDS, PROBE_KEYS, TIMESTAMP, assertModes,
3
3
  assertReasonCode, closed, compareStableVersions, deepFreeze, isPlainObject, isText,
4
4
  parseStableVersion, usage } from "./native-vocabulary.mjs";
5
5
 
@@ -12,19 +12,31 @@ import { CONTRACT_ID, FINGERPRINT, HANDSHAKE_KEYS, KNOWN_BAD_REASON, NATIVE_ACTI
12
12
  // capability; this rule is used for native live delivery alone.
13
13
 
14
14
  export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE_REASON_CODES,
15
- compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
15
+ PLANNABLE_ACTIVATION_KINDS, compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
16
16
  export { validateNativeActivationPlan } from "./native-activation.mjs";
17
17
 
18
18
  const orderedModes = modes => NATIVE_BINDING_MODES.filter(mode => modes.includes(mode));
19
19
 
20
20
  export function validateNativeDeliveryContract(value, { certification, client }) {
21
+ // Consent to live delivery is what `acc install` recorded. Up to 0.7.x a
22
+ // Claude shell shim could export it instead ("bootstrap-environment"); the
23
+ // shim is gone, and the installation record is the one source left.
21
24
  const policySource = Object.hasOwn(value ?? {}, "policySource")
22
- ? value.policySource : "bootstrap-environment";
23
- closed({ ...value, policySource },
24
- ["minimumByPlatform", "anchors", "knownBad", "activationKinds", "policySource"],
25
+ ? value.policySource : "installation-record";
26
+ // What a successful offer put in front of the model. "message" carries the
27
+ // body, so the router records the receipt as offered. "wake" carries only a
28
+ // notice that makes the client run a turn; the body reaches the model through
29
+ // the next-turn hook, which records the offer itself once its stdout carried
30
+ // the body. Recording a wake as offered would hide the body from that hook.
31
+ const offerKind = Object.hasOwn(value ?? {}, "offerKind") ? value.offerKind : "message";
32
+ closed({ ...value, policySource, offerKind },
33
+ ["minimumByPlatform", "anchors", "knownBad", "activationKinds", "policySource", "offerKind"],
25
34
  "nativeDelivery");
26
- if (!["installation-record", "bootstrap-environment"].includes(policySource)) {
27
- usage("nativeDelivery.policySource must be installation-record or bootstrap-environment");
35
+ if (policySource !== "installation-record") {
36
+ usage("nativeDelivery.policySource must be installation-record");
37
+ }
38
+ if (!["message", "wake"].includes(offerKind)) {
39
+ usage("nativeDelivery.offerKind must be message or wake");
28
40
  }
29
41
  const minimums = value.minimumByPlatform;
30
42
  if (!isPlainObject(minimums) || Object.keys(minimums).length === 0) {
@@ -89,11 +101,11 @@ export function validateNativeDeliveryContract(value, { certification, client })
89
101
  });
90
102
  const kinds = value.activationKinds;
91
103
  if (!Array.isArray(kinds) || kinds.length === 0 || new Set(kinds).size !== kinds.length
92
- || kinds.some(kind => !NATIVE_ACTIVATION_KINDS.includes(kind))) {
93
- usage(`nativeDelivery.activationKinds must be unique entries of ${NATIVE_ACTIVATION_KINDS.join(", ")}`);
104
+ || kinds.some(kind => !PLANNABLE_ACTIVATION_KINDS.includes(kind))) {
105
+ usage(`nativeDelivery.activationKinds must be unique entries of ${PLANNABLE_ACTIVATION_KINDS.join(", ")}`);
94
106
  }
95
107
  return deepFreeze({ minimumByPlatform: { ...minimums }, anchors, knownBad,
96
- activationKinds: [...kinds], policySource });
108
+ activationKinds: [...kinds], policySource, offerKind });
97
109
  }
98
110
 
99
111
  function knownBadHit(contract, version) {
@@ -9,10 +9,15 @@ export const NATIVE_BINDING_MODES = Object.freeze([
9
9
  "livePush", "idleWake", "busyQueue", "replyRoute",
10
10
  ]);
11
11
 
12
+ // Every kind an install record may carry. "shell-bootstrap" is the `claude`
13
+ // shim ACC 0.7.x wrote; records of it stay readable so it can be retired.
12
14
  export const NATIVE_ACTIVATION_KINDS = Object.freeze([
13
15
  "shell-bootstrap", "native-config", "native-service",
14
16
  ]);
15
17
 
18
+ // The kinds an adapter may still ask for.
19
+ export const PLANNABLE_ACTIVATION_KINDS = Object.freeze(["native-config", "native-service"]);
20
+
16
21
  export const NATIVE_PLATFORMS = Object.freeze([
17
22
  "darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-x64",
18
23
  ]);
@@ -34,7 +39,6 @@ export const NATIVE_REASON_CODES = Object.freeze([
34
39
  export const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z.-]+)?$/;
35
40
  export const CONTRACT_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
36
41
  export const IDENTIFIER = /^[a-z][a-z0-9_-]*$/;
37
- export const COMMAND_NAME = /^[a-z][a-z0-9_.-]*$/;
38
42
  export const FINGERPRINT = /^sha256:[0-9a-f]{64}$/;
39
43
  export const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
40
44
  export const SHELL_SOURCE = /[;&|<>`\n\0]|\$\(/;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,7 +2,7 @@ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
 
5
- import { describeDeliveryFallback, detectInstallation, livePolicyOf, loadOwnership, shellOf, verifyOwned }
5
+ import { describeDeliveryFallback, detectInstallation, livePolicyOf, loadOwnership, verifyOwned }
6
6
  from "@agents-can-communicate/installer";
7
7
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
8
8
 
@@ -114,12 +114,9 @@ export async function diagnoseAdapters({ options, runtime, detect = detectInstal
114
114
  const home = options?.home ?? runtime?.env?.HOME ?? homedir();
115
115
  const { data: dataHome } = platformPaths({ platform: runtime?.platform,
116
116
  env: runtime?.env ?? {} });
117
- // The same shell and environment install reads, so detection plans the same
118
- // shell bootstrap it would apply. Omitting them left detection with a null
119
- // shell, which degrades every shell-bootstrap client to `unsupported_shell` -
120
- // so a zsh machine with a working shim read as degraded on every run.
117
+ // The same environment install reads, so detection probes the same client.
121
118
  const clients = clientContext(home, path.join(dataHome, "acc"),
122
- { shell: shellOf(runtime?.env ?? {}), env: runtime?.env ?? {} });
119
+ { env: runtime?.env ?? {} });
123
120
  const adapters = ALL_ADAPTERS();
124
121
  const detected = await detect({ adapters, context: clients,
125
122
  probeTimeoutMs: probeTimeout(runtime?.env) });
@@ -297,6 +294,11 @@ export async function runDoctor({ options, context, runtime }) {
297
294
  ...nativeSessionLines(adapters),
298
295
  ...adapters.filter(adapter => adapter.present && adapter.outgoingDelivery?.state === "configured")
299
296
  .map(adapter => ` ${adapter.displayName} ${adapter.outgoingDelivery.diagnostic}`),
297
+ // The client's own inbound control decides whether a wake reaches the model.
298
+ // Only someone who turned live delivery on needs to hear about it.
299
+ ...adapters.filter(adapter => adapter.present && adapter.nativeDelivery.configured
300
+ && typeof adapter.inboundDelivery?.diagnostic === "string")
301
+ .map(adapter => ` ${adapter.displayName} inbound: ${adapter.inboundDelivery.diagnostic}`),
300
302
  ...(manager === null ? [] : [` automatic updates ${manager.auto ? "on" : "off"}; ACC ${manager.active.version}`
301
303
  + (manager.pin ? `; pinned to ${manager.pin}` : ""), ...(update.notice ? [` ${update.notice}`] : [])]),
302
304
  ...data.remediation.map(line => ` ${line}`),
@@ -7,8 +7,7 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
7
7
  import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
8
8
  import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
9
9
  import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
10
- import { applyPlan, detectInstallation,
11
- loadOwnership, planInstallation, rcFileFor, shellOf, shimDirFor }
10
+ import { applyPlan, detectInstallation, loadOwnership, planInstallation }
12
11
  from "@agents-can-communicate/installer";
13
12
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
14
13
 
@@ -29,7 +28,7 @@ export { decideDelivery } from "./install-delivery-consent.mjs";
29
28
  // pointed at the home itself writes beside them rather than inside them. That
30
29
  // install reports success and the client never reads a byte of it.
31
30
  export const clientContext = (home, stateRoot,
32
- { shell = null, env = {}, dataHome, cwd = process.cwd() } = {}) => ({
31
+ { env = {}, dataHome, cwd = process.cwd() } = {}) => ({
33
32
  home,
34
33
  ...(dataHome === undefined ? {} : { dataHome }),
35
34
  configDir: path.join(home, ".claude"),
@@ -39,10 +38,8 @@ export const clientContext = (home, stateRoot,
39
38
  kimiHome: path.join(home, ".kimi-code"),
40
39
  grokHome: typeof env.GROK_HOME === "string" && env.GROK_HOME !== ""
41
40
  ? env.GROK_HOME : path.join(home, ".grok"),
42
- // The user's login shell and PATH, for the optional native shell bootstrap:
43
- // which rc file could carry an ACC PATH block and which real executable a
44
- // shim would exec. Detection reads them; nothing here writes.
45
- shell,
41
+ // The user's PATH, for finding the real client executable to probe.
42
+ // Detection reads it; nothing here writes.
46
43
  env,
47
44
  // Antigravity CLI keeps its hooks in the `~/.gemini` tree Gemini CLI also
48
45
  // uses, and reads none of the same files. Two locations load - the
@@ -200,8 +197,7 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
200
197
  export function failureOf({ action, acted, failed = [], operations = [], skipped = [], home }) {
201
198
  if (failed.length === 0) return null;
202
199
  return new AccError(EXIT.DATA,
203
- [describeOutcome({ action, acted, failed, operations, skipped, home }),
204
- ...reloadAdvice(operations)].join("\n"), { failed });
200
+ describeOutcome({ action, acted, failed, operations, skipped, home }), { failed });
205
201
  }
206
202
 
207
203
  /**
@@ -220,23 +216,13 @@ export function actedOn(result) {
220
216
  + (operation.changes?.length ?? 0) > 0)).length;
221
217
  }
222
218
 
223
- // Said once, after the first PATH block is written: a running shell and a
224
- // running client know nothing about it.
225
- function reloadAdvice(operations) {
226
- const appended = operations.filter(operation => operation.appendedRcBlock === true);
227
- if (appended.length === 0) return [];
228
- const rc = appended[0].nativeActivation?.rcFile ?? "your shell rc file";
229
- return ["", `native delivery is wired for new sessions: open a new terminal (or reload ${rc}), `
230
- + "then start the client normally; a session already running keeps durable delivery"];
231
- }
232
-
233
219
  export async function runInstallCommand({ options, runtime, action = "install" }) {
234
220
  const adapters = selectAdapters(options.adapter);
235
221
  const home = options.home ?? runtime.env?.HOME ?? homedir();
236
222
  const { data: dataHome } = platformPaths({ platform: runtime.platform,
237
223
  env: runtime.env ?? {} });
238
224
  const context = clientContext(home, path.join(dataHome, "acc"),
239
- { shell: shellOf(runtime.env), env: runtime.env ?? {}, dataHome });
225
+ { env: runtime.env ?? {}, dataHome });
240
226
 
241
227
  const detected = await detectInstallation({ adapters, context,
242
228
  probeTimeoutMs: probeTimeout(runtime.env) });
@@ -273,7 +259,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
273
259
  deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter,
274
260
  allowServiceSetup: action === "install" });
275
261
  const apply = (paths = {}) => applyPlan({ plan, adapters, context: { ...context, ...paths },
276
- dataHome, dryRun, accVersion, activation: { bootstrap: paths.bootstrap } });
262
+ dataHome, dryRun, accVersion });
277
263
  const result = action === "install" && !dryRun && runtime.packageRoot
278
264
  ? await installManaged({ packageRoot: runtime.packageRoot,
279
265
  managerRoot: path.join(dataHome, "acc", "runtime"), dataHome, home, cwd: options.cwd ?? runtime.cwd,
@@ -293,9 +279,8 @@ export async function runInstallCommand({ options, runtime, action = "install" }
293
279
 
294
280
  return { data: { ...result, plan, dataHome, deliveryByAdapter: decided.deliveryByAdapter,
295
281
  deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter, asked: decided.asked },
296
- text: [describeOutcome({ action, acted, failed: result.failed,
282
+ text: describeOutcome({ action, acted, failed: result.failed,
297
283
  skipped: plan.skipped, operations: result.operations, home }),
298
- ...reloadAdvice(result.operations)].join("\n"),
299
284
  error: failureOf({ action, acted, failed: result.failed, operations: result.operations,
300
285
  skipped: plan.skipped, home }) };
301
286
  }
@@ -57,9 +57,6 @@ function questionFor(entries) {
57
57
  const names = entries.map(entry => entry.displayName ?? entry.adapterId).join(", ");
58
58
  const setup = [...new Set(entries.map(entry => entry.outgoingDelivery?.setup)
59
59
  .filter(value => typeof value === "string"))];
60
- const channels = entries.some(entry => (entry.nativeDelivery?.activationPlan?.mechanisms ?? [])
61
- .some(mechanism => mechanism.kind === "shell-bootstrap"
62
- && (mechanism.prefixArgs ?? []).some(argument => argument.startsWith("--"))));
63
60
  return [
64
61
  `Complete automatic peer-request setup for ${names}? (experimental)`,
65
62
  " Yes: automatic turns can spend tokens without waiting for you.",
@@ -69,7 +66,9 @@ function questionFor(entries) {
69
66
  ...entries.filter(needsPrerequisite).map(entry =>
70
67
  ` Download the official Codex ${entry.nativeServiceSetup.cliVersion} standalone package for its service; retain your existing codex command.`),
71
68
  ...setup.map(note => ` ${note}`),
72
- ...(channels ? [" Allow Claude Code development Channels when prompted at startup."] : []),
69
+ ...(entries.some(entry => entry.adapterId === "claude_code")
70
+ ? [" Claude Code sessions that bypass permission prompts ask before each ACC wake;",
71
+ " set Claude Code's crossSessionInbound to accept to take wakes without asking."] : []),
73
72
  " No: keep current delivery policies and use available next-turn hooks or acc inbox.",
74
73
  ].join("\n");
75
74
  }
@@ -138,13 +138,26 @@ function obligationFor(kind, explicit, addressed) {
138
138
  return obligation;
139
139
  }
140
140
 
141
- function recordedText(message, delivery) {
141
+ export function recordedText(message, delivery) {
142
142
  const diagnostics = delivery.map(item => item.outcome === "offered"
143
143
  ? `live offered to ${item.recipientParticipantId} via ${item.transport}`
144
+ : item.outcome === "woken" && item.pendingApproval === true
145
+ ? `sent a wake to ${item.recipientParticipantId} via ${item.transport}, which its session holds `
146
+ + "for approval because it bypasses permission prompts; the message arrives with its next turn"
147
+ : item.outcome === "woken"
148
+ ? `woke ${item.recipientParticipantId} via ${item.transport}; the message arrives with its next turn`
144
149
  : item.outcome === "queued"
145
150
  ? item.errorCode === "transport_permission_denied"
146
151
  ? "live offer blocked by sender permissions; message remains queued; run acc doctor in the sending session"
147
- : `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
152
+ : item.errorCode === "no_live_transport"
153
+ ? `${item.recipientParticipantId} has no live transport; the message waits in its inbox`
154
+ : item.errorCode === "recipient_offline" && item.endReason === "clear"
155
+ ? `${item.recipientParticipantId}'s conversation was cleared (/clear); the message waits `
156
+ + "until that conversation resumes"
157
+ : item.errorCode === "recipient_offline"
158
+ ? `${item.recipientParticipantId} has no open session; the message waits until it starts `
159
+ + "or resumes one"
160
+ : `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
148
161
  : `delivery already ${item.outcome}`);
149
162
  return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
150
163
  }
@@ -185,7 +198,16 @@ export function describeStatus(status) {
185
198
  // A transport accepting a message is not a model reading it. Said only when
186
199
  // it is true of something, so the everyday line stays the everyday line.
187
200
  const offered = status.counts.unretrieved?.offered ?? 0;
188
- return offered === 0 ? line : `${line}; ${offered} offered, not retrieved`;
201
+ const withOffers = offered === 0 ? line : `${line}; ${offered} offered, not retrieved`;
202
+ // Status lists a participant with no open session only while messages wait
203
+ // for it; each is named here, with the client's reason when it gave one.
204
+ const waiting = (status.participants ?? []).filter(item => item.presence === "offline")
205
+ .map(item => {
206
+ const count = (item.unretrieved?.queued ?? 0) + (item.unretrieved?.offered ?? 0);
207
+ return `${item.participantId} (${item.endReason === "clear" ? "cleared, " : ""}${count})`;
208
+ });
209
+ return waiting.length === 0 ? withOffers
210
+ : `${withOffers}; waiting for a closed session: ${waiting.join(", ")}`;
189
211
  }
190
212
 
191
213
  const HANDLERS = Object.freeze({