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
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- import { AccError, EXIT } from "@agents-can-communicate/protocol";
5
+ import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protocol";
6
6
 
7
7
  const SCHEMA_VERSION = 1;
8
8
 
@@ -23,11 +23,32 @@ const fileFor = (runtimeDir, harnessSessionId) => path.join(runtimeDir, "binding
23
23
  */
24
24
  const isPid = value => Number.isInteger(value) && value > 0;
25
25
 
26
+ function assertIdentity(record, harnessSessionId) {
27
+ if (record?.schemaVersion !== SCHEMA_VERSION) {
28
+ throw new AccError(EXIT.DATA, "unknown session binding schemaVersion",
29
+ { schemaVersion: record?.schemaVersion });
30
+ }
31
+ assertPortableId(record.accSessionId, "binding session id");
32
+ assertPortableId(record.generation, "binding generation");
33
+ if (typeof record.harnessSessionId !== "string" || record.harnessSessionId === ""
34
+ || harnessSessionId !== undefined && record.harnessSessionId !== harnessSessionId) {
35
+ throw new AccError(EXIT.DATA, "session binding names a different or invalid harness session", {});
36
+ }
37
+ }
38
+
39
+ function assertBindingDeadline(deadlineAt) {
40
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt) {
41
+ throw new AccError(EXIT.CONFLICT, "hook deadline expired before binding publication", {});
42
+ }
43
+ }
44
+
26
45
  export async function storeSessionBinding({ runtimeDir, harnessSessionId, accSessionId,
27
- generation, clientVersion, platform, clientPid }) {
46
+ generation, clientVersion, platform, clientPid, deadlineAt }) {
47
+ assertBindingDeadline(deadlineAt);
28
48
  const file = fileFor(runtimeDir, harnessSessionId);
29
- await mkdir(path.dirname(file), { recursive: true });
30
49
  const record = { schemaVersion: SCHEMA_VERSION, harnessSessionId, accSessionId, generation };
50
+ assertIdentity(record);
51
+ await mkdir(path.dirname(file), { recursive: true });
31
52
  if (typeof clientVersion === "string" && clientVersion !== "") record.clientVersion = clientVersion;
32
53
  if (typeof platform === "string" && platform !== "") record.platform = platform;
33
54
  // The vendor process this session runs in, resolved once at SessionStart.
@@ -45,6 +66,7 @@ export async function storeSessionBinding({ runtimeDir, harnessSessionId, accSes
45
66
  await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, "utf8");
46
67
  // Replace rather than append: re-attaching supersedes the old generation, and
47
68
  // two live bindings for one harness session would be worse than none.
69
+ assertBindingDeadline(deadlineAt);
48
70
  await rename(temporary, file);
49
71
  }
50
72
 
@@ -66,10 +88,7 @@ export async function loadSessionBinding({ runtimeDir, harnessSessionId }) {
66
88
  throw new AccError(EXIT.DATA, "session binding is not valid JSON",
67
89
  { file, cause: error.message });
68
90
  }
69
- if (record?.schemaVersion !== SCHEMA_VERSION) {
70
- throw new AccError(EXIT.DATA, "unknown session binding schemaVersion",
71
- { file, schemaVersion: record?.schemaVersion });
72
- }
91
+ assertIdentity(record, harnessSessionId);
73
92
  const binding = { accSessionId: record.accSessionId, generation: record.generation };
74
93
  if (typeof record.clientVersion === "string") binding.clientVersion = record.clientVersion;
75
94
  if (typeof record.platform === "string") binding.platform = record.platform;
@@ -77,7 +96,8 @@ export async function loadSessionBinding({ runtimeDir, harnessSessionId }) {
77
96
  return binding;
78
97
  }
79
98
 
80
- export async function clearSessionBinding({ runtimeDir, harnessSessionId }) {
99
+ export async function clearSessionBinding({ runtimeDir, harnessSessionId, deadlineAt }) {
100
+ assertBindingDeadline(deadlineAt);
81
101
  await rm(fileFor(runtimeDir, harnessSessionId), { force: true });
82
102
  }
83
103
 
@@ -107,7 +127,7 @@ export async function listSessionBindings({ runtimeDir }) {
107
127
  if (!name.endsWith(".json")) continue;
108
128
  const record = await readFile(path.join(dir, name), "utf8")
109
129
  .then(JSON.parse).catch(() => null);
110
- if (record?.schemaVersion !== SCHEMA_VERSION) continue;
130
+ try { assertIdentity(record); } catch { continue; }
111
131
  bindings.push({ harnessSessionId: record.harnessSessionId,
112
132
  accSessionId: record.accSessionId, generation: record.generation,
113
133
  ...(typeof record.clientVersion === "string"
@@ -4,8 +4,9 @@ import path from "node:path";
4
4
  // Two clients keep configuration ACC must add to in TOML, and ACC ships without
5
5
  // dependencies. Parsing and re-emitting the file would mean writing a TOML
6
6
  // round-tripper and losing the user's comments and formatting to it. Instead ACC
7
- // owns a delimited region and never reads the rest: install replaces the region,
8
- // uninstall deletes it, and everything outside comes back byte for byte.
7
+ // offers a delimited-region default. Clients that relocate comments must pass
8
+ // an ownership-aware cleanup callback; a comment boundary alone cannot establish
9
+ // ownership after the client has rewritten its configuration.
9
10
  export const BEGIN = "# >>> agents-can-communicate (managed; edits here are overwritten)";
10
11
  export const END = "# <<< agents-can-communicate";
11
12
 
@@ -40,17 +41,18 @@ export const tomlString = value =>
40
41
 
41
42
  /**
42
43
  * Replace ACC's region in a TOML file, creating the file if needed.
44
+ * Clients that rewrite comments must supply their own ownership-aware cleanup.
43
45
  *
44
46
  * The block goes at the end because a table header there closes whatever table
45
47
  * preceded it: appended anywhere else, the user's last section would swallow
46
48
  * ACC's keys.
47
49
  */
48
- export async function writeTomlBlock(file, body) {
50
+ export async function writeTomlBlock(file, body, strip = stripBlock) {
49
51
  const existing = await readFile(file, "utf8").catch(error => {
50
52
  if (error.code === "ENOENT") return "";
51
53
  throw error;
52
54
  });
53
- const withoutOurs = stripBlock(existing);
55
+ const withoutOurs = strip(existing);
54
56
  const separator = withoutOurs === "" || withoutOurs.endsWith("\n") ? "" : "\n";
55
57
  await mkdir(path.dirname(file), { recursive: true });
56
58
  await writeFile(file, `${withoutOurs}${separator}${renderBlock(body)}\n`);
@@ -58,13 +60,13 @@ export async function writeTomlBlock(file, body) {
58
60
  }
59
61
 
60
62
  /** Remove ACC's region, reporting whether anything was there. */
61
- export async function removeTomlBlock(file) {
63
+ export async function removeTomlBlock(file, strip = stripBlock) {
62
64
  const existing = await readFile(file, "utf8").catch(error => {
63
65
  if (error.code === "ENOENT") return null;
64
66
  throw error;
65
67
  });
66
68
  if (existing === null) return false;
67
- const stripped = stripBlock(existing);
69
+ const stripped = strip(existing);
68
70
  if (stripped === existing) return false;
69
71
  await writeFile(file, stripped);
70
72
  return true;
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./src/index.mjs"
7
+ ".": "./src/index.mjs",
8
+ "./managed-entry": "./src/managed-runtime/entry.mjs",
9
+ "./managed-worker": "./src/managed-runtime/worker.mjs"
8
10
  },
9
11
  "files": [
10
12
  "src/"
@@ -1,18 +1,19 @@
1
1
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
2
 
3
- // Commands the model is offered stay few and high level; attach, heartbeat, and
4
- // detach exist for adapters and are deliberately not advertised as model tools.
3
+ // Native adapters manage session lifecycle automatically. Manual CLI sessions
4
+ // use attach, heartbeat, and detach; both paths are described in command help.
5
5
  //
6
6
  // `--session` and `--generation` are optional on every agent-facing command:
7
- // the CLI works out which session is calling it (see session-owner.mjs). They
8
- // stay accepted because an adapter, a script, or an agent holding a session id
9
- // from `acc status --json` has a reason to be explicit. They remain required on
10
- // attach, heartbeat and detach, which are the adapter's own lifecycle calls.
7
+ // an operator may instead configure the explicit owner pair in the environment
8
+ // (see session-owner.mjs). Public status/sync also work without an owner pair.
9
+ // A public session id alone proves nothing. Heartbeat and detach always require
10
+ // both arguments; attach creates and returns the pair.
11
11
  export const COMMANDS = Object.freeze({
12
12
  attach: { required: ["participant"], optional: ["harness", "cadence", "parent", "session"] },
13
13
  heartbeat: { required: ["session", "generation"], optional: [] },
14
14
  detach: { required: ["session", "generation"], optional: [] },
15
- sync: { required: [], optional: ["session", "cursor", "limit", "scope"] },
15
+ sync: { required: [], optional: ["session", "generation", "cursor", "limit", "scope",
16
+ "message", "type"], flags: ["current"] },
16
17
  work: { required: [], optional: ["session", "generation", "summary", "mode",
17
18
  "state"], repeated: ["hint"], flags: ["clear"] },
18
19
  claim: { required: ["resource"],
@@ -25,8 +26,8 @@ export const COMMANDS = Object.freeze({
25
26
  optional: ["claim", "resource", "session", "generation", "authority", "reason"] },
26
27
  message: { required: ["subject", "body"],
27
28
  optional: ["session", "generation", "type", "obligation", "client-message-id"],
28
- repeated: ["to"] },
29
- inbox: { required: [], optional: ["session", "generation", "message"] },
29
+ repeated: ["to", "supersedes", "withdraws"] },
30
+ inbox: { required: [], optional: ["session", "generation", "message", "cursor", "limit"] },
30
31
  reply: { required: ["message", "body"],
31
32
  optional: ["session", "generation", "subject", "client-message-id"] },
32
33
  // Asking another agent to do something as a message with a reply obligation.
@@ -36,13 +37,13 @@ export const COMMANDS = Object.freeze({
36
37
  finish: { required: ["goal"], optional: ["session", "generation", "status", "to",
37
38
  "client-message-id"],
38
39
  repeated: ["completed", "remaining", "blocker"] },
39
- status: { required: [], optional: ["participant"], flags: ["all"] },
40
+ status: { required: [], optional: ["session", "generation", "participant"], flags: ["all"] },
40
41
  doctor: { required: [], optional: ["home"], flags: ["repair"] },
41
42
  // The one command with a subcommand. Kept as an explicit list rather than a
42
43
  // free positional: `acc config delete` should fail at the parser, not deep
43
44
  // inside a handler that has already decided what to do.
44
- config: { required: [], optional: [], flags: ["yes", "force"],
45
- subcommands: ["init", "validate"] },
45
+ config: { required: [], optional: [], subcommands: ["init", "validate"],
46
+ subcommandOptions: { init: { flags: ["yes", "force"] }, validate: { flags: [] } } },
46
47
  // No `--yes`: neither of these ever asked, so the flag agreed to nothing. It
47
48
  // was accepted and read by nobody, which is a promise that a confirmation
48
49
  // exists to be skipped.
@@ -57,7 +58,7 @@ export const COMMANDS = Object.freeze({
57
58
  uninstall: { required: [], optional: ["home"], repeated: ["adapter"], flags: ["dry-run"] },
58
59
  // Asking npm whether there is a newer ACC. The one command that touches the
59
60
  // network, and never on the hook path.
60
- update: { required: [], optional: [], flags: ["apply"] },
61
+ update: { required: [], optional: ["auto", "pin"], flags: ["check", "apply"] },
61
62
  // The two things a person types first after installing from a registry. The
62
63
  // CLI answered neither: `acc --version` and `acc --help` were both "unknown
63
64
  // command", and `acc` on its own asked for a command without naming one.
@@ -65,10 +66,8 @@ export const COMMANDS = Object.freeze({
65
66
  version: { required: [], optional: [] },
66
67
  });
67
68
 
68
- // Spelled as the commands they mean, and only in first position. A message body
69
- // legitimately begins with "--" - exchanging diffs is the point of this tool -
70
- // so reading them anywhere in the argv would make `acc message --body "--help"`
71
- // print the help instead of sending it.
69
+ // Leading aliases select a command. Command-local help is recognized while
70
+ // consuming options below, never by searching inside their values.
72
71
  const ALIASES = Object.freeze({ "--help": "help", "-h": "help",
73
72
  "--version": "version", "-v": "version", "-V": "version" });
74
73
 
@@ -80,26 +79,39 @@ function usage(message, details = {}) {
80
79
 
81
80
  const camel = name => name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
82
81
 
82
+ export function commandSpec(name, subcommand) {
83
+ const spec = COMMANDS[name];
84
+ return { ...spec, ...spec.subcommandOptions?.[subcommand] };
85
+ }
86
+
83
87
  export function parseArgs(argv) {
84
88
  if (!Array.isArray(argv) || argv.length === 0) {
85
89
  usage("a command is required - `acc help` lists them");
86
90
  }
87
91
  const [first, ...rest] = argv;
88
- const command = ALIASES[first] ?? first;
89
- const spec = COMMANDS[command];
90
- if (spec === undefined) {
92
+ let command = Object.hasOwn(ALIASES, first) ? ALIASES[first] : first;
93
+ let helpRequested = false;
94
+ if (command === "help" && rest[0] !== undefined && !rest[0].startsWith("-")) {
95
+ command = rest.shift();
96
+ helpRequested = true;
97
+ }
98
+ let spec = COMMANDS[command];
99
+ if (!Object.hasOwn(COMMANDS, command)) {
91
100
  usage(`unknown command: ${command} - \`acc help\` lists them`, { command });
92
101
  }
93
102
 
94
103
  let tokens = rest;
95
104
  let subcommand;
96
105
  if (spec.subcommands !== undefined) {
97
- [subcommand, ...tokens] = rest;
98
- if (subcommand === undefined || !spec.subcommands.includes(subcommand)) {
106
+ if (rest[0] !== undefined && !rest[0].startsWith("-")) {
107
+ [subcommand, ...tokens] = rest;
108
+ }
109
+ if (subcommand !== undefined && !spec.subcommands.includes(subcommand)) {
99
110
  usage(`${command} requires one of: ${spec.subcommands.join(", ")}`,
100
111
  { command, subcommand: subcommand ?? null });
101
112
  }
102
113
  }
114
+ spec = commandSpec(command, subcommand);
103
115
 
104
116
  const repeated = new Set(spec.repeated ?? []);
105
117
  const flags = new Set([...(spec.flags ?? []), "json"]);
@@ -117,6 +129,12 @@ export function parseArgs(argv) {
117
129
 
118
130
  for (let index = 0; index < tokens.length; index += 1) {
119
131
  const token = tokens[index];
132
+ // A preceding value-taking option has already consumed its value. In
133
+ // particular, `--body --help` still sends the literal text "--help".
134
+ if (token === "--help" || token === "-h") {
135
+ helpRequested = true;
136
+ continue;
137
+ }
120
138
  if (!token.startsWith("--") || token.length === 2) usage(`unexpected argument: ${token}`);
121
139
  const separator = token.indexOf("=");
122
140
  const name = separator === -1 ? token.slice(2) : token.slice(2, separator);
@@ -146,6 +164,15 @@ export function parseArgs(argv) {
146
164
  else options[key] = value;
147
165
  }
148
166
 
167
+ if (helpRequested) {
168
+ return { command: "help", options: { helpCommand: command,
169
+ ...(subcommand === undefined ? {} : { subcommand }),
170
+ ...(options.json === true ? { json: true } : {}) } };
171
+ }
172
+ if (spec.subcommands !== undefined && subcommand === undefined) {
173
+ usage(`${command} requires one of: ${spec.subcommands.join(", ")}`,
174
+ { command, subcommand: null });
175
+ }
149
176
  for (const name of spec.required ?? []) {
150
177
  if (!Object.hasOwn(options, camel(name))) {
151
178
  usage(`${command} requires --${name}`, { command, option: name });
@@ -10,6 +10,7 @@ import { ALL_ADAPTERS, clientContext, probeTimeout } from "./install-command.mjs
10
10
  import { describePresence } from "./main.mjs";
11
11
  import { platformPaths } from "./platform-paths.mjs";
12
12
  import { noticeUpdate } from "./update-check.mjs";
13
+ import { readControl } from "./managed-runtime/state.mjs";
13
14
  import { diagnoseFilesystemStore, repairFilesystemStore }
14
15
  from "@agents-can-communicate/storage-filesystem";
15
16
 
@@ -229,9 +230,13 @@ export async function runDoctor({ options, context, runtime }) {
229
230
  const running = typeof runtime?.version === "function"
230
231
  ? await runtime.version().catch(() => null)
231
232
  : null;
232
- const update = await noticeUpdate({ dataHome, running, env: runtime?.env ?? {},
233
- now: Date.parse(clock.now()), get: runtime?.fetch,
234
- io: { readFile, writeFile, mkdir } });
233
+ const manager = runtime?.managerRoot ? await readControl(runtime.managerRoot) : null;
234
+ const update = manager === null
235
+ ? await noticeUpdate({ dataHome, running, env: runtime?.env ?? {},
236
+ now: Date.parse(clock.now()), get: runtime?.fetch, io: { readFile, writeFile, mkdir } })
237
+ : { checked: false, running, latest: manager.pending?.version ?? null,
238
+ newer: manager.pending !== null, auto: manager.auto, pin: manager.pin,
239
+ pending: manager.pending?.version ?? null, phase: manager.phase, notice: manager.notice };
235
240
 
236
241
  // Before the store is read for anything else. `collectStatus` reads every
237
242
  // record, so on the store this command exists to describe it threw first and
@@ -285,7 +290,7 @@ export async function runDoctor({ options, context, runtime }) {
285
290
  remediation: [...adapters.flatMap(adapter => adapter.remediation),
286
291
  // Said here rather than in its own line of prose, because this list is
287
292
  // what a reader acts on and an upgrade is one more thing to run.
288
- ...(update.newer ? [`acc update --apply # ${update.latest} is on npm, `
293
+ ...(update.newer ? [`acc update # ${update.latest} is on npm, `
289
294
  + `you have ${running}`] : [])],
290
295
  };
291
296
  const installed = adapters.filter(adapter => adapter.installed).length;
@@ -303,6 +308,8 @@ export async function runDoctor({ options, context, runtime }) {
303
308
  // claims that "active" means a model read anything.
304
309
  ...adapters.filter(adapter => adapter.present)
305
310
  .map(adapter => ` ${adapter.displayName} native delivery: ${describeNative(adapter.nativeDelivery)}`),
311
+ ...(manager === null ? [] : [` automatic updates ${manager.auto ? "on" : "off"}; ACC ${manager.active.version}`
312
+ + (manager.pin ? `; pinned to ${manager.pin}` : ""), ...(manager.notice ? [` ${manager.notice}`] : [])]),
306
313
  ...data.remediation.map(line => ` ${line}`),
307
314
  // `0 of 4` is a true line that reads as a broken machine, and on an
308
315
  // MCP-only one it would read that way on every run forever. The server needs
@@ -16,9 +16,15 @@ export function hermeticEnv(env = process.env) {
16
16
  return Object.fromEntries(Object.entries(env).filter(([key]) => !key.startsWith("GIT_")));
17
17
  }
18
18
 
19
- export function createGitProbe({ run } = {}) {
20
- const exec = run ?? (async (cwd, args) =>
21
- (await execFileAsync("git", args, { cwd, env: hermeticEnv() })).stdout.trim());
19
+ export function createGitProbe({ run, deadlineAt } = {}) {
20
+ const exec = async (cwd, args) => {
21
+ const remaining = deadlineAt === undefined ? 0 : deadlineAt - Date.now();
22
+ if (deadlineAt !== undefined && remaining <= 0) throw new Error("Git probe deadline expired");
23
+ return run === undefined
24
+ ? (await execFileAsync("git", args, { cwd, env: hermeticEnv(), timeout: remaining,
25
+ killSignal: "SIGKILL" })).stdout.trim()
26
+ : run(cwd, args);
27
+ };
22
28
 
23
29
  return async function gitProbe({ cwd }) {
24
30
  const commonDir = await exec(cwd, ["rev-parse", "--path-format=absolute",
@@ -1,4 +1,10 @@
1
- import { COMMANDS } from "./args.mjs";
1
+ import { CLAIM_ENFORCEMENTS, CLAIM_MODES, GENERIC_MESSAGE_KINDS, HANDOFF_STATUSES,
2
+ INTENT_MODES, INTENT_STATES, MESSAGE_KINDS, OBLIGATIONS } from "@agents-can-communicate/protocol";
3
+ import { LIVE_POLICIES } from "@agents-can-communicate/installer";
4
+
5
+ import { COMMANDS, commandSpec } from "./args.mjs";
6
+ import { ALL_ADAPTERS } from "./install-command.mjs";
7
+ import { needsOwner } from "./session-owner.mjs";
2
8
 
3
9
  /**
4
10
  * What `acc` says to a person who has just installed it.
@@ -7,15 +13,14 @@ import { COMMANDS } from "./args.mjs";
7
13
  * is always listed. Only the heading a command sits under and its one line are
8
14
  * written here, and the tests prove both cover every command in the table.
9
15
  *
10
- * The required options are deliberately not repeated: the parser already
11
- * answers `acc claim` with "claim requires --resource", which is the same
12
- * answer at the moment it is actually needed.
16
+ * Per-command help also reads the parser's option table, and exposes the
17
+ * protocol's enum choices before a caller has to attempt a mutation.
13
18
  */
14
19
  const GROUPS = Object.freeze([
15
20
  ["Set up", ["install", "uninstall", "doctor", "config"]],
16
21
  ["In a session", ["status", "sync", "work", "claim", "release", "inbox", "reply",
17
22
  "ack", "message", "request", "finish"]],
18
- ["Driven by adapters, not by people", ["attach", "heartbeat", "detach"]],
23
+ ["Session lifecycle", ["attach", "heartbeat", "detach"]],
19
24
  ["About acc", ["help", "version", "update"]],
20
25
  ]);
21
26
 
@@ -25,22 +30,22 @@ const SUMMARY = Object.freeze({
25
30
  doctor: "clients, versions, install health, and what to run next",
26
31
  config: "write or check acc.workspace.json (init | validate)",
27
32
  status: "who else is here, what they hold, how protected this workspace is",
28
- sync: "what has happened since a cursor; silent while you are alone",
33
+ sync: "events since a cursor, or bounded message history",
29
34
  work: "publish what this session is doing, or --clear when it has stopped",
30
35
  claim: "reserve a resource; exit 5 when someone else already holds it",
31
36
  release: "give a claim back",
32
37
  ack: "answer a message that asked for one, so it stops asking",
33
38
  message: "send a typed message to named participants",
34
- inbox: "read only unresolved messages addressed to this participant",
39
+ inbox: "list pending message headers; --message reads one complete message",
35
40
  reply: "reply to one message and acknowledge it in the same operation",
36
41
  request: "ask another agent to do something in a reply-required message",
37
42
  finish: "write the handoff and release what this session held",
38
- attach: "open a session; an adapter calls this, not a person",
43
+ attach: "open a manual session and return its session/generation pair",
39
44
  heartbeat: "say the session is still alive",
40
45
  detach: "close a session",
41
- help: "this list",
46
+ help: "list commands, or inspect one with acc help <command>",
42
47
  version: "print the version that is installed",
43
- update: "ask npm whether a newer acc exists; --apply installs it",
48
+ update: "install a newer acc and refresh clients; --check only reports",
44
49
  });
45
50
 
46
51
  /** The same list `acc help --json` returns, so a tool can read it too. */
@@ -59,6 +64,96 @@ export function describeCommands() {
59
64
  const DOCS = "https://github.com/automatis-tools/agents-can-communicate"
60
65
  + "/blob/main/docs/CLI.md";
61
66
 
67
+ const ADAPTER_IDS = Object.freeze(ALL_ADAPTERS().map(adapter => adapter.id));
68
+ const CHOICES = Object.freeze({
69
+ work: { mode: INTENT_MODES, state: INTENT_STATES },
70
+ claim: { mode: CLAIM_MODES, enforcement: CLAIM_ENFORCEMENTS },
71
+ finish: { status: HANDOFF_STATUSES },
72
+ message: { type: GENERIC_MESSAGE_KINDS, obligation: OBLIGATIONS },
73
+ sync: { scope: ["delta", "full", "history"], type: MESSAGE_KINDS },
74
+ install: { delivery: LIVE_POLICIES, adapter: ADAPTER_IDS },
75
+ uninstall: { adapter: ADAPTER_IDS },
76
+ });
77
+
78
+ const NOTES = Object.freeze({
79
+ attach: ["Native hooks already manage their own session. Attach only for a manual CLI session.",
80
+ "Keep the returned session and generation together for subsequent calls.",
81
+ "--cadence is a positive interval in milliseconds."],
82
+ work: ["Provide --summary unless using --clear. Default mode: edit; default state: active."],
83
+ claim: ["--lease is in seconds. Default mode: exclusive; default enforcement: advisory.",
84
+ "For files, use file:src/a.mjs or file:src/**. A claim may conflict with a peer."],
85
+ release: ["Provide --claim or --resource. Releasing another owner's claim requires human or policy authority."],
86
+ message: ["Default type: note. question/request require --to and obligation reply; note uses none.",
87
+ "decision permits none, or acknowledge when addressed. Use reply for answers and finish for handoffs.",
88
+ "For decisions, repeat --supersedes or --withdraws with 1..16 prior decision IDs; never combine the two.",
89
+ "Changes inherit target recipients and authors, including offline participants. Any peer may record a change; competing branches remain visible."],
90
+ inbox: ["Without --message, returns read-only {items, nextCursor} summary pages, newest first.",
91
+ "Default: 20 items, at most 12000 bytes of formatted page JSON. --limit accepts 1..500.",
92
+ "Continue with --cursor <nextCursor>; omit it to see new arrivals. Listing never retrieves bodies.",
93
+ "Replaced decisions leave the pending list without changing receipts; exact inbox and history retain them.",
94
+ "--message reads one complete addressed message, including acknowledged mail; do not combine with cursor or limit.",
95
+ "Exact reads return a one-item array. Acknowledged inspection leaves the receipt unchanged."],
96
+ sync: ["Default delta uses 16-digit event cursors. full adds an unbounded diagnostic snapshot.",
97
+ "history lists read-only message summaries, newest first: default 20 items, at most 12000 bytes of formatted page JSON.",
98
+ "With history, --type filters before paging; --cursor takes the complete message ID in nextCursor.",
99
+ "history --type decision --current lists terminal decisions and withdrawals, including conflicts. Current means no recorded successor, not consensus.",
100
+ "history --message reads one complete historical message without changing receipts; do not combine with cursor, limit, or type."],
101
+ reply: ["Returns the outgoing message/delivery plus the original acknowledged receipt."],
102
+ finish: ["Default status: partial. Records a handoff, releases claims, and closes this ACC session."],
103
+ config: ["init writes optional workspace configuration; validate only checks it.",
104
+ "Use acc config <subcommand> --help for its options."],
105
+ update: ["Run acc update to install the latest release and refresh client integrations.",
106
+ "Use acc update --check to only report whether a newer release exists.",
107
+ "Automatic updates are on after acc install; activation waits for active clients and ACC processes.",
108
+ "--auto on|off changes automatic updates; --pin VERSION holds an exact stable version; --pin none follows stable releases.",
109
+ "--apply remains accepted as a compatibility alias for acc update."],
110
+ });
111
+
112
+ const CONFIG_HELP = Object.freeze({
113
+ init: { summary: "write optional workspace configuration",
114
+ notes: ["init requires confirmation or --yes. --force bypasses the active-session check; an existing config is never overwritten."] },
115
+ validate: { summary: "check optional workspace configuration",
116
+ notes: ["Checks acc.workspace.json in the selected directory without writing it."] },
117
+ });
118
+
119
+ export function describeCommand(name, subcommand) {
120
+ const spec = commandSpec(name, subcommand);
121
+ const detail = name === "config" ? CONFIG_HELP[subcommand] : undefined;
122
+ return { name, summary: detail?.summary ?? SUMMARY[name],
123
+ ...(subcommand === undefined ? {} : { subcommand }),
124
+ required: spec.required ?? [], optional: spec.optional ?? [],
125
+ repeated: spec.repeated ?? [], flags: spec.flags ?? [],
126
+ subcommands: spec.subcommands ?? [], choices: CHOICES[name] ?? {},
127
+ notes: [...(detail?.notes ?? NOTES[name] ?? []), ...(needsOwner(name)
128
+ ? ["Use your own --session and --generation from the ACC hook or manual attach; never a peer's pair."]
129
+ : [])] };
130
+ }
131
+
132
+ export function commandHelpText(command) {
133
+ const { name, summary, subcommand, subcommands, choices, notes } = command;
134
+ const suffix = subcommand !== undefined ? ` ${subcommand}`
135
+ : subcommands.length > 0 ? ` <${subcommands.join("|")}>` : "";
136
+ const lines = [`acc ${name} - ${summary}`, "", `Usage: acc ${name}${suffix} [options]`, ""];
137
+ for (const [key, label] of [["required", "Required"], ["optional", "Optional"],
138
+ ["repeated", "Repeatable"], ["flags", "Flags"]]) {
139
+ if (command[key].length === 0) continue;
140
+ lines.push(`${label}:`);
141
+ for (const option of command[key]) {
142
+ const value = key === "flags" ? "" : ` <${choices[option]?.join("|") ?? "value"}>`;
143
+ lines.push(` --${option}${value}`);
144
+ }
145
+ lines.push("");
146
+ }
147
+ const globals = ["--json", "--help", "-h"];
148
+ if (!["install", "uninstall", "update", "help", "version"].includes(name)) {
149
+ globals.push("--cwd <path>");
150
+ if (name !== "config" || subcommand !== "validate") globals.push("--workspace <config>");
151
+ }
152
+ lines.push(`Global: ${globals.join(", ")}`, "", ...notes,
153
+ `Full reference: ${DOCS}`);
154
+ return lines.join("\n");
155
+ }
156
+
62
157
  export function helpText() {
63
158
  const width = Math.max(...Object.keys(COMMANDS).map(name => name.length)) + 4;
64
159
  const lines = ["acc - coordinate the agent sessions you already opened", ""];
@@ -69,10 +164,10 @@ export function helpText() {
69
164
  }
70
165
  lines.push("");
71
166
  }
72
- // A URL rather than a path: only `docs/CAPABILITIES.md` is packed, so a
73
- // reference to `docs/CLI.md` would name a file the installed package does not
74
- // have. The test alongside holds this to the repository the manifest names.
75
- lines.push("Every command takes --json for machine output and --cwd to choose the workspace.",
167
+ // The longer reference is linked to the repository named in the manifest;
168
+ // installed command help supplies local syntax without needing that link.
169
+ lines.push("Every command takes --json for machine output.",
170
+ "Use acc <command> --help or acc help <command> for options and accepted values.",
76
171
  `Full reference: ${DOCS}`);
77
172
  return lines.join("\n");
78
173
  }