agents-can-communicate 0.1.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.
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/bin/acc-hook.mjs +53 -0
- package/bin/acc-mcp.mjs +46 -0
- package/bin/acc.mjs +24 -0
- package/docs/CAPABILITIES.md +153 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +13 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.claude-plugin/plugin.json +5 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/hooks.json +56 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +217 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +106 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +278 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +13 -0
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/hooks.json +55 -0
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +217 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +81 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +133 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +235 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +6 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/hooks/hooks.json +69 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +217 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +13 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +74 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/hooks.mjs +109 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +149 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +13 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/.kimi-plugin/plugin.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +217 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +73 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +125 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +216 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +101 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/config-merge.mjs +215 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +217 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +55 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +148 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +16 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +95 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +71 -0
- package/node_modules/@agents-can-communicate/cli/package.json +12 -0
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +163 -0
- package/node_modules/@agents-can-communicate/cli/src/claim-spelling.mjs +79 -0
- package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +149 -0
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +85 -0
- package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +41 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +78 -0
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +10 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +107 -0
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +344 -0
- package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +109 -0
- package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +58 -0
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +126 -0
- package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +151 -0
- package/node_modules/@agents-can-communicate/core/package.json +12 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +180 -0
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +334 -0
- package/node_modules/@agents-can-communicate/core/src/index.mjs +7 -0
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +75 -0
- package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +86 -0
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +95 -0
- package/node_modules/@agents-can-communicate/core/src/participants.mjs +48 -0
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +56 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +44 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +190 -0
- package/node_modules/@agents-can-communicate/core/src/status.mjs +132 -0
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +273 -0
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +238 -0
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +109 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +12 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +379 -0
- package/node_modules/@agents-can-communicate/installer/package.json +10 -0
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +58 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +79 -0
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +6 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +162 -0
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +62 -0
- package/node_modules/@agents-can-communicate/mcp-server/package.json +12 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +58 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +250 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +201 -0
- package/node_modules/@agents-can-communicate/protocol/package.json +12 -0
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +151 -0
- package/node_modules/@agents-can-communicate/protocol/src/envelopes.mjs +25 -0
- package/node_modules/@agents-can-communicate/protocol/src/errors.mjs +30 -0
- package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +103 -0
- package/node_modules/@agents-can-communicate/protocol/src/ids.mjs +25 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +9 -0
- package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +74 -0
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +175 -0
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +55 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +12 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +135 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +67 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +4 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +87 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/record-id.mjs +44 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +114 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +80 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +55 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +292 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +87 -0
- package/package.json +81 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
5
|
+
import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
6
|
+
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
7
|
+
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
8
|
+
import { applyPlan, detectInstallation, planInstallation }
|
|
9
|
+
from "@agents-can-communicate/installer";
|
|
10
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
11
|
+
|
|
12
|
+
import { platformPaths } from "./platform-paths.mjs";
|
|
13
|
+
|
|
14
|
+
// Where each client keeps its own configuration. All of it derives from one
|
|
15
|
+
// home, so a test - or an operator with a second account - can point the whole
|
|
16
|
+
// installation somewhere else in one move.
|
|
17
|
+
// Each client keeps its own directory under the user's home, and an adapter
|
|
18
|
+
// pointed at the home itself writes beside them rather than inside them. That
|
|
19
|
+
// install reports success and the client never reads a byte of it.
|
|
20
|
+
export const clientContext = home => ({
|
|
21
|
+
home,
|
|
22
|
+
configDir: path.join(home, ".claude"),
|
|
23
|
+
agentsHome: home,
|
|
24
|
+
codexHome: path.join(home, ".codex"),
|
|
25
|
+
kimiHome: path.join(home, ".kimi-code"),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const ALL_ADAPTERS = () => [createClaudeCodeAdapter(), createCodexAdapter(),
|
|
29
|
+
createGeminiCliAdapter(), createKimiAdapter()];
|
|
30
|
+
|
|
31
|
+
function selectAdapters(requested) {
|
|
32
|
+
const all = ALL_ADAPTERS();
|
|
33
|
+
if (requested === undefined) return all;
|
|
34
|
+
const wanted = Array.isArray(requested) ? requested : [requested];
|
|
35
|
+
const known = new Map(all.map(adapter => [adapter.id, adapter]));
|
|
36
|
+
return wanted.map(id => {
|
|
37
|
+
const adapter = known.get(id);
|
|
38
|
+
if (adapter === undefined) {
|
|
39
|
+
throw new AccError(EXIT.USAGE, `unknown adapter: ${id}`,
|
|
40
|
+
{ adapter: id, known: [...known.keys()] });
|
|
41
|
+
}
|
|
42
|
+
return adapter;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `acc install`, `acc install --dry-run`, and `acc uninstall`.
|
|
48
|
+
*
|
|
49
|
+
* Detection, planning and application are three separate steps on purpose:
|
|
50
|
+
* detection only reads, the plan is deterministic JSON that says exactly what
|
|
51
|
+
* would change, and application is the only step that writes. `--dry-run` shows
|
|
52
|
+
* the plan and stops - so what an operator approves is the same object that is
|
|
53
|
+
* then carried out, not a description of it produced somewhere else.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* What the command says it did.
|
|
57
|
+
*
|
|
58
|
+
* `installed 0 adapter(s)` was the whole report whether four clients were
|
|
59
|
+
* absent, one refused, or nothing was asked for. The reasons existed - the plan
|
|
60
|
+
* carries one per skipped adapter and the result one per failure - and only
|
|
61
|
+
* `--json` ever showed them.
|
|
62
|
+
*/
|
|
63
|
+
export function describeOutcome({ action, acted, failed = [], skipped = [] }) {
|
|
64
|
+
return [`${action}ed ${acted} adapter(s)`
|
|
65
|
+
+ (failed.length > 0 ? `; ${failed.length} failed` : ""),
|
|
66
|
+
...skipped.map(entry => ` skip ${entry.adapterId}: ${entry.reason}`),
|
|
67
|
+
...failed.map(entry => ` ${entry.adapterId}: ${entry.error}`)].join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A failed adapter ends the command.
|
|
72
|
+
*
|
|
73
|
+
* It used to be counted in a line that began with a success and exit 0, which is
|
|
74
|
+
* what a malformed `~/.claude/settings.json` produced: the adapter refused,
|
|
75
|
+
* correctly, and the script that ran the installer was told it had worked.
|
|
76
|
+
*/
|
|
77
|
+
export function failureOf({ action, acted, failed = [] }) {
|
|
78
|
+
if (failed.length === 0) return null;
|
|
79
|
+
return new AccError(EXIT.DATA,
|
|
80
|
+
describeOutcome({ action, acted, failed }), { failed });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function runInstallCommand({ options, runtime, action = "install" }) {
|
|
84
|
+
const adapters = selectAdapters(options.adapter);
|
|
85
|
+
const home = options.home ?? runtime.env?.HOME ?? homedir();
|
|
86
|
+
const context = clientContext(home);
|
|
87
|
+
const { data: dataHome } = platformPaths({ platform: runtime.platform,
|
|
88
|
+
env: runtime.env ?? {} });
|
|
89
|
+
|
|
90
|
+
const detected = await detectInstallation({ adapters, context });
|
|
91
|
+
const plan = planInstallation({ adapters, detected, context, action });
|
|
92
|
+
|
|
93
|
+
const dryRun = options.dryRun === true;
|
|
94
|
+
const result = await applyPlan({ plan, adapters, context, dataHome, dryRun });
|
|
95
|
+
|
|
96
|
+
const acted = result.operations.filter(operation => operation.applied).length;
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
return { data: { ...result, plan, dataHome },
|
|
99
|
+
text: [`would ${action}:`, ...plan.operations.flatMap(operation => operation.summary),
|
|
100
|
+
...plan.skipped.map(entry => `skip ${entry.adapterId}: ${entry.reason}`)].join("\n") };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { data: { ...result, plan, dataHome },
|
|
104
|
+
text: describeOutcome({ action, acted, failed: result.failed,
|
|
105
|
+
skipped: plan.skipped }),
|
|
106
|
+
error: failureOf({ action, acted, failed: result.failed }) };
|
|
107
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { AccError, CONFIG_FILENAME, EXIT, failure, ok }
|
|
2
|
+
from "@agents-can-communicate/protocol";
|
|
3
|
+
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
4
|
+
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
5
|
+
|
|
6
|
+
import { parseArgs, positiveNumber } from "./args.mjs";
|
|
7
|
+
|
|
8
|
+
// A usage error names what is missing rather than failing deeper in a service
|
|
9
|
+
// with the argument already half-applied.
|
|
10
|
+
const usage = message => new AccError(EXIT.USAGE, message);
|
|
11
|
+
import { describeCommands, helpText } from "./help.mjs";
|
|
12
|
+
import { runConfigCommand } from "./config-command.mjs";
|
|
13
|
+
import { runInstallCommand } from "./install-command.mjs";
|
|
14
|
+
import { runDoctor } from "./doctor-command.mjs";
|
|
15
|
+
import { createGitProbe } from "./git-probe.mjs";
|
|
16
|
+
import { canonicalClaim } from "./claim-spelling.mjs";
|
|
17
|
+
import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
18
|
+
import { resolveOwner } from "./session-owner.mjs";
|
|
19
|
+
import { discoverWorkspace } from "./workspace-discovery.mjs";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_CADENCE_MS = 30_000;
|
|
22
|
+
|
|
23
|
+
async function openContext(options, runtime) {
|
|
24
|
+
const descriptor = await discoverWorkspace({
|
|
25
|
+
cwd: options.cwd ?? runtime.cwd,
|
|
26
|
+
env: runtime.env,
|
|
27
|
+
gitProbe: runtime.gitProbe ?? createGitProbe(),
|
|
28
|
+
explicitConfig: options.workspace,
|
|
29
|
+
});
|
|
30
|
+
const paths = runtimePaths({
|
|
31
|
+
dataHome: runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
|
|
32
|
+
env: runtime.env }),
|
|
33
|
+
workspaceId: descriptor.id,
|
|
34
|
+
workspaceRoots: descriptor.roots,
|
|
35
|
+
});
|
|
36
|
+
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
37
|
+
ids: runtime.ids, workspaceId: descriptor.id });
|
|
38
|
+
return { descriptor, paths,
|
|
39
|
+
service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const human = value => (typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
43
|
+
|
|
44
|
+
const HANDLERS = Object.freeze({
|
|
45
|
+
attach: async ({ options, context }) => {
|
|
46
|
+
const session = await context.service.openSession({
|
|
47
|
+
workspaceId: context.descriptor.id,
|
|
48
|
+
participantId: options.participant,
|
|
49
|
+
displayName: options.participant,
|
|
50
|
+
harness: options.harness ?? "cli",
|
|
51
|
+
parentSessionId: options.parent ?? null,
|
|
52
|
+
heartbeatCadenceMs: options.cadence
|
|
53
|
+
? positiveNumber(options.cadence, "cadence") : DEFAULT_CADENCE_MS,
|
|
54
|
+
sessionId: options.session,
|
|
55
|
+
descriptor: context.descriptor,
|
|
56
|
+
});
|
|
57
|
+
// The adapter needs both to prove ownership later, so both are printed.
|
|
58
|
+
return { data: { sessionId: session.sessionId, generation: session.generation },
|
|
59
|
+
text: `${session.sessionId} ${session.generation}` };
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
heartbeat: async ({ options, context }) => {
|
|
63
|
+
const session = await context.service.heartbeatSession({ sessionId: options.session,
|
|
64
|
+
generation: options.generation });
|
|
65
|
+
return { data: { heartbeatAt: session.heartbeatAt }, text: session.heartbeatAt };
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
detach: async ({ options, context }) => {
|
|
69
|
+
const session = await context.service.closeSession({ sessionId: options.session,
|
|
70
|
+
generation: options.generation });
|
|
71
|
+
return { data: { sessionId: session.sessionId, state: session.state },
|
|
72
|
+
text: `closed ${session.sessionId}` };
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
sync: async ({ options, context }) => {
|
|
76
|
+
const result = await context.service.sync({ sessionId: options.session,
|
|
77
|
+
cursor: options.cursor ?? null, scope: options.scope,
|
|
78
|
+
limit: options.limit ? positiveNumber(options.limit, "limit") : undefined });
|
|
79
|
+
// Solo zero-overhead: nothing to say means nothing printed, not a banner.
|
|
80
|
+
const text = result.solo ? "" : `${result.attention.length} attention; `
|
|
81
|
+
+ `${result.roster.length} session(s); cursor ${result.cursor}`;
|
|
82
|
+
return { data: result, text };
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
work: async ({ options, context }) => {
|
|
86
|
+
// Saying nothing and saying "I have stopped" are different. Without this an
|
|
87
|
+
// intent stayed on the roster until the session closed, so peers read a
|
|
88
|
+
// finished piece of work as still in progress.
|
|
89
|
+
if (options.clear === true) {
|
|
90
|
+
const cleared = await context.service.clearIntent({ sessionId: options.session,
|
|
91
|
+
generation: options.generation });
|
|
92
|
+
return { data: cleared, text: "intent cleared" };
|
|
93
|
+
}
|
|
94
|
+
if (options.summary === undefined) throw usage("work requires --summary");
|
|
95
|
+
const intent = await context.service.setIntent({ sessionId: options.session,
|
|
96
|
+
generation: options.generation, summary: options.summary, mode: options.mode ?? "edit",
|
|
97
|
+
state: options.state, workstreamId: options.workstream ?? null,
|
|
98
|
+
resourceHints: options.hint ?? [] });
|
|
99
|
+
return { data: intent, text: `intent: ${intent.summary}` };
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
claim: async ({ options, context }) => {
|
|
103
|
+
const claim = await context.service.acquireClaim({ sessionId: options.session,
|
|
104
|
+
generation: options.generation,
|
|
105
|
+
resource: await canonicalClaim(options.resource, context.descriptor),
|
|
106
|
+
mode: options.mode ?? "exclusive", enforcement: options.enforcement ?? "advisory",
|
|
107
|
+
reason: options.reason ?? "unspecified",
|
|
108
|
+
leaseSeconds: options.lease ? positiveNumber(options.lease, "lease") : undefined,
|
|
109
|
+
descriptor: context.descriptor });
|
|
110
|
+
return { data: claim, text: `claimed ${claim.resource}` };
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
release: async ({ options, context }) => {
|
|
114
|
+
const request = { claimId: options.claim, sessionId: options.session,
|
|
115
|
+
generation: options.generation, authority: options.authority, reason: options.reason };
|
|
116
|
+
if (options.authority === undefined) await context.service.releaseClaim(request);
|
|
117
|
+
else await context.service.forceReleaseClaim(request);
|
|
118
|
+
return { data: { claimId: options.claim }, text: `released ${options.claim}` };
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
message: async ({ options, context }) => {
|
|
122
|
+
const message = await context.service.sendMessage({ sessionId: options.session,
|
|
123
|
+
generation: options.generation, toParticipantIds: options.to ?? [],
|
|
124
|
+
type: options.type ?? "note", subject: options.subject, body: options.body,
|
|
125
|
+
priority: options.priority, workstreamId: options.workstream ?? null,
|
|
126
|
+
requiresAck: options.requiresAck === true, descriptor: context.descriptor });
|
|
127
|
+
return { data: message, text: `sent ${message.messageId}` };
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Ask another agent to do something. One call, one write.
|
|
132
|
+
*
|
|
133
|
+
* The recipient hears about it twice by design: the task raises an attention
|
|
134
|
+
* item for the participant it names, and the message reaches their turn as
|
|
135
|
+
* quoted peer text explaining why.
|
|
136
|
+
*/
|
|
137
|
+
request: async ({ options, context }) => {
|
|
138
|
+
const { task, message } = await context.service.requestWork({
|
|
139
|
+
sessionId: options.session, generation: options.generation,
|
|
140
|
+
toParticipantId: options.to, title: options.title, detail: options.detail,
|
|
141
|
+
workstreamId: options.workstream, priority: options.priority,
|
|
142
|
+
dependsOn: options.dependsOn ?? [], descriptor: context.descriptor });
|
|
143
|
+
return { data: { task, message },
|
|
144
|
+
text: `requested ${task.taskId} of ${options.to}` };
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
ack: async ({ options, context }) => {
|
|
148
|
+
const receipt = await context.service.markDelivery({ sessionId: options.session,
|
|
149
|
+
generation: options.generation, messageId: options.message,
|
|
150
|
+
state: options.state ?? "acknowledged" });
|
|
151
|
+
return { data: receipt, text: `${receipt.messageId} ${receipt.state}` };
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* What was settled, and on whose authority.
|
|
156
|
+
*
|
|
157
|
+
* `--human` is the caller stating that a person actually decided this. The
|
|
158
|
+
* core refuses `--authority human` without it, because a peer proposal
|
|
159
|
+
* becoming a human decision on its own is the one way this record could
|
|
160
|
+
* launder an agent's opinion into a ruling.
|
|
161
|
+
*/
|
|
162
|
+
decide: async ({ options, context }) => {
|
|
163
|
+
const decision = await context.service.recordDecision({
|
|
164
|
+
sessionId: options.session, generation: options.generation,
|
|
165
|
+
title: options.title, outcome: options.outcome,
|
|
166
|
+
authority: options.authority ?? "workstream",
|
|
167
|
+
workstreamId: options.workstream ?? null,
|
|
168
|
+
decidedBy: options.decidedBy,
|
|
169
|
+
supersedes: options.supersedes ?? null,
|
|
170
|
+
humanConfirmed: options.human === true,
|
|
171
|
+
descriptor: context.descriptor });
|
|
172
|
+
return { data: decision,
|
|
173
|
+
text: `${decision.decisionId} ${decision.authority}: ${decision.title}` };
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
workstream: async ({ options, context }) => {
|
|
177
|
+
const owner = { sessionId: options.session, generation: options.generation };
|
|
178
|
+
// Taking the coordination of one and creating one are the same noun, so
|
|
179
|
+
// they stay one command rather than two the model has to choose between -
|
|
180
|
+
// the same shape `acc task` already has.
|
|
181
|
+
if (options.take === true || options.release === true) {
|
|
182
|
+
if (options.workstream === undefined) {
|
|
183
|
+
throw usage(`workstream --${options.take === true ? "take" : "release"} `
|
|
184
|
+
+ "requires --workstream");
|
|
185
|
+
}
|
|
186
|
+
const acted = options.take === true
|
|
187
|
+
? await context.service.acquireCoordinator({ ...owner,
|
|
188
|
+
workstreamId: options.workstream })
|
|
189
|
+
: await context.service.releaseCoordinator({ ...owner,
|
|
190
|
+
workstreamId: options.workstream });
|
|
191
|
+
return { data: acted,
|
|
192
|
+
text: `${acted.workstreamId} ${options.take === true ? "coordinated" : "released"}` };
|
|
193
|
+
}
|
|
194
|
+
if (options.title === undefined) throw usage("workstream requires --title");
|
|
195
|
+
if (options.objective === undefined) throw usage("workstream requires --objective");
|
|
196
|
+
const workstream = await context.service.createWorkstream({ ...owner,
|
|
197
|
+
title: options.title, objective: options.objective,
|
|
198
|
+
descriptor: context.descriptor });
|
|
199
|
+
return { data: workstream, text: `${workstream.workstreamId} ${workstream.state}` };
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
task: async ({ options, context }) => {
|
|
203
|
+
const owner = { sessionId: options.session, generation: options.generation };
|
|
204
|
+
// Taking work and moving it along are the same noun as creating it, so they
|
|
205
|
+
// stay one command rather than three the model has to choose between.
|
|
206
|
+
if (options.take === true) {
|
|
207
|
+
if (options.task === undefined) throw usage("task --take requires --task");
|
|
208
|
+
const taken = await context.service.claimTask({ ...owner, taskId: options.task,
|
|
209
|
+
force: options.force === true });
|
|
210
|
+
return { data: taken, text: `${taken.taskId} ${taken.state}` };
|
|
211
|
+
}
|
|
212
|
+
if (options.decline === true) {
|
|
213
|
+
if (options.task === undefined) throw usage("task --decline requires --task");
|
|
214
|
+
const refused = await context.service.declineTask({ ...owner,
|
|
215
|
+
taskId: options.task, reason: options.reason });
|
|
216
|
+
return { data: refused, text: `${refused.taskId} declined` };
|
|
217
|
+
}
|
|
218
|
+
if (options.state !== undefined) {
|
|
219
|
+
if (options.task === undefined) throw usage("task --state requires --task");
|
|
220
|
+
const moved = await context.service.transitionTask({ ...owner,
|
|
221
|
+
taskId: options.task, state: options.state });
|
|
222
|
+
return { data: moved, text: `${moved.taskId} ${moved.state}` };
|
|
223
|
+
}
|
|
224
|
+
if (options.title === undefined) throw usage("task requires --title");
|
|
225
|
+
const task = await context.service.createTask({ ...owner,
|
|
226
|
+
workstreamId: options.workstream, title: options.title, detail: options.detail,
|
|
227
|
+
assigneeParticipantId: options.assignee, taskId: options.task,
|
|
228
|
+
dependsOn: options.dependsOn ?? [], descriptor: context.descriptor });
|
|
229
|
+
return { data: task, text: `${task.taskId} ${task.state}` };
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
finish: async ({ options, context }) => {
|
|
233
|
+
const handoff = await context.service.finishSession({ sessionId: options.session,
|
|
234
|
+
generation: options.generation, goal: options.goal, status: options.status,
|
|
235
|
+
toParticipantId: options.to ?? null, completed: options.completed ?? [],
|
|
236
|
+
remaining: options.remaining ?? [], blockers: options.blocker ?? [] });
|
|
237
|
+
return { data: handoff, text: `handoff ${handoff.handoffId}` };
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
status: async ({ options, context }) => {
|
|
241
|
+
const status = await context.service.collectStatus({
|
|
242
|
+
participantId: options.participant, all: options.all === true });
|
|
243
|
+
const text = `${status.counts.live} live; ${status.counts.claims} claim(s); `
|
|
244
|
+
+ `protection ${status.protection}`;
|
|
245
|
+
return { data: status, text };
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
config: async ({ options, runtime }) => {
|
|
249
|
+
const result = await runConfigCommand({ subcommand: options.subcommand,
|
|
250
|
+
cwd: options.cwd ?? runtime.cwd,
|
|
251
|
+
// A pipe is not a person. Without a TTY there is nobody to answer, so the
|
|
252
|
+
// command demands --yes rather than hanging or assuming consent.
|
|
253
|
+
interactive: runtime.stdout?.isTTY === true,
|
|
254
|
+
yes: options.yes === true,
|
|
255
|
+
confirm: runtime.confirm ?? (async () => false),
|
|
256
|
+
force: options.force === true,
|
|
257
|
+
// Opened lazily and only by `init`: `config validate` has to work on a
|
|
258
|
+
// workspace discovery cannot open, which is what a reader runs it to
|
|
259
|
+
// find out.
|
|
260
|
+
probeWorkspace: () => openContext(options, runtime),
|
|
261
|
+
ids: runtime.ids });
|
|
262
|
+
if (result.subcommand === "validate") {
|
|
263
|
+
return { data: result, text: result.present
|
|
264
|
+
? `${result.file} is valid` : `no ${CONFIG_FILENAME}; defaults apply` };
|
|
265
|
+
}
|
|
266
|
+
return { data: result,
|
|
267
|
+
text: result.written ? `wrote ${result.file}` : `not written: ${result.file}` };
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
install: async ({ options, runtime }) =>
|
|
271
|
+
runInstallCommand({ options, runtime, action: "install" }),
|
|
272
|
+
|
|
273
|
+
uninstall: async ({ options, runtime }) =>
|
|
274
|
+
runInstallCommand({ options, runtime, action: "uninstall" }),
|
|
275
|
+
|
|
276
|
+
doctor: async ({ options, context, runtime }) => runDoctor({ options, context, runtime }),
|
|
277
|
+
|
|
278
|
+
help: async () => ({ data: { commands: describeCommands() }, text: helpText() }),
|
|
279
|
+
|
|
280
|
+
version: async ({ runtime }) => {
|
|
281
|
+
// Read by the composition root from the package manifest: `bin/` sits at
|
|
282
|
+
// the same depth in the published package as it does in this tree, which is
|
|
283
|
+
// not true of anything under `packages/`. A version typed into the source
|
|
284
|
+
// would be one more place the next bump has to reach, and the kind that
|
|
285
|
+
// goes stale without failing.
|
|
286
|
+
if (typeof runtime.version !== "function") {
|
|
287
|
+
throw new AccError(EXIT.DATA, "this build was assembled without a version to report");
|
|
288
|
+
}
|
|
289
|
+
const version = await runtime.version();
|
|
290
|
+
return { data: { version }, text: version };
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @returns {Promise<number>} the process exit code
|
|
296
|
+
*/
|
|
297
|
+
const NO_WORKSPACE = Object.freeze(["config", "install", "uninstall", "help", "version"]);
|
|
298
|
+
|
|
299
|
+
export async function main(argv, runtime) {
|
|
300
|
+
const write = (stream, text) => new Promise((resolve, reject) =>
|
|
301
|
+
stream.write(text, error => (error ? reject(error) : resolve())));
|
|
302
|
+
let parsed;
|
|
303
|
+
try {
|
|
304
|
+
parsed = parseArgs(argv);
|
|
305
|
+
// `config` is the one command that must work on a workspace ACC cannot
|
|
306
|
+
// open. Discovery validates the config too, so a broken one would fail
|
|
307
|
+
// there first and `acc config validate` - the command a user runs to find
|
|
308
|
+
// out what is wrong - would never reach its own report.
|
|
309
|
+
// These work on a machine, not a workspace. `config` must run even when
|
|
310
|
+
// discovery cannot open the workspace - that is what a user is trying to
|
|
311
|
+
// find out - install touches client configuration rather than ACC state,
|
|
312
|
+
// and `help` has to answer in a directory that is no workspace at all.
|
|
313
|
+
const context = NO_WORKSPACE.includes(parsed.command)
|
|
314
|
+
? null
|
|
315
|
+
: await openContext(parsed.options, runtime);
|
|
316
|
+
// Which session is calling is answered once, here, rather than by each
|
|
317
|
+
// handler: every one of them needs the same pair, and a handler that forgot
|
|
318
|
+
// to ask would be an operation an agent cannot reach.
|
|
319
|
+
const options = context === null ? parsed.options
|
|
320
|
+
: await resolveOwner({ command: parsed.command, options: parsed.options,
|
|
321
|
+
context, env: runtime.env });
|
|
322
|
+
const { data, text, error: outcome } = await HANDLERS[parsed.command](
|
|
323
|
+
{ options, context, runtime });
|
|
324
|
+
// A handler may have done real work and still failed: `acc install` writes
|
|
325
|
+
// for the clients it could and reports the one it could not. The data is
|
|
326
|
+
// printed either way, and the command still fails.
|
|
327
|
+
if (outcome != null) throw Object.assign(outcome, { details: { ...outcome.details, ...data } });
|
|
328
|
+
// Machine mode writes exactly one JSON object to stdout and nothing else.
|
|
329
|
+
if (parsed.options.json === true) await write(runtime.stdout, `${JSON.stringify(ok(data))}\n`);
|
|
330
|
+
else if (text !== "") await write(runtime.stdout, `${human(text)}\n`);
|
|
331
|
+
return EXIT.OK;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
const code = error instanceof AccError ? error.code : EXIT.DATA;
|
|
334
|
+
// A usage error happens before the parse completes, so machine mode has to
|
|
335
|
+
// be recognised from the raw argv. Otherwise the one caller that cannot
|
|
336
|
+
// read prose - an adapter - gets prose exactly when it made a mistake.
|
|
337
|
+
if (parsed?.options?.json === true || argv.includes("--json")) {
|
|
338
|
+
await write(runtime.stdout, `${JSON.stringify(failure(error))}\n`);
|
|
339
|
+
} else {
|
|
340
|
+
await write(runtime.stderr, `${error.message}\n`);
|
|
341
|
+
}
|
|
342
|
+
return code;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
4
|
+
|
|
5
|
+
// Resolution is a pure function of platform and environment, so the Windows and
|
|
6
|
+
// Linux answers are testable from a machine that is neither. Reading
|
|
7
|
+
// process.platform inside would mean two of the three branches are only ever
|
|
8
|
+
// exercised by whoever happens to run them.
|
|
9
|
+
const AREAS = Object.freeze(["data", "config", "cache"]);
|
|
10
|
+
|
|
11
|
+
const OVERRIDE = Object.freeze({
|
|
12
|
+
data: "ACC_DATA_HOME",
|
|
13
|
+
config: "ACC_CONFIG_HOME",
|
|
14
|
+
cache: "ACC_CACHE_HOME",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const XDG = Object.freeze({
|
|
18
|
+
data: "XDG_DATA_HOME",
|
|
19
|
+
config: "XDG_CONFIG_HOME",
|
|
20
|
+
cache: "XDG_CACHE_HOME",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// An exported-but-empty variable is the normal shape of "unset" in a shell
|
|
24
|
+
// script. Treating it as a path puts runtime state at the filesystem root.
|
|
25
|
+
const set = value => typeof value === "string" && value.length > 0;
|
|
26
|
+
|
|
27
|
+
const usage = (message, details) => {
|
|
28
|
+
throw new AccError(EXIT.USAGE, message, details);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function windowsPaths(env) {
|
|
32
|
+
if (!set(env.APPDATA)) {
|
|
33
|
+
usage("cannot resolve the Windows application data directory");
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
data: env.APPDATA,
|
|
37
|
+
config: env.APPDATA,
|
|
38
|
+
// Roaming is the honest fallback: a cache that roams is wasteful, not wrong.
|
|
39
|
+
cache: set(env.LOCALAPPDATA) ? env.LOCALAPPDATA : env.APPDATA,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireHome(env) {
|
|
44
|
+
if (!set(env.HOME)) usage("cannot resolve the user home directory");
|
|
45
|
+
return env;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function macosPaths(env) {
|
|
49
|
+
const support = path.join(env.HOME, "Library", "Application Support");
|
|
50
|
+
// XDG variables are deliberately ignored here. They are common on a machine
|
|
51
|
+
// that also runs Linux tooling, and letting one relocate macOS state would
|
|
52
|
+
// move a user's sessions the day they install something unrelated.
|
|
53
|
+
return { data: support, config: support,
|
|
54
|
+
cache: path.join(env.HOME, "Library", "Caches") };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function xdgPaths(env) {
|
|
58
|
+
const fallback = { data: path.join(env.HOME, ".local", "share"),
|
|
59
|
+
config: path.join(env.HOME, ".config"), cache: path.join(env.HOME, ".cache") };
|
|
60
|
+
return Object.fromEntries(AREAS.map(area =>
|
|
61
|
+
[area, set(env[XDG[area]]) ? env[XDG[area]] : fallback[area]]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Where ACC keeps state, configuration, and cache on this platform.
|
|
66
|
+
*
|
|
67
|
+
* None of it is ever inside a workspace. A checkout can be deleted, cloned, or
|
|
68
|
+
* synced to another machine without carrying presence, locks, or messages with
|
|
69
|
+
* it - and "just put it in a dotfile next to the project" is the exact
|
|
70
|
+
* regression this exists to prevent, which is why passing `workspaceRoots`
|
|
71
|
+
* makes it an error rather than a convention.
|
|
72
|
+
*/
|
|
73
|
+
export function platformPaths({ platform = process.platform, env = process.env,
|
|
74
|
+
workspaceRoots = [] } = {}) {
|
|
75
|
+
// Windows paths are not absolute to a posix `path`, so the check has to use
|
|
76
|
+
// the same flavour the platform does.
|
|
77
|
+
const flavour = platform === "win32" ? path.win32 : path.posix;
|
|
78
|
+
|
|
79
|
+
const overridden = Object.fromEntries(AREAS
|
|
80
|
+
.filter(area => set(env[OVERRIDE[area]]))
|
|
81
|
+
.map(area => [area, env[OVERRIDE[area]]]));
|
|
82
|
+
|
|
83
|
+
// The platform is only consulted for what was not named outright, so a fully
|
|
84
|
+
// overridden environment never has to satisfy that platform's own
|
|
85
|
+
// prerequisites - which is what makes a test fixture for one platform
|
|
86
|
+
// runnable on another.
|
|
87
|
+
const missing = AREAS.filter(area => overridden[area] === undefined);
|
|
88
|
+
const base = missing.length === 0 ? {}
|
|
89
|
+
: platform === "win32" ? windowsPaths(env)
|
|
90
|
+
: platform === "darwin" ? macosPaths(requireHome(env))
|
|
91
|
+
: xdgPaths(requireHome(env));
|
|
92
|
+
|
|
93
|
+
const resolved = Object.fromEntries(AREAS.map(area =>
|
|
94
|
+
[area, overridden[area] ?? base[area]]));
|
|
95
|
+
|
|
96
|
+
for (const [area, location] of Object.entries(resolved)) {
|
|
97
|
+
if (!flavour.isAbsolute(location)) {
|
|
98
|
+
usage(`the ${area} location must be an absolute path`, { area, location });
|
|
99
|
+
}
|
|
100
|
+
for (const root of workspaceRoots) {
|
|
101
|
+
const relative = flavour.relative(root, location);
|
|
102
|
+
if (relative === ""
|
|
103
|
+
|| (!flavour.isAbsolute(relative) && !relative.startsWith(".."))) {
|
|
104
|
+
usage(`${area} state must not live inside the workspace`, { area, location, root });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze(resolved);
|
|
109
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protocol";
|
|
4
|
+
|
|
5
|
+
const AREAS = ["protocol", "events", "state", "locks", "ephemeral"];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{ root: string, protocol: string, events: string, state: string,
|
|
9
|
+
* locks: string, ephemeral: string }} RuntimePaths
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runtime state lives under the platform's user-data directory, never inside
|
|
14
|
+
* the workspace. A checkout can be deleted, cloned, or synced to another
|
|
15
|
+
* machine without carrying presence, locks, or messages with it.
|
|
16
|
+
*/
|
|
17
|
+
export function runtimePaths({ dataHome, workspaceId, workspaceRoots = [] }) {
|
|
18
|
+
if (typeof dataHome !== "string" || !path.isAbsolute(dataHome)) {
|
|
19
|
+
throw new AccError(EXIT.USAGE, "the data home must be an absolute path", { dataHome });
|
|
20
|
+
}
|
|
21
|
+
assertPortableId(workspaceId, "workspace id");
|
|
22
|
+
const root = path.join(dataHome, "acc", "workspaces", workspaceId);
|
|
23
|
+
// Enforced here rather than only asserted in a test, because "just put it in
|
|
24
|
+
// .agents next to the project" is the exact regression this design exists to
|
|
25
|
+
// prevent, and it would otherwise look like it works.
|
|
26
|
+
for (const workspaceRoot of workspaceRoots) {
|
|
27
|
+
const relative = path.relative(workspaceRoot, root);
|
|
28
|
+
if (relative === "" || (!path.isAbsolute(relative) && !relative.startsWith(".."))) {
|
|
29
|
+
throw new AccError(EXIT.USAGE, "runtime state must not live inside the workspace",
|
|
30
|
+
{ root, workspaceRoot });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze(Object.fromEntries([["root", root],
|
|
34
|
+
// `ephemeral` holds presence and Intent for workspaces that have not
|
|
35
|
+
// materialised durable state yet, so it is deliberately a sibling of the
|
|
36
|
+
// event log rather than a corner of it.
|
|
37
|
+
...AREAS.map(area => [area, path.join(root, area)])]));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function platformDataHome({ platform = process.platform, env = process.env } = {}) {
|
|
41
|
+
if (typeof env.ACC_DATA_HOME === "string" && env.ACC_DATA_HOME.length > 0) {
|
|
42
|
+
return env.ACC_DATA_HOME;
|
|
43
|
+
}
|
|
44
|
+
if (platform === "win32") {
|
|
45
|
+
if (typeof env.APPDATA === "string" && env.APPDATA.length > 0) return env.APPDATA;
|
|
46
|
+
throw new AccError(EXIT.USAGE, "cannot resolve the Windows application data directory");
|
|
47
|
+
}
|
|
48
|
+
if (typeof env.XDG_DATA_HOME === "string" && env.XDG_DATA_HOME.length > 0
|
|
49
|
+
&& platform !== "darwin") {
|
|
50
|
+
return env.XDG_DATA_HOME;
|
|
51
|
+
}
|
|
52
|
+
if (typeof env.HOME !== "string" || env.HOME.length === 0) {
|
|
53
|
+
throw new AccError(EXIT.USAGE, "cannot resolve the user home directory");
|
|
54
|
+
}
|
|
55
|
+
return platform === "darwin"
|
|
56
|
+
? path.join(env.HOME, "Library", "Application Support")
|
|
57
|
+
: path.join(env.HOME, ".local", "share");
|
|
58
|
+
}
|