agents-can-communicate 0.5.9 → 0.6.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 (80) hide show
  1. package/README.md +2 -1
  2. package/bin/acc-antigravity-relay.mjs +7 -0
  3. package/bin/entrypoints/acc-antigravity-relay.mjs +116 -0
  4. package/bin/entrypoints/acc-bootstrap.mjs +4 -0
  5. package/bin/entrypoints/acc-hook.mjs +11 -7
  6. package/bin/entrypoints/antigravity-relay-binding.mjs +43 -0
  7. package/docs/ADAPTER_AUTHORING.md +33 -0
  8. package/docs/ARCHITECTURE.md +27 -4
  9. package/docs/CAPABILITIES.md +22 -16
  10. package/docs/CLI.md +21 -3
  11. package/docs/CONCEPTS.md +7 -0
  12. package/docs/CONFIGURATION.md +1 -0
  13. package/docs/GETTING_STARTED.md +10 -4
  14. package/docs/HOW_IT_WORKS.md +7 -1
  15. package/docs/TROUBLESHOOTING.md +99 -0
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +91 -0
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/PreInvocation-1.2.7.json +11 -0
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/SessionStart-1.2.7.json +9 -0
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-error-answers-1.2.7.json +19 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-live-push-1.2.7.json +31 -0
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +217 -0
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product-evidence.json +132 -0
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product.json +29 -0
  24. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/hooks-first-trust-no-workspace-1.2.7.json +37 -0
  25. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/live-push-surfaces-1.2.7.json +38 -0
  26. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +28 -0
  27. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +5 -0
  28. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +329 -0
  29. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +119 -0
  30. package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +98 -0
  31. package/node_modules/@agents-can-communicate/adapter-antigravity/src/hooks.mjs +201 -0
  32. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +708 -0
  33. package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +170 -0
  34. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +116 -0
  35. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +123 -0
  36. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +195 -0
  37. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +64 -0
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +14 -2
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +2 -1
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +5 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  43. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +14 -2
  44. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  45. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +14 -2
  46. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  47. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  48. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +14 -2
  49. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  50. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +14 -2
  51. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  52. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +37 -1
  53. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +25 -5
  54. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
  55. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
  56. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +1 -1
  57. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  58. package/node_modules/@agents-can-communicate/cli/src/args.mjs +6 -2
  59. package/node_modules/@agents-can-communicate/cli/src/help.mjs +1 -1
  60. package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +118 -0
  61. package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
  62. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +17 -3
  63. package/node_modules/@agents-can-communicate/cli/src/main.mjs +8 -4
  64. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command-prefix.mjs +26 -0
  65. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +6 -4
  66. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +3 -1
  67. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +27 -6
  68. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  69. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  70. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  71. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +31 -0
  72. package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +16 -3
  73. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +70 -17
  74. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  75. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +4 -0
  76. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -0
  77. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  78. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  79. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  80. package/package.json +3 -1
@@ -1,6 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import path from "node:path";
3
3
 
4
+ import { createAntigravityAdapter } from "@agents-can-communicate/adapter-antigravity";
4
5
  import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
5
6
  import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
6
7
  import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
@@ -27,7 +28,8 @@ export { decideDelivery } from "./install-delivery-consent.mjs";
27
28
  // Each client keeps its own directory under the user's home, and an adapter
28
29
  // pointed at the home itself writes beside them rather than inside them. That
29
30
  // install reports success and the client never reads a byte of it.
30
- export const clientContext = (home, stateRoot, { shell = null, env = {}, dataHome } = {}) => ({
31
+ export const clientContext = (home, stateRoot,
32
+ { shell = null, env = {}, dataHome, cwd = process.cwd() } = {}) => ({
31
33
  home,
32
34
  ...(dataHome === undefined ? {} : { dataHome }),
33
35
  configDir: path.join(home, ".claude"),
@@ -42,6 +44,18 @@ export const clientContext = (home, stateRoot, { shell = null, env = {}, dataHom
42
44
  // shim would exec. Detection reads them; nothing here writes.
43
45
  shell,
44
46
  env,
47
+ // Antigravity CLI keeps its hooks in the `~/.gemini` tree Gemini CLI also
48
+ // uses, and reads none of the same files. Two locations load - the
49
+ // machine-wide `~/.gemini/config/hooks.json` and a project's own
50
+ // `.agents/hooks.json`, the second only while that project is an open
51
+ // workspace. Global is the default (issue #178), because a workspace
52
+ // registration in a folder nobody opened as one registers nothing and says
53
+ // nothing. ACC_ANTIGRAVITY_HOOKS is carried through exactly as it was set,
54
+ // wrong values included, so the adapter can refuse one by name rather than
55
+ // quietly installing somewhere else.
56
+ antigravityWorkspace: cwd,
57
+ antigravityHookLocation: typeof env.ACC_ANTIGRAVITY_HOOKS === "string"
58
+ && env.ACC_ANTIGRAVITY_HOOKS !== "" ? env.ACC_ANTIGRAVITY_HOOKS : "global",
45
59
  // Where ACC keeps its own state, for the client that has to be told. Codex
46
60
  // sandboxes the commands a model runs to the workspace, and ACC's state is
47
61
  // outside every workspace on purpose - so an agent there could read the
@@ -50,8 +64,8 @@ export const clientContext = (home, stateRoot, { shell = null, env = {}, dataHom
50
64
  stateRoot,
51
65
  });
52
66
 
53
- export const ALL_ADAPTERS = () => [createClaudeCodeAdapter(), createCodexAdapter(),
54
- createGeminiCliAdapter(), createGrokAdapter(), createKimiAdapter()];
67
+ export const ALL_ADAPTERS = () => [createAntigravityAdapter(), createClaudeCodeAdapter(),
68
+ createCodexAdapter(), createGeminiCliAdapter(), createGrokAdapter(), createKimiAdapter()];
55
69
 
56
70
  /**
57
71
  * How long to wait for a client to say its version.
@@ -24,6 +24,7 @@ import { canonicalClaim } from "./claim-spelling.mjs";
24
24
  import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
25
25
  import { resolveOwner } from "./session-owner.mjs";
26
26
  import { discoverWorkspace } from "./workspace-discovery.mjs";
27
+ import { resolveSavedHookWorkspace } from "./hook-workspace.mjs";
27
28
 
28
29
  const DEFAULT_CADENCE_MS = 30_000;
29
30
 
@@ -68,14 +69,17 @@ async function claimOn(options, context) {
68
69
  * looking is safe.
69
70
  */
70
71
  async function locateContext(options, runtime) {
71
- const descriptor = await discoverWorkspace({
72
+ const dataHome = runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
73
+ env: runtime.env });
74
+ const discovery = {
72
75
  cwd: options.cwd ?? runtime.cwd,
73
76
  env: runtime.env,
74
77
  gitProbe: runtime.gitProbe ?? createGitProbe(),
75
78
  explicitConfig: options.workspace,
76
- });
77
- const dataHome = runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
78
- env: runtime.env });
79
+ };
80
+ const saved = await resolveSavedHookWorkspace({ ...discovery, dataHome,
81
+ reference: options.workspace });
82
+ const descriptor = saved?.descriptor ?? await discoverWorkspace(discovery);
79
83
  const paths = runtimePaths({
80
84
  dataHome,
81
85
  workspaceId: descriptor.id,
@@ -0,0 +1,26 @@
1
+ // Shared with CLI parsing, but dependency-free: immutable launchers cannot load
2
+ // workspace modules before runtime admission. Consume values, never search for
3
+ // command words that might appear inside a path or a message.
4
+ export const GLOBAL_OPTIONS = Object.freeze(["json", "workspace", "cwd"]);
5
+
6
+ export function commandPrefix(argv) {
7
+ const leading = [];
8
+ let offset = 0;
9
+ while (offset < argv.length) {
10
+ const token = argv[offset];
11
+ const name = token.startsWith("--") ? token.slice(2).split("=", 1)[0] : "";
12
+ if (!GLOBAL_OPTIONS.includes(name)) break;
13
+ leading.push(token);
14
+ offset += 1;
15
+ if (name !== "json" && !token.includes("=")) {
16
+ const value = argv[offset];
17
+ if (value === undefined || value.startsWith("--")
18
+ && GLOBAL_OPTIONS.includes(value.slice(2).split("=", 1)[0])) {
19
+ return { error: `option --${name} requires a value` };
20
+ }
21
+ leading.push(value);
22
+ offset += 1;
23
+ }
24
+ }
25
+ return { command: argv[offset], leading, rest: argv.slice(offset + 1) };
26
+ }
@@ -4,9 +4,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import { scheduleWorker } from "./schedule.mjs";
5
5
  import { acquireRuntime } from "./leases.mjs";
6
6
  import { canonicalManagerRoot, readControl, readManagedJson } from "./state.mjs";
7
+ import { commandPrefix } from "./command-prefix.mjs";
7
8
 
8
9
  export const ENTRY_KINDS = Object.freeze([
9
- "acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel",
10
+ "acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel", "acc-antigravity-relay",
10
11
  ]);
11
12
 
12
13
  const BOOTSTRAP_FLAGS = new Map([["--adapter", "adapter"], ["--real-executable", "realExecutable"],
@@ -42,7 +43,7 @@ export function invokedDirectly(url) {
42
43
 
43
44
  function unavailable(kind) {
44
45
  // Never reflect control file content or payloads into a client diagnostic.
45
- if (kind === "acc-hook" || kind === "acc-claude-channel") {
46
+ if (kind === "acc-hook" || kind === "acc-claude-channel" || kind === "acc-antigravity-relay") {
46
47
  process.stderr.write("acc: coordination unavailable during runtime update; session may continue\n");
47
48
  process.exitCode = 0;
48
49
  } else if (kind === "acc-bootstrap") {
@@ -81,6 +82,7 @@ async function updateImplementation(packageRoot, control) {
81
82
  /** The lease lasts until OS process death, including callbacks after main returns. */
82
83
  export async function runEntry({ kind, packageRoot, managerRoot, managedRequired = false }) {
83
84
  if (!ENTRY_KINDS.includes(kind)) throw new Error("unknown ACC entry point");
85
+ const command = kind === "acc" ? commandPrefix(process.argv.slice(2)).command : null;
84
86
  const bootstrapOptions = kind === "acc-bootstrap" ? parseBootstrapOptions(process.argv.slice(2)) : null;
85
87
  if (kind === "acc-bootstrap" && bootstrapOptions === null) {
86
88
  if (process.env.ACC_BOOTSTRAP_DEBUG === "1") {
@@ -107,7 +109,7 @@ export async function runEntry({ kind, packageRoot, managerRoot, managedRequired
107
109
  if (control !== null) {
108
110
  selected = control.active.root;
109
111
  managed = root;
110
- const update = kind === "acc" && ["update", "doctor"].includes(process.argv[2])
112
+ const update = kind === "acc" && ["update", "doctor"].includes(command)
111
113
  ? await updateImplementation(packageRoot, control) : null;
112
114
  if (update !== null) {
113
115
  selected = update;
@@ -116,7 +118,7 @@ export async function runEntry({ kind, packageRoot, managerRoot, managedRequired
116
118
  const lease = await acquireRuntime(root, { pid: process.pid, kind });
117
119
  selected = lease.runtime.root;
118
120
  const quiet = kind === "acc" && ["update", "install", "uninstall", "help", "version",
119
- "--help", "-h", "--version", "-v", "-V"].includes(process.argv[2]);
121
+ "--help", "-h", "--version", "-v", "-V"].includes(command);
120
122
  if (!quiet) await scheduleWorker(root, control);
121
123
  }
122
124
  } else if (managedRequired) throw new Error("managed runtime is not initialized");
@@ -4,7 +4,8 @@ import path from "node:path";
4
4
  import { ENTRY_KINDS } from "./entry.mjs";
5
5
  import { managedDirectory, syncDirectory } from "./state.mjs";
6
6
 
7
- const MODULES = ["entry.mjs", "state.mjs", "generation-files.mjs", "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
7
+ const MODULES = ["entry.mjs", "command-prefix.mjs", "state.mjs", "generation-files.mjs",
8
+ "mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
8
9
  async function durableFile(file, bytes, mode = 0o600) {
9
10
  const handle = await open(file, "wx", mode);
10
11
  try { await handle.writeFile(bytes); await handle.sync(); }
@@ -15,6 +16,7 @@ export const stablePaths = root => ({
15
16
  cli: path.join(root, "bin", "acc.mjs"), runner: path.join(root, "bin", "acc-hook.mjs"),
16
17
  bootstrap: path.join(root, "bin", "acc-bootstrap.mjs"),
17
18
  channel: path.join(root, "bin", "acc-claude-channel.mjs"),
19
+ antigravityRelay: path.join(root, "bin", "acc-antigravity-relay.mjs"),
18
20
  });
19
21
 
20
22
  /** Publish immutable launcher modules, then atomically replace each tiny entry.
@@ -15,6 +15,23 @@ const unresolved = command => new AccError(EXIT.USAGE,
15
15
  + "session IDs and a shared checkout do not establish CLI ownership.",
16
16
  { command, reasonCode: "caller_identity_unresolved" });
17
17
 
18
+ async function observationOwner(options, context) {
19
+ if (options.session === undefined) return null;
20
+ const existing = await context.service.locateSession(options.session);
21
+ if (existing === null) {
22
+ throw new AccError(EXIT.CONFLICT,
23
+ "the selected session is absent from this workspace; use --cwd from the ACC hook header "
24
+ + "or the directory where your manual session attached. Do not attach a replacement to recover context.",
25
+ { reasonCode: "caller_workspace_mismatch", sessionId: options.session,
26
+ workspaceId: context.descriptor.id });
27
+ }
28
+ if (options.generation !== undefined && existing.record.generation !== options.generation) {
29
+ throw new AccError(EXIT.CONFLICT, "cannot observe as a replaced session generation",
30
+ { reasonCode: "caller_generation_mismatch", sessionId: options.session });
31
+ }
32
+ return existing.record;
33
+ }
34
+
18
35
  /**
19
36
  * Accept caller-supplied credentials; never discover credentials from peers.
20
37
  *
@@ -30,22 +47,26 @@ export async function resolveOwner({ command, options, context, env = {} }) {
30
47
  const explicit = options.session !== undefined && options.generation !== undefined;
31
48
  const configured = typeof env.ACC_SESSION === "string" && typeof env.ACC_GENERATION === "string";
32
49
  if (!explicit && !configured) {
33
- if (soft) return options;
50
+ if (soft) {
51
+ await observationOwner(options, context);
52
+ return options;
53
+ }
34
54
  throw unresolved(command);
35
55
  }
36
56
 
37
57
  const owner = explicit ? options : { session: env.ACC_SESSION, generation: env.ACC_GENERATION };
38
58
  if (options.session !== undefined && options.session !== owner.session) {
39
59
  // A public observation scope is allowed; it does not request credentials.
40
- if (soft) return options;
60
+ if (soft) {
61
+ await observationOwner(options, context);
62
+ return options;
63
+ }
41
64
  throw unresolved(command);
42
65
  }
43
66
  // A supplied generation must reach the core unchanged so stale calls fail.
44
67
  const resolved = { ...options, session: owner.session,
45
68
  generation: options.generation ?? owner.generation };
46
- if (command === "status" && options.participant === undefined) {
47
- const { participants } = await context.service.collectStatus({});
48
- resolved.participant = participants.find(item => item.sessionId === owner.session)?.participantId;
49
- }
69
+ const observed = soft ? await observationOwner(resolved, context) : null;
70
+ if (command === "status" && options.participant === undefined) resolved.participant = observed.participantId;
50
71
  return resolved;
51
72
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/delivery-router",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/hook-runner",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -41,3 +41,34 @@ export async function bindNative({ adapter, event, hookBinding, clientVersion, p
41
41
  }
42
42
  return result;
43
43
  }
44
+
45
+ const HINT_MAX_BYTES = 512;
46
+ const HINT_MAX_MS = 250;
47
+
48
+ /**
49
+ * One line an adapter asks the agent to act on, when the agent itself can fix
50
+ * a degraded native binding - Antigravity's relay is started from the agent's
51
+ * own shell, and nothing else can start it. Bounded and fail-open: a slow or
52
+ * failing adapter costs the turn nothing, and anything but one short line is
53
+ * dropped rather than trusted into model context.
54
+ */
55
+ export async function nativeActivationHintFor({ adapter, event, nativeBinding, binding, context,
56
+ paths, deadline }) {
57
+ if (typeof adapter?.nativeActivationHint !== "function" || nativeBinding?.state !== "degraded") return null;
58
+ const budget = Math.min(HINT_MAX_MS, deadline - Date.now() - HINT_MAX_MS);
59
+ if (budget <= 0) return null;
60
+ let timer = null;
61
+ try {
62
+ const line = await Promise.race([
63
+ Promise.resolve(adapter.nativeActivationHint({ event, nativeBinding, runtimeDir: paths.root,
64
+ clientPid: binding?.clientPid, env: context.env })),
65
+ new Promise(resolve => { timer = setTimeout(resolve, budget, null); }),
66
+ ]);
67
+ return typeof line === "string" && line !== "" && !/[\r\n]/.test(line)
68
+ && Buffer.byteLength(line, "utf8") <= HINT_MAX_BYTES ? line : null;
69
+ } catch {
70
+ return null;
71
+ } finally {
72
+ if (timer !== null) clearTimeout(timer);
73
+ }
74
+ }
@@ -1,8 +1,11 @@
1
1
  import { assertPortableId } from "@agents-can-communicate/protocol";
2
2
 
3
- export const ownerHeader = binding => "ACC CLI (append): --session "
3
+ const shellQuote = value => `'${value.replaceAll("'", "'\\''")}'`;
4
+
5
+ export const ownerHeader = (binding, cwd, workspaceRef) => "ACC CLI (append): --session "
4
6
  + assertPortableId(binding.accSessionId, "sessionId") + " --generation "
5
- + assertPortableId(binding.generation, "generation");
7
+ + assertPortableId(binding.generation, "generation") + " --cwd " + shellQuote(cwd)
8
+ + (workspaceRef === undefined ? "" : " --workspace " + shellQuote(workspaceRef));
6
9
 
7
10
  export function ownerOnlyOutcome(inject, owner, budgetBytes) {
8
11
  if (Buffer.byteLength(owner, "utf8") > budgetBytes) {
@@ -20,7 +23,7 @@ export async function appendToolOwner(result, { event, binding, context, adapter
20
23
  const current = await context.service.locateSession(binding.accSessionId, context.descriptor.id);
21
24
  if (current?.record.state !== "open" || current.record.generation !== binding.generation) return result;
22
25
 
23
- const owner = ownerHeader(binding);
26
+ const owner = ownerHeader(binding, context.workspaceCwd, context.workspaceRef);
24
27
  const injected = adapter.injectToolOwnerOutcome({ owner, tool: event.tool });
25
28
  if (injected === null) return result;
26
29
  const fitted = ownerOnlyOutcome(() => injected, owner,
@@ -28,3 +31,13 @@ export async function appendToolOwner(result, { event, binding, context, adapter
28
31
  return { ...result, stdout: fitted.stdout,
29
32
  stderr: [result.stderr, fitted.stderr].filter(Boolean).join("\n") };
30
33
  }
34
+
35
+ // SessionStart can also be a context reset. Return the binding just resumed by
36
+ // the hook; never require the model to remember it or attach another session.
37
+ export function appendStartOwner(result, { event, context, adapter }) {
38
+ if (event.kind !== "sessionStart" || result.accSessionId === undefined
39
+ || typeof adapter.injectStartOwnerOutcome !== "function") return result;
40
+ return { ...result, ...ownerOnlyOutcome(text => adapter.injectStartOwnerOutcome(text),
41
+ ownerHeader(result, context.workspaceCwd, context.workspaceRef),
42
+ context.descriptor.policy?.contextBudgetBytes ?? 6_000) };
43
+ }
@@ -9,15 +9,15 @@ import { clearNativeAttempt, clearSessionBinding, effectiveCapabilities, loadSes
9
9
  import { createCoordinationService } from "@agents-can-communicate/core";
10
10
  import { AccError, createId } from "@agents-can-communicate/protocol";
11
11
  import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
12
- import { clearPin, createGitProbe, discoverWorkspace, platformDataHome, runtimePaths, writePin }
12
+ import { clearPin, createGitProbe, resolveHookWorkspace, platformDataHome, runtimePaths, writePin }
13
13
  from "@agents-can-communicate/cli";
14
14
 
15
15
  import { resolveClientPid } from "./client-pid.mjs";
16
16
  import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
17
- import { bindNative, nativeDiagnosticDeadline } from "./native-attempt.mjs";
17
+ import { bindNative, nativeActivationHintFor, nativeDiagnosticDeadline } from "./native-attempt.mjs";
18
18
  import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
19
19
  import { withSessionLifecycle } from "./session-lifecycle.mjs";
20
- import { appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
20
+ import { appendStartOwner, appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
21
21
 
22
22
  // Kept cohesive above 300 lines because every handler shares one fail-open
23
23
  // hook boundary, binding lifecycle, and client-specific outcome contract.
@@ -211,12 +211,13 @@ async function runtimeFacts(fromUrl) {
211
211
  // unreachable from there.
212
212
  const managerRootFor = dataHome => path.join(dataHome, "acc", "runtime");
213
213
 
214
- async function openContext({ cwd, dataHome, runtime, env, deadline }) {
214
+ async function openContext({ event, adapterId, dataHome, runtime, env, deadline }) {
215
215
  assertHookBudget(deadline);
216
- const descriptor = await discoverWorkspace({ cwd, env: env ?? {},
216
+ const resolvedDataHome = dataHome ?? platformDataHome({ env: env ?? {} });
217
+ const { descriptor, workspaceCwd, workspaceRef } = await resolveHookWorkspace({ adapterId, event,
218
+ dataHome: resolvedDataHome, env: env ?? {}, clock: runtime.clock, deadlineAt: deadline,
217
219
  gitProbe: createGitProbe({ deadlineAt: deadline }) });
218
220
  assertHookBudget(deadline);
219
- const resolvedDataHome = dataHome ?? platformDataHome({ env: env ?? {} });
220
221
  const paths = runtimePaths({
221
222
  dataHome: resolvedDataHome,
222
223
  workspaceId: descriptor.id,
@@ -224,13 +225,17 @@ async function openContext({ cwd, dataHome, runtime, env, deadline }) {
224
225
  });
225
226
  const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
226
227
  ids: runtime.ids, workspaceId: descriptor.id, deadlineAt: deadline });
227
- return { descriptor, paths, dataHome: resolvedDataHome, env: env ?? {}, realpath: runtime.realpath ?? realpath,
228
+ return { descriptor, paths, workspaceCwd, workspaceRef,
229
+ dataHome: resolvedDataHome, env: env ?? {}, realpath: runtime.realpath ?? realpath,
228
230
  service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
229
231
  }
230
232
 
231
233
  // What this turn is shown and which complete groups it may commit as
232
234
  // offered. Runs after the heartbeat and the bounded native retry.
233
- async function projectTurn({ binding, context, adapter, adapterId }) {
235
+ // `render` is how the projected text reaches this client: before a turn it is
236
+ // the adapter's context envelope, at the end of one it is a continuation.
237
+ async function projectTurn({ binding, context, adapter, adapterId,
238
+ render = text => adapter.injectOutcome?.(text), activationHint = null }) {
234
239
  const sync = await context.service.sync({ sessionId: binding.accSessionId,
235
240
  cursor: null, scope: "delta" });
236
241
 
@@ -253,8 +258,14 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
253
258
  // Only this hook's payload selected the binding. Supply its own pair as
254
259
  // trusted context, outside peer bodies, rather than exporting inheritable
255
260
  // credentials or teaching the CLI to guess from a public roster.
256
- const owner = ownerHeader(binding);
257
261
  const totalBudget = context.descriptor.policy?.contextBudgetBytes ?? 6_000;
262
+ // An adapter's one-line ask rides with the owner line, which every projected
263
+ // turn carries - and only when both fit in half the budget, so peer bodies
264
+ // always keep the rest.
265
+ const header = ownerHeader(binding, context.workspaceCwd, context.workspaceRef);
266
+ const owner = activationHint === null
267
+ || byteLength(header) + 1 + byteLength(activationHint) > totalBudget / 2
268
+ ? header : `${header}\n${activationHint}`;
258
269
  // A peer can join after this prompt has begun. The current turn must already
259
270
  // have its own arguments when it needs inbox/reply, without reattaching or
260
271
  // waiting for another user prompt. Solo emits identity, not a peer notice.
@@ -330,7 +341,7 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
330
341
  // The entry point owns the transport boundary. This handler only prepares
331
342
  // offer inputs; recording them here would claim delivery before stdout's
332
343
  // callback proves that the bytes crossed.
333
- const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
344
+ const outcome = { stdout: "", ...render(projected) };
334
345
  const writableOffers = outcome.stdout === "" ? [] : offerInputs;
335
346
  return { ...outcome,
336
347
  stderr: [outcome.stderr, ownerWarning, degradation].filter(Boolean).join("\n"),
@@ -483,7 +494,9 @@ const HANDLERS = {
483
494
  const started = await HANDLERS.sessionStart(input);
484
495
  const fresh = await loadSessionBinding({ runtimeDir: paths.root,
485
496
  harnessSessionId: event.sessionId });
486
- const turn = await projectTurn({ ...input, binding: fresh });
497
+ const activationHint = await nativeActivationHintFor({ adapter, event,
498
+ nativeBinding: started.nativeBinding, binding: fresh, context, paths, deadline });
499
+ const turn = await projectTurn({ ...input, binding: fresh, activationHint });
487
500
  return { ...turn, nativeBinding: started.nativeBinding };
488
501
  }
489
502
  const current = await context.service.locateSession(binding.accSessionId);
@@ -497,7 +510,9 @@ const HANDLERS = {
497
510
  const started = await HANDLERS.sessionStart(input);
498
511
  const fresh = await loadSessionBinding({ runtimeDir: paths.root,
499
512
  harnessSessionId: event.sessionId });
500
- const turn = await projectTurn({ ...input, binding: fresh });
513
+ const activationHint = await nativeActivationHintFor({ adapter, event,
514
+ nativeBinding: started.nativeBinding, binding: fresh, context, paths, deadline });
515
+ const turn = await projectTurn({ ...input, binding: fresh, activationHint });
501
516
  return { ...turn, nativeBinding: started.nativeBinding };
502
517
  }
503
518
  // A turn is the clearest sign a session is alive. Never a reason to fail:
@@ -509,10 +524,38 @@ const HANDLERS = {
509
524
  const nativeBinding = await bindNative({ adapter, event, hookBinding: binding,
510
525
  clientVersion: binding.clientVersion, platform: binding.platform, context, paths,
511
526
  deadline });
512
- const turn = await projectTurn(input);
527
+ const activationHint = await nativeActivationHintFor({ adapter, event, nativeBinding,
528
+ binding, context, paths, deadline });
529
+ const turn = await projectTurn({ ...input, activationHint });
513
530
  return nativeBinding === undefined ? turn : { ...turn, nativeBinding };
514
531
  },
515
532
 
533
+ /**
534
+ * The end of a turn, for a client that can hold one open.
535
+ *
536
+ * A peer message that arrives while the model is producing its last answer
537
+ * has no next invocation to ride on, and would otherwise wait for the next
538
+ * user prompt - which can be hours. A client whose end-of-turn hook can
539
+ * continue the turn (`continueTurnOutcome`) gets that message now instead.
540
+ *
541
+ * Only for peer bodies the projector actually offers. Continuing a turn costs
542
+ * the operator a model invocation, so an owner header, an attention count or
543
+ * a degradation notice is never a reason to. The adapter owns the shape and
544
+ * the ceiling, and reads its own counter from the payload handed back to it;
545
+ * when it declines, it prints nothing, no offer is recorded, and the body
546
+ * stays queued for the next invocation. Every failure ends the turn normally.
547
+ */
548
+ async turnEnd(input) {
549
+ const { binding, context, adapter, payload } = input;
550
+ if (typeof adapter.continueTurnOutcome !== "function" || binding === null) return {};
551
+ const current = await context.service.locateSession(binding.accSessionId);
552
+ if (current === null || current.record.state !== "open"
553
+ || current.record.generation !== binding.generation) return {};
554
+ const turn = await projectTurn({ ...input,
555
+ render: text => adapter.continueTurnOutcome({ reason: text, payload }) });
556
+ return (turn.offerInputs ?? []).length === 0 ? {} : turn;
557
+ },
558
+
516
559
  async beforeTool({ binding, context, event, adapter }) {
517
560
  if (binding === null) return { decision: "allow" };
518
561
  if (event.targets.length === 0) {
@@ -580,6 +623,13 @@ const HANDLERS = {
580
623
  * only thing this function refuses to do is fail closed.
581
624
  */
582
625
  export async function runHook({ adapterId, payload, adapters, dataHome, env,
626
+ // The arguments the client's hook command carried, after the adapter id.
627
+ // Most clients name the event inside the payload and their adapters ignore
628
+ // this; Antigravity CLI sends no `hook_event_name` at all, and two of its
629
+ // four events hand over byte-identical envelopes - so for that client the
630
+ // registered command's own argument is the only thing that knows which hook
631
+ // ran. Passed to every adapter, read by the ones that need it.
632
+ args = [],
583
633
  runtime = defaultRuntime(), budgetMs = DEFAULT_BUDGET_MS,
584
634
  readProcessTable = defaultReadProcessTable,
585
635
  probeClientVersion = defaultProbeClientVersion,
@@ -592,8 +642,8 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
592
642
  const adapter = adapters?.[adapterId];
593
643
  if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
594
644
 
595
- const event = await adapter.normalizeHook(payload);
596
- const context = await openContext({ cwd: event.cwd, dataHome, runtime, env, deadline });
645
+ const event = await adapter.normalizeHook(payload, { args });
646
+ const context = await openContext({ event, adapterId, dataHome, runtime, env, deadline });
597
647
  const handler = HANDLERS[event.kind];
598
648
  const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
599
649
  const invoke = async () => {
@@ -608,10 +658,13 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
608
658
  const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
609
659
  harnessSessionId: event.sessionId });
610
660
  assertHookBudget(deadline);
661
+ // The raw payload goes to a handler only to be handed back to the adapter
662
+ // that produced it; nothing in core reads it.
611
663
  const result = handler === undefined ? {} : await handler({ event, context, adapter, adapterId,
612
- binding, paths: context.paths,
664
+ binding, paths: context.paths, payload,
613
665
  readProcessTable, probeClientVersion, platform, deadline });
614
- return appendToolOwner(result, { event, binding, context, adapter });
666
+ return appendToolOwner(appendStartOwner(result, { event, context, adapter }),
667
+ { event, binding, context, adapter });
615
668
  };
616
669
  const work = lifecycle
617
670
  ? withSessionLifecycle({ root: context.paths.root, sessionId: event.sessionId,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -159,6 +159,10 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
159
159
  try {
160
160
  const detected = await adapter.detect({ ...context, clientVersion: entry.version,
161
161
  platform, nativeDelivery: entry.nativeDelivery });
162
+ // An adapter that knows it cannot be installed right now says so here,
163
+ // rather than by throwing out of install and taking every other client
164
+ // on the machine down with it.
165
+ if (detected.blocked) entry.blocked = detected.blocked;
162
166
  if (detected.outgoingDelivery) entry.outgoingDelivery = detected.outgoingDelivery;
163
167
  if (detected.nativeSetup) entry.nativeSetup = detected.nativeSetup;
164
168
  entry.diagnostics = [...(detected.diagnostics ?? [])];
@@ -78,6 +78,16 @@ export function planInstallation({ adapters, detected, context, action = "instal
78
78
  + `if it is, wire it with --adapter ${entry.adapterId}` });
79
79
  continue;
80
80
  }
81
+ // An adapter can refuse itself for a reason that is nobody's fault and that
82
+ // no file on disk shows: Antigravity CLI cannot be wired until somebody
83
+ // picks between a machine-wide and a per-workspace hook registration. It is
84
+ // skipped by name so the rest of the run proceeds - but never when ACC has
85
+ // a record for it, because that record is the only account of what was
86
+ // written and an unremovable install is the worse failure.
87
+ if (entry.blocked !== undefined && recordedById.get(entry.adapterId) === undefined) {
88
+ skipped.push({ adapterId: entry.adapterId, reason: entry.blocked.reason });
89
+ continue;
90
+ }
81
91
  // The version doing the installing is whichever `acc` came first on PATH,
82
92
  // and the shim it writes pins that copy's node and runner. So a second ACC
83
93
  // on the machine quietly replaces every client's wiring with its own, older
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "accManagedUpdateProtocol": 2,
5
5
  "accStoreVersion": 6,
6
6
  "type": "module",
@@ -70,6 +70,7 @@
70
70
  "LICENSE"
71
71
  ],
72
72
  "dependencies": {
73
+ "@agents-can-communicate/adapter-antigravity": "*",
73
74
  "@agents-can-communicate/adapter-claude-code": "*",
74
75
  "@agents-can-communicate/adapter-codex": "*",
75
76
  "@agents-can-communicate/adapter-gemini-cli": "*",
@@ -86,6 +87,7 @@
86
87
  "@agents-can-communicate/storage-filesystem": "*"
87
88
  },
88
89
  "bundleDependencies": [
90
+ "@agents-can-communicate/adapter-antigravity",
89
91
  "@agents-can-communicate/adapter-claude-code",
90
92
  "@agents-can-communicate/adapter-codex",
91
93
  "@agents-can-communicate/adapter-gemini-cli",