agents-can-communicate 0.3.0 → 0.4.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 (157) hide show
  1. package/README.md +112 -91
  2. package/SECURITY.md +8 -5
  3. package/bin/acc-bootstrap.mjs +5 -54
  4. package/bin/acc-claude-channel.mjs +8 -175
  5. package/bin/acc-hook.mjs +4 -133
  6. package/bin/acc-mcp.mjs +4 -59
  7. package/bin/acc-update-worker.mjs +8 -0
  8. package/bin/acc.mjs +5 -39
  9. package/bin/entrypoints/acc-bootstrap.mjs +47 -0
  10. package/bin/entrypoints/acc-claude-channel.mjs +149 -0
  11. package/bin/entrypoints/acc-hook.mjs +58 -0
  12. package/bin/entrypoints/acc-mcp.mjs +70 -0
  13. package/bin/entrypoints/acc.mjs +51 -0
  14. package/bin/entrypoints/claude-channel-stdio.mjs +29 -0
  15. package/bin/entrypoints/hook-output.mjs +73 -0
  16. package/docs/ADAPTER_AUTHORING.md +62 -24
  17. package/docs/ARCHITECTURE.md +79 -20
  18. package/docs/CAPABILITIES.md +52 -30
  19. package/docs/CLI.md +164 -35
  20. package/docs/CONCEPTS.md +97 -93
  21. package/docs/CONFIGURATION.md +66 -45
  22. package/docs/DESIGN_DECISIONS.md +24 -15
  23. package/docs/GETTING_STARTED.md +124 -118
  24. package/docs/GLOSSARY.md +12 -6
  25. package/docs/HOW_IT_WORKS.md +136 -229
  26. package/docs/MCP.md +98 -31
  27. package/docs/PROTOCOL.md +76 -14
  28. package/docs/RELEASING.md +128 -86
  29. package/docs/SECURITY_MODEL.md +52 -15
  30. package/docs/TROUBLESHOOTING.md +88 -18
  31. package/docs/UPGRADING.md +119 -0
  32. package/docs/WHY_ACC.md +19 -16
  33. package/docs/index.md +40 -24
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +112 -9
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +6 -6
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +2 -1
  38. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +38 -13
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +220 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product-evidence.json +1549 -0
  41. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-current-binding-product.json +27 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product-evidence.json +1549 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-product.json +27 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport-evidence.json +259 -0
  45. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-local-daemon-transport.json +21 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product-evidence.json +1549 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-current-binding-product.json +27 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product-evidence.json +1549 -0
  49. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-product.json +27 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport-evidence.json +259 -0
  51. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.153.4-local-daemon-transport.json +21 -0
  52. package/node_modules/@agents-can-communicate/adapter-codex/package.json +14 -2
  53. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +111 -8
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +26 -44
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +70 -17
  56. package/node_modules/@agents-can-communicate/adapter-codex/src/config-block.mjs +231 -0
  57. package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +1 -1
  58. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +71 -63
  59. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +107 -103
  60. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +96 -0
  61. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +3 -2
  62. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  63. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +111 -8
  64. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  65. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +2 -2
  66. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  67. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +111 -8
  68. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +2 -2
  69. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  70. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +111 -8
  71. package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +1 -1
  72. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +2 -2
  73. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +5 -0
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +26 -4
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/decision-text.mjs +19 -0
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +5 -3
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +9 -2
  80. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -6
  81. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +29 -9
  82. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +8 -6
  83. package/node_modules/@agents-can-communicate/cli/package.json +4 -2
  84. package/node_modules/@agents-can-communicate/cli/src/args.mjs +49 -22
  85. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +11 -4
  86. package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +9 -3
  87. package/node_modules/@agents-can-communicate/cli/src/help.mjs +109 -14
  88. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +66 -18
  89. package/node_modules/@agents-can-communicate/cli/src/main.mjs +35 -12
  90. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +110 -0
  91. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command.mjs +63 -0
  92. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +71 -0
  93. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +110 -0
  94. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +167 -0
  95. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/install.mjs +73 -0
  96. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +60 -0
  97. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/leases.mjs +60 -0
  98. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/location.mjs +55 -0
  99. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +145 -0
  100. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/policy.mjs +4 -0
  101. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/refresh.mjs +40 -0
  102. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +21 -0
  103. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +122 -0
  104. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +65 -0
  105. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +33 -108
  106. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +25 -7
  107. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/core/src/attention.mjs +3 -0
  109. package/node_modules/@agents-can-communicate/core/src/claims.mjs +13 -8
  110. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +48 -24
  111. package/node_modules/@agents-can-communicate/core/src/decision-changes.mjs +36 -0
  112. package/node_modules/@agents-can-communicate/core/src/decision-state.mjs +42 -0
  113. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +11 -4
  114. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +48 -17
  115. package/node_modules/@agents-can-communicate/core/src/intents.mjs +35 -15
  116. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +7 -5
  117. package/node_modules/@agents-can-communicate/core/src/message-pages.mjs +75 -0
  118. package/node_modules/@agents-can-communicate/core/src/next-turn-delivery.mjs +31 -0
  119. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +128 -0
  120. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +58 -132
  121. package/node_modules/@agents-can-communicate/core/src/sync.mjs +31 -2
  122. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  123. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +38 -0
  124. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +86 -10
  125. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  126. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +1 -0
  127. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -4
  128. package/node_modules/@agents-can-communicate/hook-runner/src/native-retirement.mjs +48 -0
  129. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +1 -1
  130. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +175 -67
  131. package/node_modules/@agents-can-communicate/hook-runner/src/session-lifecycle.mjs +13 -0
  132. package/node_modules/@agents-can-communicate/installer/package.json +4 -2
  133. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +17 -2
  134. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +11 -5
  135. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +1 -1
  136. package/node_modules/@agents-can-communicate/installer/src/index.mjs +1 -0
  137. package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +21 -0
  138. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +20 -6
  139. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +10 -4
  140. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +3 -3
  141. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  142. package/node_modules/@agents-can-communicate/mcp-server/src/protocol.mjs +85 -0
  143. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +3 -2
  144. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +59 -57
  145. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +47 -33
  146. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  147. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +3 -0
  148. package/node_modules/@agents-can-communicate/protocol/src/decision-changes.mjs +15 -0
  149. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +3 -1
  150. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +14 -5
  151. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  152. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +5 -1
  153. package/node_modules/@agents-can-communicate/storage-filesystem/src/deadline.mjs +7 -0
  154. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +2 -0
  155. package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +25 -0
  156. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +28 -25
  157. package/package.json +2 -1
@@ -6,12 +6,17 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
6
6
  import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
7
7
  import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
8
8
  import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
9
- import { LIVE_POLICIES, applyPlan, describeActivation, detectInstallation, livePolicyOf,
9
+ import { LIVE_POLICIES, applyPlan, detectInstallation, livePolicyOf,
10
10
  loadOwnership, planInstallation, rcFileFor, shellOf, shimDirFor }
11
11
  from "@agents-can-communicate/installer";
12
12
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
13
13
 
14
14
  import { platformPaths } from "./platform-paths.mjs";
15
+ import { installManaged, uninstallManaged } from "./managed-runtime/install.mjs";
16
+
17
+ // Kept cohesive above 300 lines because this is the install command boundary:
18
+ // detection, consent, planning, application, and reporting share one context
19
+ // and result contract that would otherwise be duplicated across modules.
15
20
 
16
21
  // Where each client keeps its own configuration. All of it derives from one
17
22
  // home, so a test - or an operator with a second account - can point the whole
@@ -19,11 +24,13 @@ import { platformPaths } from "./platform-paths.mjs";
19
24
  // Each client keeps its own directory under the user's home, and an adapter
20
25
  // pointed at the home itself writes beside them rather than inside them. That
21
26
  // install reports success and the client never reads a byte of it.
22
- export const clientContext = (home, stateRoot, { shell = null, env = {} } = {}) => ({
27
+ export const clientContext = (home, stateRoot, { shell = null, env = {}, dataHome } = {}) => ({
23
28
  home,
29
+ ...(dataHome === undefined ? {} : { dataHome }),
24
30
  configDir: path.join(home, ".claude"),
25
31
  agentsHome: home,
26
- codexHome: path.join(home, ".codex"),
32
+ codexHome: typeof env.CODEX_HOME === "string" && env.CODEX_HOME !== ""
33
+ ? env.CODEX_HOME : path.join(home, ".codex"),
27
34
  kimiHome: path.join(home, ".kimi-code"),
28
35
  grokHome: path.join(home, ".grok"),
29
36
  // The user's login shell and PATH, for the optional native shell bootstrap:
@@ -142,6 +149,9 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
142
149
  ...operations.filter(operation => operation.applied
143
150
  && typeof operation.deliveryDiagnostic === "string")
144
151
  .map(operation => ` ${operation.deliveryDiagnostic}`),
152
+ ...operations.filter(operation => operation.applied)
153
+ .flatMap(operation => (operation.needsAction ?? [])
154
+ .map(step => ` ${operation.adapterId}: ${step}`)),
145
155
  ...skipped.map(entry => ` skip ${entry.adapterId}: ${entry.reason}`),
146
156
  ...failed.map(entry => ` ${entry.adapterId}: ${entry.error}`),
147
157
  // Said once, where it is needed: the reader has just been shown a list of
@@ -192,18 +202,43 @@ export function actedOn(result) {
192
202
  const isInteractive = runtime => typeof runtime.isInteractive === "function"
193
203
  && runtime.isInteractive() === true;
194
204
 
195
- // One default-No question per eligible client, asked only after detection has
196
- // finished and only when a person is at both ends of the terminal. It names
197
- // the client, the mechanism, and the files or service it would touch, and says
198
- // when the choice takes effect.
199
- function questionFor(entry, context) {
200
- const activation = { livePolicy: "actionable", shell: context.shell,
201
- rcFile: rcFileFor(context.home, context.shell), shimDir: shimDirFor(context.stateRoot),
202
- mechanisms: entry.nativeDelivery.activationPlan.mechanisms };
203
- return [`Enable native live delivery for ${entry.displayName} ${entry.version}?`,
204
- ...describeActivation(activation).map(line => ` ${line}`),
205
- " applies to newly started sessions; a new PATH block needs a new or reloaded shell"]
206
- .join("\n");
205
+ /**
206
+ * One default-No question per eligible client, asked only after detection has
207
+ * finished and only when a person is at both ends of the terminal.
208
+ *
209
+ * Written for someone who has never heard of this project. It used to open with
210
+ * "Enable native live delivery" over a list of internal artefact names, which
211
+ * told a first-time reader neither what they would gain nor what it would cost -
212
+ * and left out the part that matters most: saying yes makes their own `claude`
213
+ * start with the vendor's `--dangerously-load-development-channels` flag. A
214
+ * consent prompt that hides the loudest consequence is not consent.
215
+ *
216
+ * So the order is the reader's, not the installer's: what changes for them,
217
+ * what it costs, what it touches, and that "no" costs them nothing.
218
+ */
219
+ function questionFor(entry, context, { onlyEligible = false } = {}) {
220
+ const bootstrap = entry.nativeDelivery.activationPlan.mechanisms
221
+ .find(mechanism => mechanism.kind === "shell-bootstrap") ?? null;
222
+ // The presence of a flag is what makes this worth asking about at all: it is
223
+ // why the client will warn at every start from now on. Which flag it is, and
224
+ // that a shim adds it, are answers to a question nobody is asking here - the
225
+ // shim `exec`s and is gone, so nothing runs "through" acc afterwards either.
226
+ const flags = (bootstrap?.prefixArgs ?? []).filter(argument => argument.startsWith("--"));
227
+ void context;
228
+ // Why the default is No, and why only one client is being asked about, are
229
+ // the two questions the bare prompt provokes and never answered: a `[y/N]`
230
+ // reads as "not recommended" on its own, and a single question among four
231
+ // installed clients reads as an omission. Both have the same short answer.
232
+ const aside = ["experimental", ...(onlyEligible ? [`${entry.displayName} only`] : [])]
233
+ .join("; ");
234
+ return [
235
+ `Let idle agents answer each other while you are away? (${aside})`,
236
+ ...(flags.length === 0 || bootstrap === null
237
+ ? [" Yes: they reply without waiting for you."]
238
+ : [` Yes: they reply without waiting for you, and ${entry.displayName} asks you`,
239
+ " to allow development channels every time it starts."]),
240
+ " No: messages still arrive, at the session's next turn.",
241
+ ].join("\n");
207
242
  }
208
243
 
209
244
  /**
@@ -220,6 +255,11 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
220
255
  throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
221
256
  }
222
257
  const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
258
+ // Whether this client is the only one that could be asked at all. When it is,
259
+ // the question says so, because otherwise being asked about one of four
260
+ // installed clients looks like the other three were forgotten.
261
+ const onlyEligible = detected
262
+ .filter(candidate => candidate.nativeDelivery?.state === "eligible").length === 1;
223
263
  const deliveryByAdapter = {};
224
264
  const asked = [];
225
265
  let withheld = 0;
@@ -234,7 +274,7 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
234
274
  deliveryByAdapter[entry.adapterId] = "off";
235
275
  if (eligible) withheld += 1;
236
276
  } else {
237
- const yes = await runtime.confirm(questionFor(entry, context),
277
+ const yes = await runtime.confirm(questionFor(entry, context, { onlyEligible }),
238
278
  { input: runtime.input, output: runtime.output }) === true;
239
279
  deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
240
280
  asked.push(entry.adapterId);
@@ -263,7 +303,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
263
303
  const { data: dataHome } = platformPaths({ platform: runtime.platform,
264
304
  env: runtime.env ?? {} });
265
305
  const context = clientContext(home, path.join(dataHome, "acc"),
266
- { shell: shellOf(runtime.env), env: runtime.env ?? {} });
306
+ { shell: shellOf(runtime.env), env: runtime.env ?? {}, dataHome });
267
307
 
268
308
  const detected = await detectInstallation({ adapters, context,
269
309
  probeTimeoutMs: probeTimeout(runtime.env) });
@@ -297,7 +337,15 @@ export async function runInstallCommand({ options, runtime, action = "install" }
297
337
  const plan = planInstallation({ adapters, detected, context, action, recorded,
298
338
  accVersion, allowDowngrade: options.downgrade === true, requested,
299
339
  deliveryByAdapter: decided.deliveryByAdapter });
300
- const result = await applyPlan({ plan, adapters, context, dataHome, dryRun, accVersion });
340
+ const apply = (paths = {}) => applyPlan({ plan, adapters, context: { ...context, ...paths },
341
+ dataHome, dryRun, accVersion, activation: { bootstrap: paths.bootstrap } });
342
+ const result = action === "install" && !dryRun && runtime.packageRoot
343
+ ? await installManaged({ packageRoot: runtime.packageRoot,
344
+ managerRoot: path.join(dataHome, "acc", "runtime"), dataHome, home, cwd: options.cwd ?? runtime.cwd,
345
+ targets: plan.operations.map(operation => operation.adapterId), env: runtime.env, apply })
346
+ : action === "uninstall" && !dryRun && runtime.managerRoot
347
+ ? await uninstallManaged({ managerRoot: runtime.managerRoot, apply })
348
+ : await apply();
301
349
 
302
350
  const acted = actedOn(result);
303
351
  if (dryRun) {
@@ -13,7 +13,7 @@ import { parseArgs, positiveNumber } from "./args.mjs";
13
13
  // A usage error names what is missing rather than failing deeper in a service
14
14
  // with the argument already half-applied.
15
15
  const usage = message => new AccError(EXIT.USAGE, message);
16
- import { describeCommands, helpText } from "./help.mjs";
16
+ import { commandHelpText, describeCommand, describeCommands, helpText } from "./help.mjs";
17
17
  import { runUpdateCommand } from "./update-command.mjs";
18
18
  import { runConfigCommand } from "./config-command.mjs";
19
19
  import { runInstallCommand } from "./install-command.mjs";
@@ -73,23 +73,24 @@ async function locateContext(options, runtime) {
73
73
  gitProbe: runtime.gitProbe ?? createGitProbe(),
74
74
  explicitConfig: options.workspace,
75
75
  });
76
+ const dataHome = runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
77
+ env: runtime.env });
76
78
  const paths = runtimePaths({
77
- dataHome: runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
78
- env: runtime.env }),
79
+ dataHome,
79
80
  workspaceId: descriptor.id,
80
81
  workspaceRoots: descriptor.roots,
81
82
  });
82
- return { descriptor, paths };
83
+ return { descriptor, paths, dataHome };
83
84
  }
84
85
 
85
- async function withService({ descriptor, paths }, runtime) {
86
+ async function withService({ descriptor, paths, dataHome }, runtime) {
86
87
  const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
87
88
  ids: runtime.ids, workspaceId: descriptor.id });
88
89
  const service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
89
- return { descriptor, paths,
90
+ return { descriptor, paths, dataHome,
90
91
  service,
91
92
  deliveryRouter: typeof runtime.createDeliveryRouter === "function"
92
- ? runtime.createDeliveryRouter({ service, clock: runtime.clock }) : null };
93
+ ? runtime.createDeliveryRouter({ service, clock: runtime.clock, dataHome }) : null };
93
94
  }
94
95
 
95
96
  async function openContext(options, runtime) {
@@ -200,7 +201,9 @@ const HANDLERS = Object.freeze({
200
201
  sync: async ({ options, context }) => {
201
202
  const result = await context.service.sync({ sessionId: options.session,
202
203
  cursor: options.cursor ?? null, scope: options.scope,
204
+ messageId: options.message, kind: options.type, current: options.current,
203
205
  limit: options.limit ? positiveNumber(options.limit, "limit") : undefined });
206
+ if (result.scope === "history") return { data: result, text: JSON.stringify(result, null, 2) };
204
207
  // Solo zero-overhead: nothing to say means nothing printed, not a banner.
205
208
  const text = result.solo ? "" : `${result.attention.length} attention; `
206
209
  + `${result.roster.length} session(s); cursor ${result.cursor}`;
@@ -250,7 +253,9 @@ const HANDLERS = Object.freeze({
250
253
  context.service.sendMessage({ sessionId: options.session,
251
254
  generation: options.generation, clientMessageId: clientMessageId(options, context),
252
255
  toParticipantIds, kind,
253
- obligation: obligationFor(kind, options.obligation, toParticipantIds.length > 0),
256
+ supersedes: options.supersedes, withdraws: options.withdraws,
257
+ obligation: obligationFor(kind, options.obligation, toParticipantIds.length > 0
258
+ || options.supersedes !== undefined || options.withdraws !== undefined),
254
259
  subject: options.subject, body: options.body, descriptor: context.descriptor }) });
255
260
  const message = routed.recorded;
256
261
  return { data: { message, delivery: routed.delivery },
@@ -258,6 +263,15 @@ const HANDLERS = Object.freeze({
258
263
  },
259
264
 
260
265
  inbox: async ({ options, context }) => {
266
+ if (options.message === undefined) {
267
+ const page = await context.service.listInbox({ sessionId: options.session,
268
+ generation: options.generation, cursor: options.cursor,
269
+ limit: options.limit === undefined ? undefined : positiveNumber(options.limit, "limit") });
270
+ return { data: page, text: JSON.stringify(page, null, 2) };
271
+ }
272
+ if (options.cursor !== undefined || options.limit !== undefined) {
273
+ throw usage("an exact inbox read cannot use --cursor or --limit");
274
+ }
261
275
  const messages = await context.service.readInbox({ sessionId: options.session,
262
276
  generation: options.generation, messageId: options.message });
263
277
  return { data: messages, text: messages.length === 0
@@ -271,8 +285,8 @@ const HANDLERS = Object.freeze({
271
285
  generation: options.generation, messageId: options.message, body: options.body,
272
286
  subject: options.subject, clientMessageId: clientMessageId(options, context) }) });
273
287
  const result = routed.recorded;
274
- return { data: { message: result.reply, delivery: routed.delivery },
275
- text: recordedText(result.reply, routed.delivery) };
288
+ return { data: { message: result.reply, receipt: result.receipt, delivery: routed.delivery },
289
+ text: `${recordedText(result.reply, routed.delivery)}; acknowledged ${result.receipt.messageId}` };
276
290
  },
277
291
 
278
292
  request: async ({ options, context }) => {
@@ -346,7 +360,7 @@ const HANDLERS = Object.freeze({
346
360
  ids: runtime.ids });
347
361
  if (result.subcommand === "validate") {
348
362
  return { data: result, text: result.present
349
- ? `${result.file} is valid` : `no ${CONFIG_FILENAME}; defaults apply` };
363
+ ? `${result.file} is valid` : `no ${CONFIG_FILENAME} in selected directory; ancestors not checked` };
350
364
  }
351
365
  return { data: result,
352
366
  text: result.written ? `wrote ${result.file}` : `not written: ${result.file}` };
@@ -362,7 +376,13 @@ const HANDLERS = Object.freeze({
362
376
 
363
377
  update: async ({ options, runtime }) => runUpdateCommand({ options, runtime }),
364
378
 
365
- help: async () => ({ data: { commands: describeCommands() }, text: helpText() }),
379
+ help: async ({ options }) => {
380
+ if (options.helpCommand === undefined) {
381
+ return { data: { commands: describeCommands() }, text: helpText() };
382
+ }
383
+ const data = describeCommand(options.helpCommand, options.subcommand);
384
+ return { data, text: commandHelpText(data) };
385
+ },
366
386
 
367
387
  version: async ({ runtime }) => {
368
388
  // Read by the composition root from the package manifest: `bin/` sits at
@@ -390,6 +410,9 @@ export async function main(argv, runtime) {
390
410
  let parsed;
391
411
  try {
392
412
  parsed = parseArgs(argv);
413
+ if (runtime.managementOnly && !["help", "version", "update"].includes(parsed.command)) {
414
+ throw new AccError(EXIT.DATA, "runtime unavailable; retry after the update or run acc update to recover");
415
+ }
393
416
  // `config` is the one command that must work on a workspace ACC cannot
394
417
  // open. Discovery validates the config too, so a broken one would fail
395
418
  // there first and `acc config validate` - the command a user runs to find
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { assertPortableId } from "@agents-can-communicate/protocol";
6
+ import { readSessionRecord } from "@agents-can-communicate/storage-filesystem";
7
+ import { listRuntimeHolds } from "./leases.mjs";
8
+ import { confirmedDead, defaultPidIsAlive, withManagerLock } from "./mutex.mjs";
9
+ import { canonicalManagerRoot, managedDirectory, readControl, readManagedJson, writeControl } from "./state.mjs";
10
+
11
+ // Both 0.3.1 and current MCP write this exact continuity record, without
12
+ // native client facts. A foreign harness id may look like an MCP key: require
13
+ // its canonical file and a validated matching MCP owner before exempting it.
14
+ async function isMcpContinuity(record, name, workspaceRoot, workspaceId) {
15
+ if (record?.schemaVersion !== 1 || Object.keys(record).sort().join(",")
16
+ !== "accSessionId,generation,harnessSessionId,schemaVersion") return false;
17
+ try {
18
+ const parts = record.harnessSessionId.split(":");
19
+ if (parts.length !== 3 || parts[0] !== "mcp" || parts[2] !== workspaceId) return false;
20
+ for (const id of [parts[1], parts[2], record.accSessionId, record.generation]) assertPortableId(id, "MCP continuity id");
21
+ const expected = createHash("sha256").update(record.harnessSessionId).digest("hex").slice(0, 32) + ".json";
22
+ if (name !== expected) return false;
23
+ const owner = await readSessionRecord({ root: workspaceRoot, workspaceId, sessionId: record.accSessionId });
24
+ return owner?.harness === "mcp" && owner.participantId === parts[1]
25
+ && owner.workspaceId === workspaceId && owner.sessionId === record.accSessionId
26
+ && owner.generation === record.generation;
27
+ } catch { return false; } // Ambiguous or corrupt ownership remains a safety hold.
28
+ }
29
+
30
+ /** Native bindings outlive finish/TTL; MCP process lifetime is covered by its runtime lease. */
31
+ export async function listNativeHolds(root, { pidIsAlive = defaultPidIsAlive } = {}) {
32
+ const workspaces = path.join(path.dirname(await canonicalManagerRoot(root)), "workspaces");
33
+ if (!await managedDirectory(workspaces)) return [];
34
+ const holds = [];
35
+ for (const entry of await readdir(workspaces, { withFileTypes: true })) {
36
+ if (!entry.isDirectory()) {
37
+ if (entry.isSymbolicLink()) holds.push({ kind: "native", reason: "unknown_workspace" });
38
+ continue;
39
+ }
40
+ const bindings = path.join(workspaces, entry.name, "bindings");
41
+ if (!await managedDirectory(bindings)) continue;
42
+ for (const name of await readdir(bindings)) {
43
+ if (!name.endsWith(".json")) continue;
44
+ const record = await readManagedJson(path.join(bindings, name)).catch(() => null);
45
+ if (record === undefined) continue; // Concurrent lifecycle removal, before fencing.
46
+ if (await isMcpContinuity(record, name, path.dirname(bindings), entry.name)) continue;
47
+ const pid = record?.schemaVersion === 1 && Number.isSafeInteger(record.clientPid)
48
+ && record.clientPid > 0 ? record.clientPid : null;
49
+ if (pid !== null && await confirmedDead(pid, pidIsAlive)) continue;
50
+ holds.push({ kind: "native", pid, reason: pid === null ? "unknown_client_pid" : "client_running" });
51
+ }
52
+ }
53
+ return holds;
54
+ }
55
+
56
+ async function prepareCandidate(control, root, env) {
57
+ const file = path.join(control.pending.root, "node_modules", "@agents-can-communicate", "cli",
58
+ "src", "managed-runtime", "refresh.mjs");
59
+ const { prepareRefresh } = await import(pathToFileURL(file).href);
60
+ return prepareRefresh({ control, root, env });
61
+ }
62
+ // JSON parser diagnostics may quote private config bytes. Keep the cause and
63
+ // known artifact paths, never the offending input or terminal control bytes.
64
+ const safeText = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, " ").slice(0, 1000);
65
+ function safeFailure(error) {
66
+ const message = typeof error === "string" ? error : "integration refresh failed";
67
+ if (/(?:valid|parse|parsing) JSON|JSON (?:input|at position)|Unexpected token|Expected.*(?:property|comma|colon)/i.test(message)) return "configuration is not valid JSON";
68
+ return safeText(message);
69
+ }
70
+ const recipe = c => JSON.stringify([c.active, c.pending, c.home, c.targets, c.auto, c.pin]);
71
+
72
+ /** Detection is outside the fence; every integration write and commit stays inside it. */
73
+ export async function activatePending(root, { prepare = prepareCandidate, env = process.env,
74
+ pidIsAlive = defaultPidIsAlive, ignorePid = null } = {}) {
75
+ root = await canonicalManagerRoot(root);
76
+ const before = await readControl(root);
77
+ if (!before?.pending) return { activated: false, reason: "no_pending_update" };
78
+ const apply = await prepare(before, root, env);
79
+ return withManagerLock(root, async () => {
80
+ const current = await readControl(root);
81
+ if (!current || recipe(current) !== recipe(before)) return { activated: false, reason: "state_changed" };
82
+ const holds = (await listRuntimeHolds(root, { pidIsAlive })).filter(lease => lease.pid !== ignorePid);
83
+ holds.push(...await listNativeHolds(root, { pidIsAlive }));
84
+ if (holds.length) {
85
+ const notice = `ACC ${current.pending.version} is ready; waiting for ${holds.length} active or unidentified process(es).`;
86
+ await writeControl(root, { ...current, notice });
87
+ return { activated: false, reason: "processes_active", holds: holds.length, notice };
88
+ }
89
+ const activating = { ...current, phase: "activating", notice: "Finishing integration refresh." };
90
+ await writeControl(root, activating);
91
+ let result;
92
+ try {
93
+ result = await apply(); // Never release the fence with a mutating promise still running.
94
+ if (!result || !Array.isArray(result.failed) || result.failed.length) {
95
+ throw new Error("integration refresh is incomplete");
96
+ }
97
+ } catch (error) {
98
+ const notice = "Integration refresh is incomplete; run acc update to recover.";
99
+ await writeControl(root, { ...activating, notice });
100
+ const installation = result && Array.isArray(result.failed) ? { ...result,
101
+ failed: result.failed.map(failure => ({ ...failure, adapterId: safeText(failure.adapterId), error: safeFailure(failure.error),
102
+ paths: (failure.paths ?? []).map(file => safeText(file)) })) } : undefined;
103
+ return { activated: false, reason: "refresh_failed", notice, error: safeFailure(error.message),
104
+ ...(installation ? { installation } : {}) };
105
+ }
106
+ await writeControl(root, { ...activating, active: current.pending, pending: null,
107
+ phase: "ready", notice: `ACC ${current.pending.version} is active.` });
108
+ return { activated: true, version: current.pending.version, installation: result };
109
+ });
110
+ }
@@ -0,0 +1,63 @@
1
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
+ import { stableVersion, newerVersion } from "./download.mjs";
3
+ import { withManagerLock } from "./mutex.mjs";
4
+ import { scheduleWorker } from "./schedule.mjs";
5
+ import { readControl, writeControl } from "./state.mjs";
6
+ import { performUpdate, runWorker } from "./worker.mjs";
7
+
8
+ export function validateUpdateOptions(options) {
9
+ if (options.auto !== undefined && !["on", "off"].includes(options.auto)) {
10
+ throw new AccError(EXIT.USAGE, "--auto expects on or off");
11
+ }
12
+ if (options.pin !== undefined && options.pin !== "none" && !stableVersion(options.pin)) {
13
+ throw new AccError(EXIT.USAGE, "--pin expects an exact stable version or none");
14
+ }
15
+ if (options.check && (options.auto !== undefined || options.pin !== undefined)) {
16
+ throw new AccError(EXIT.USAGE, "--check cannot change update settings");
17
+ }
18
+ }
19
+
20
+ export async function runManagedUpdate({ options, runtime }) {
21
+ const root = runtime.managerRoot;
22
+ const env = runtime.env ?? {};
23
+ try {
24
+ if (options.auto !== undefined || options.pin !== undefined) {
25
+ const control = await withManagerLock(root, async () => {
26
+ const current = await readControl(root);
27
+ if (!current) throw new Error("run acc install before configuring automatic updates");
28
+ if (current.phase !== "ready" && options.pin !== undefined) throw new Error("finish the interrupted update before changing its version pin");
29
+ const pin = options.pin === undefined ? current.pin : options.pin === "none" ? null : options.pin;
30
+ if (pin !== null && newerVersion(current.active.version, pin)) {
31
+ throw new Error("pin cannot downgrade workspace data; choose the running version or a newer release");
32
+ }
33
+ return writeControl(root, { ...current,
34
+ auto: options.auto === undefined ? current.auto : options.auto === "on", pin,
35
+ pending: options.pin !== undefined && current.pending?.version !== pin ? null : current.pending,
36
+ notice: null });
37
+ });
38
+ await scheduleWorker(root, control, { env });
39
+ return { data: { auto: control.auto, pin: control.pin, running: control.active.version },
40
+ text: `Automatic updates ${control.auto ? "on" : "off"}. ${control.pin ? `Pinned to ${control.pin}.` : "Following stable releases."}` };
41
+ }
42
+ const result = options.check
43
+ ? await performUpdate(root, { check: true, env })
44
+ : await runWorker(root, { force: true, env, ignorePid: process.pid });
45
+ const text = result.activated ? `Updated ACC to ${result.version}; integrations refreshed.`
46
+ : result.notice && result.reason === "processes_active" ? result.notice
47
+ : result.reason === "refresh_failed" ? [result.notice,
48
+ ...(result.installation?.failed ?? []).map(failure =>
49
+ `${failure.adapterId}: ${failure.error}${failure.paths?.length ? ` (${failure.paths.join(", ")})` : ""}`),
50
+ ...(!result.installation?.failed?.length && result.error ? [result.error] : [])].join("\n")
51
+ : result.checked ? result.newer ? `ACC ${result.latest} is available; run acc update.`
52
+ : `ACC ${result.running} is current${result.pin ? ` for pin ${result.pin}` : ""}.`
53
+ : result.reason === "network_disabled" ? "Update networking is off (ACC_NO_UPDATE_CHECK)."
54
+ : result.notice ?? "Update is pending.";
55
+ return { data: result, text,
56
+ ...(result.reason === "refresh_failed" ? { error: new AccError(EXIT.DATA, text) } : {}) };
57
+ } catch (error) {
58
+ if (/manager lock held/.test(error.message)) {
59
+ return { data: { inProgress: true }, text: "An update is already in progress; ACC will activate it when running clients exit." };
60
+ }
61
+ throw error instanceof AccError ? error : new AccError(EXIT.DATA, error.message);
62
+ }
63
+ }
@@ -0,0 +1,71 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { stageOwnGeneration } from "./generation.mjs";
6
+ import { verifyGeneration } from "./install.mjs";
7
+ import { managedDirectory } from "./state.mjs";
8
+ import { ENTRY_KINDS } from "./entry.mjs";
9
+
10
+ const NAME = "agents-can-communicate";
11
+ const exec = promisify(execFile);
12
+ export const stableVersion = version => typeof version === "string"
13
+ && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version);
14
+ export function newerVersion(candidate, current) {
15
+ if (!stableVersion(candidate) || !stableVersion(current)) return false;
16
+ const left = candidate.split(".").map(BigInt), right = current.split(".").map(BigInt);
17
+ for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] > right[i];
18
+ return false;
19
+ }
20
+
21
+ /** Metadata only. Downloading and npm run exclusively in the update worker. */
22
+ export async function fetchRelease({ pin = null, env = process.env, get = fetch } = {}) {
23
+ if (pin !== null && !stableVersion(pin)) throw new Error("pin must be an exact stable version");
24
+ const registry = new URL(env.npm_config_registry || "https://registry.npmjs.org/");
25
+ if (!["https:", "http:"].includes(registry.protocol)) throw new Error("invalid npm registry");
26
+ registry.pathname = `${registry.pathname.replace(/\/$/, "")}/`;
27
+ const url = new URL(`${NAME}/${pin ?? "latest"}`, registry).href;
28
+ const response = await get(url, { signal: AbortSignal.timeout(5_000), headers: { accept: "application/json" } });
29
+ if (!response?.ok) throw new Error(`registry request failed (${response?.status ?? "no response"})`);
30
+ const body = await response.json();
31
+ if (body?.name !== NAME) throw new Error("release package identity mismatch");
32
+ if (!stableVersion(body.version) || pin !== null && body.version !== pin) throw new Error("release is not the requested stable version");
33
+ const integrity = body.dist?.integrity;
34
+ if (typeof integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity)
35
+ || Buffer.from(integrity.slice(7), "base64").length !== 64) throw new Error("release integrity is missing or invalid");
36
+ return { version: body.version, integrity, registry: registry.href };
37
+ }
38
+
39
+ async function npmCli(env) {
40
+ const directories = [path.dirname(process.execPath), ...(env.PATH ?? "").split(path.delimiter)];
41
+ for (const directory of directories.filter(Boolean)) {
42
+ try { return await realpath(path.join(directory, "npm")); } catch { /* Try the next installed npm. */ }
43
+ }
44
+ throw new Error("npm is unavailable; the working ACC runtime was kept");
45
+ }
46
+
47
+ export async function downloadRelease(root, release, { env = process.env } = {}) {
48
+ const downloads = path.join(root, "downloads");
49
+ await managedDirectory(downloads, { create: true });
50
+ const temporary = await mkdtemp(path.join(downloads, ".download-"));
51
+ try {
52
+ await writeFile(path.join(temporary, "package.json"), JSON.stringify({ private: true, name: "acc-runtime-download", version: "1.0.0" }));
53
+ await exec(process.execPath, [await npmCli(env), "install", "--ignore-scripts", "--omit=dev",
54
+ "--global=false", "--prefix", temporary, "--package-lock=true",
55
+ "--package-lock-only=false", "--dry-run=false", "--workspaces=false",
56
+ "--include-workspace-root=false", "--no-audit", "--no-fund", "--save=true", "--save-exact", "--registry", release.registry,
57
+ `${NAME}@${release.version}`], { cwd: temporary, env, timeout: 90_000, maxBuffer: 1024 * 1024 });
58
+ const lock = JSON.parse(await readFile(path.join(temporary, "package-lock.json"), "utf8"));
59
+ if (lock.packages?.[`node_modules/${NAME}`]?.integrity !== release.integrity) {
60
+ throw new Error("downloaded release integrity differs from discovery");
61
+ }
62
+ const packageRoot = path.join(temporary, "node_modules", NAME);
63
+ const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
64
+ if (manifest.name !== NAME || manifest.version !== release.version) throw new Error("downloaded package identity mismatch");
65
+ for (const name of ENTRY_KINDS) await access(path.join(packageRoot, "bin", "entrypoints", `${name}.mjs`));
66
+ await access(path.join(packageRoot, "bin", "acc-update-worker.mjs"));
67
+ const generation = await stageOwnGeneration({ packageRoot, managerRoot: root });
68
+ await verifyGeneration(generation, { env });
69
+ return { version: generation.version, root: generation.root };
70
+ } finally { await rm(temporary, { recursive: true, force: true }); }
71
+ }
@@ -0,0 +1,110 @@
1
+ import { realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { scheduleWorker } from "./schedule.mjs";
5
+ import { acquireRuntime } from "./leases.mjs";
6
+ import { canonicalManagerRoot, readControl } from "./state.mjs";
7
+
8
+ export const ENTRY_KINDS = Object.freeze([
9
+ "acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel",
10
+ ]);
11
+
12
+ const BOOTSTRAP_FLAGS = new Map([["--adapter", "adapter"], ["--real-executable", "realExecutable"],
13
+ ["--data-home", "dataHome"]]);
14
+
15
+ /** Shared with the runtime; parse before admission without importing installer code. */
16
+ export function parseBootstrapOptions(args) {
17
+ const options = {};
18
+ for (let index = 0; index < args.length; index += 2) {
19
+ const key = BOOTSTRAP_FLAGS.get(args[index]);
20
+ const value = args[index + 1];
21
+ if (key === undefined || typeof value !== "string" || value === "" || key in options) return null;
22
+ options[key] = value;
23
+ }
24
+ return BOOTSTRAP_FLAGS.values().every(key => key in options) && path.isAbsolute(options.dataHome)
25
+ ? options : null;
26
+ }
27
+
28
+ // Bootstrap resolution deliberately needs only Node built-ins: no selected
29
+ // generation is imported until admission and its actual PID lease are durable.
30
+ export function managerLocation({ env = process.env, platform = process.platform } = {}) {
31
+ const data = env.ACC_DATA_HOME || (platform === "win32" ? env.APPDATA
32
+ : platform === "darwin" ? env.HOME && path.join(env.HOME, "Library", "Application Support")
33
+ : env.XDG_DATA_HOME || env.HOME && path.join(env.HOME, ".local", "share"));
34
+ if (typeof data !== "string" || !path.isAbsolute(data)) throw new Error("cannot resolve ACC data home");
35
+ return path.join(data, "acc", "runtime");
36
+ }
37
+
38
+ export function invokedDirectly(url) {
39
+ try { return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(url)); }
40
+ catch { return false; }
41
+ }
42
+
43
+ function unavailable(kind) {
44
+ // Never reflect control file content or payloads into a client diagnostic.
45
+ if (kind === "acc-hook" || kind === "acc-claude-channel") {
46
+ process.stderr.write("acc: coordination unavailable during runtime update; session may continue\n");
47
+ process.exitCode = 0;
48
+ } else if (kind === "acc-bootstrap") {
49
+ process.exitCode = 1; // The vendor shell shim launches its original command.
50
+ } else {
51
+ process.stderr.write("acc: runtime unavailable; retry after the update or run acc update to recover\n");
52
+ process.exitCode = 4;
53
+ }
54
+ }
55
+
56
+ /** The lease lasts until OS process death, including callbacks after main returns. */
57
+ export async function runEntry({ kind, packageRoot, managerRoot, managedRequired = false }) {
58
+ if (!ENTRY_KINDS.includes(kind)) throw new Error("unknown ACC entry point");
59
+ const bootstrapOptions = kind === "acc-bootstrap" ? parseBootstrapOptions(process.argv.slice(2)) : null;
60
+ if (kind === "acc-bootstrap" && bootstrapOptions === null) {
61
+ if (process.env.ACC_BOOTSTRAP_DEBUG === "1") {
62
+ process.stderr.write("acc-bootstrap: usage: --adapter <id> --real-executable <path> --data-home <path>\n");
63
+ }
64
+ process.exitCode = 2;
65
+ return;
66
+ }
67
+ let selected = packageRoot;
68
+ let managed = null;
69
+ let root;
70
+ try {
71
+ const requestedDataHome = bootstrapOptions === null ? null : await canonicalManagerRoot(bootstrapOptions.dataHome);
72
+ const requestedRoot = requestedDataHome === null ? null
73
+ : await canonicalManagerRoot(path.join(requestedDataHome, "acc", "runtime"));
74
+ if (managerRoot && requestedDataHome !== null
75
+ && requestedDataHome !== await canonicalManagerRoot(path.dirname(path.dirname(managerRoot)))) {
76
+ throw new Error("bootstrap cache home differs from its launcher");
77
+ }
78
+ root = await canonicalManagerRoot(managerRoot ?? requestedRoot ?? managerLocation());
79
+ if (requestedRoot !== null && root !== requestedRoot) throw new Error("bootstrap data home differs from its launcher");
80
+ const control = await readControl(root);
81
+ if (control !== null) {
82
+ selected = control.active.root;
83
+ managed = root;
84
+ const lease = await acquireRuntime(root, { pid: process.pid, kind });
85
+ selected = lease.runtime.root;
86
+ managed = root;
87
+ const quiet = kind === "acc" && ["update", "install", "uninstall", "help", "version",
88
+ "--help", "-h", "--version", "-v", "-V"].includes(process.argv[2]);
89
+ if (!quiet) await scheduleWorker(root, control);
90
+ } else if (managedRequired) throw new Error("managed runtime is not initialized");
91
+ } catch {
92
+ // Help and update recovery must remain reachable when state cannot admit a
93
+ // workspace command. The CLI parser enforces this restricted path; merely
94
+ // putting --help in a message value cannot bypass runtime admission.
95
+ if (kind === "acc" && selected) {
96
+ const runtime = await import(pathToFileURL(path.join(selected, "bin", "entrypoints", "acc.mjs")).href);
97
+ await runtime.main({ managerRoot: root ?? null, packageRoot: selected, managementOnly: true });
98
+ return;
99
+ }
100
+ unavailable(kind);
101
+ if (kind === "acc-claude-channel" && selected) {
102
+ const { startInertChannel } = await import(pathToFileURL(path.join(selected,
103
+ "bin", "entrypoints", "claude-channel-stdio.mjs")).href);
104
+ startInertChannel();
105
+ }
106
+ return;
107
+ }
108
+ const runtime = await import(pathToFileURL(path.join(selected, "bin", "entrypoints", `${kind}.mjs`)).href);
109
+ await runtime.main({ managerRoot: managed, packageRoot: selected });
110
+ }