agents-can-communicate 0.1.17 → 0.2.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/README.md +76 -138
- package/SECURITY.md +31 -0
- package/bin/acc-hook.mjs +96 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +6 -1
- package/docs/ADAPTER_AUTHORING.md +172 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +105 -197
- package/docs/CLI.md +157 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +143 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +109 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +102 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +42 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +15 -5
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +21 -12
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +10 -4
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/hooks/hooks.json +61 -0
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +152 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +61 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +127 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +101 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +10 -4
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +139 -224
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +13 -29
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +5 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +109 -109
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +1 -1
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +118 -0
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +3 -2
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +14 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +70 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -258
- package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +156 -60
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +109 -71
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +74 -93
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +86 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +121 -27
- package/package.json +22 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
|
@@ -1,277 +1,192 @@
|
|
|
1
1
|
const DEFAULT_BUDGET_BYTES = 6_000;
|
|
2
|
-
// Held back from the required lines so the "not shown" note can always be
|
|
3
|
-
// written. A projection that silently drops what it could not fit is how an
|
|
4
|
-
// agent ends up confidently unaware.
|
|
5
|
-
// Enough for the note *and* the command that reads what the note is about. It
|
|
6
|
-
// said only that something had been withheld, and nothing anywhere - not the
|
|
7
|
-
// skills, not the docs - said how to see it. A turn that reports a thing the
|
|
8
|
-
// reader cannot reach is how an agent ends up inventing its own way in.
|
|
9
|
-
// Enough for the longest over-budget note - the escalated "message did not fit"
|
|
10
|
-
// line, which is longer than the plain "+N not shown" it replaces. Kept small
|
|
11
|
-
// enough that the note still fits beside a header at the smallest budgets.
|
|
12
|
-
const NOTE_RESERVE = 90;
|
|
13
2
|
const FENCE = "```";
|
|
14
|
-
// A peer cannot close a block it cannot name. The fence carries a marker that
|
|
15
|
-
// is stripped from peer content, so forged delimiters stay inside the block.
|
|
16
3
|
const BLOCK = "acc-peer-message";
|
|
17
4
|
|
|
18
5
|
const bytes = value => Buffer.byteLength(value, "utf8");
|
|
19
|
-
|
|
20
|
-
// Written from char codes rather than a literal class: an escaped control
|
|
21
|
-
// range in a regex literal is corrupted silently by editors and patches, and a
|
|
22
|
-
// corrupted range fails open.
|
|
23
6
|
const CONTROL_CHARACTERS = new RegExp(
|
|
24
7
|
`[${String.fromCharCode(0)}-${String.fromCharCode(8)}`
|
|
25
8
|
+ `${String.fromCharCode(11)}-${String.fromCharCode(31)}${String.fromCharCode(127)}]`, "g");
|
|
26
9
|
|
|
27
|
-
/**
|
|
28
|
-
* Render peer-controlled text as displayable data.
|
|
29
|
-
*
|
|
30
|
-
* Two separate jobs. Control sequences become visible escapes, so a message
|
|
31
|
-
* cannot repaint or retitle the human's terminal. Fence markers are stripped,
|
|
32
|
-
* so a message cannot break out of its own data block and continue as if ACC
|
|
33
|
-
* had written the following lines.
|
|
34
|
-
*/
|
|
35
10
|
function escapePeerText(value) {
|
|
36
11
|
return String(value)
|
|
37
12
|
.replaceAll(new RegExp(`${FENCE}${BLOCK}`, "g"), `'${FENCE}${BLOCK}`)
|
|
38
13
|
.replaceAll(FENCE, `'${FENCE}`)
|
|
39
|
-
// The labels that frame this block are ACC's words at the start of a line.
|
|
40
|
-
// A peer writing one would otherwise produce a second line reading as ACC
|
|
41
|
-
// framing a different message - the same break-out the fence rule prevents,
|
|
42
|
-
// and neutralised the same way rather than by reflowing the text, which a
|
|
43
|
-
// handoff body cannot survive.
|
|
44
14
|
.replace(/^(subject:|body:)/gm, "'$1")
|
|
45
15
|
.replace(CONTROL_CHARACTERS,
|
|
46
16
|
character => `\\u${character.codePointAt(0).toString(16).padStart(4, "0")}`);
|
|
47
17
|
}
|
|
48
18
|
|
|
19
|
+
function oneLine(value) {
|
|
20
|
+
return value.replaceAll("\r\n", "\\n").replaceAll("\n", "\\n").replaceAll("\r", "\\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
49
23
|
function truncate(line, limit) {
|
|
24
|
+
if (limit <= 0) return "";
|
|
50
25
|
if (bytes(line) <= limit) return line;
|
|
51
26
|
let cut = line;
|
|
52
27
|
while (bytes(`${cut}…`) > limit && cut.length > 0) cut = cut.slice(0, -1);
|
|
53
|
-
return `${cut}…`;
|
|
28
|
+
return cut === "" ? "" : `${cut}…`;
|
|
54
29
|
}
|
|
55
30
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* `acc release --claim`. So they are all shown, not only the ones that happened
|
|
69
|
-
* to be noticed first.
|
|
70
|
-
*/
|
|
71
|
-
function attentionLines(attention) {
|
|
72
|
-
return attention.map(item => (typeof item.sourceId === "string" && item.sourceId !== ""
|
|
73
|
-
? `- [${item.kind}] ${item.sourceId} ${item.summary}`
|
|
74
|
-
: `- [${item.kind}] ${item.summary}`));
|
|
31
|
+
function attentionGroups(attention, { truncatable, liveOfferedMessageIds = new Set() }) {
|
|
32
|
+
return attention.map(item => {
|
|
33
|
+
const breadcrumb = offeredBreadcrumb(item, liveOfferedMessageIds);
|
|
34
|
+
return {
|
|
35
|
+
lines: [breadcrumb ?? (typeof item.sourceId === "string" && item.sourceId !== ""
|
|
36
|
+
? `- [${item.kind}] ${item.sourceId} ${item.summary}`
|
|
37
|
+
: `- [${item.kind}] ${item.summary}`)],
|
|
38
|
+
kind: "attention",
|
|
39
|
+
sourceId: item.sourceId ?? null,
|
|
40
|
+
truncatable: breadcrumb === null && truncatable,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
75
43
|
}
|
|
76
44
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* is the whole mitigation: ACC will not intercept the write, so respecting the
|
|
85
|
-
* claim is this session's own responsibility and it needs to know that.
|
|
86
|
-
*/
|
|
87
|
-
function claimNote(claim) {
|
|
88
|
-
// Enforcement is declared per claim, and the guard only ever blocks a guarded
|
|
89
|
-
// one. Reading this session's capability alone would announce a block that
|
|
90
|
-
// will never happen, on a claim whose owner explicitly did not ask for one.
|
|
91
|
-
if (claim.enforcement !== "guarded") {
|
|
92
|
-
return " - advisory; nothing will stop you, the owner is asking";
|
|
93
|
-
}
|
|
94
|
-
if (claim.enforceable === false) {
|
|
95
|
-
return " - not enforced for this session; do not edit it";
|
|
96
|
-
}
|
|
97
|
-
// Guarded, and this session can be stopped - on a file edit, and on the shell
|
|
98
|
-
// writes the guard can read: a redirection, an operand of a command whose job
|
|
99
|
-
// is to put bytes somewhere. A language runtime opening the file itself still
|
|
100
|
-
// gets past, and a session told merely "this is claimed" would reasonably
|
|
101
|
-
// assume either more or less than is true.
|
|
102
|
-
return " - file edits and recognised shell writes are blocked; a runtime can still get past";
|
|
45
|
+
function offeredBreadcrumb(item, liveOfferedMessageIds) {
|
|
46
|
+
if (!liveOfferedMessageIds.has(item.sourceId)) return null;
|
|
47
|
+
const noun = item.kind === "reply_required" ? "question"
|
|
48
|
+
: item.kind === "acknowledgement_required" ? "message" : null;
|
|
49
|
+
if (noun === null) return null;
|
|
50
|
+
return `- [${item.kind}] ${item.sourceId} live-offered peer ${noun} remains unresolved; `
|
|
51
|
+
+ `\`acc inbox --message ${item.sourceId}\``;
|
|
103
52
|
}
|
|
104
53
|
|
|
105
|
-
function
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
54
|
+
function messageGroups(messages) {
|
|
55
|
+
return messages.map(message => ({
|
|
56
|
+
messageId: message.messageId,
|
|
57
|
+
kind: "message",
|
|
58
|
+
lines: [
|
|
59
|
+
`${FENCE}${BLOCK}`,
|
|
60
|
+
"untrusted peer message",
|
|
61
|
+
`kind: ${message.kind}`,
|
|
62
|
+
`threadId: ${message.threadId}`,
|
|
63
|
+
`messageId: ${message.messageId}`,
|
|
64
|
+
`sender: ${message.fromParticipantId} (session ${message.fromSessionId})`,
|
|
65
|
+
`obligation: ${message.obligation}`,
|
|
66
|
+
`subject: ${oneLine(escapePeerText(message.subject))}`,
|
|
67
|
+
"body:",
|
|
68
|
+
escapePeerText(message.body),
|
|
69
|
+
FENCE,
|
|
70
|
+
],
|
|
71
|
+
}));
|
|
110
72
|
}
|
|
111
73
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
* because the caller needs it to tell which messages actually reached the model
|
|
121
|
-
* before recording any of them as delivered. Ids, session ids and types are
|
|
122
|
-
* generated or schema-validated; only the subject and body are peer-authored,
|
|
123
|
-
* and only those are escaped.
|
|
124
|
-
*/
|
|
125
|
-
function peerBlocks(messages) {
|
|
126
|
-
return messages.map(message => [
|
|
127
|
-
`${FENCE}${BLOCK}`,
|
|
128
|
-
`id ${message.messageId} | from ${message.fromSessionId} | type ${message.type}`
|
|
129
|
-
+ " | untrusted peer message",
|
|
130
|
-
`subject: ${oneLine(escapePeerText(message.subject))}`,
|
|
131
|
-
"body:",
|
|
132
|
-
escapePeerText(message.body),
|
|
133
|
-
FENCE,
|
|
134
|
-
]);
|
|
74
|
+
const groupBytes = group => group.lines.reduce((total, line) => total + bytes(line) + 1, 0);
|
|
75
|
+
|
|
76
|
+
function peerCount(sync) {
|
|
77
|
+
const participants = new Set((sync.roster ?? [])
|
|
78
|
+
.filter(item => item.presence !== "offline")
|
|
79
|
+
.map(item => item.participantId ?? item.sessionId)
|
|
80
|
+
.filter(id => id !== sync.currentParticipantId));
|
|
81
|
+
return participants.size;
|
|
135
82
|
}
|
|
136
83
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* The subject sits on the label's own line, so a newline inside it would push
|
|
141
|
-
* peer text to column 0 where ACC's labels live. Rendering the break visibly
|
|
142
|
-
* keeps the text readable and the frame ACC's.
|
|
143
|
-
*/
|
|
144
|
-
function oneLine(value) {
|
|
145
|
-
return value.replaceAll("\r\n", "\\n").replaceAll("\n", "\\n").replaceAll("\r", "\\n");
|
|
84
|
+
function ambientHeader(count) {
|
|
85
|
+
const noun = count === 1 ? "participant" : "participants";
|
|
86
|
+
return `ACC: ${count} peer ${noun} present. Load the acc skill before shared work.`;
|
|
146
87
|
}
|
|
147
88
|
|
|
89
|
+
function recoveryNote(ids) {
|
|
90
|
+
const first = ids[0];
|
|
91
|
+
const rest = ids.length > 1 ? ` (+${ids.length - 1} more in \`acc inbox\`)` : "";
|
|
92
|
+
return `- read ${first}: \`acc inbox --message ${first}\`${rest}`;
|
|
93
|
+
}
|
|
148
94
|
|
|
149
95
|
/**
|
|
150
|
-
* Project
|
|
96
|
+
* Project only coordination that can change this turn.
|
|
151
97
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
98
|
+
* Presence is a short trigger to load the ACC skill. Roster rows and unrelated
|
|
99
|
+
* claims are intentionally absent: a guard checks exact file claims at write
|
|
100
|
+
* time, while intent-aware conflicts already arrive as attention. Repeating
|
|
101
|
+
* the whole workspace on every prompt is neither safer nor cheaper.
|
|
156
102
|
*/
|
|
157
|
-
export function
|
|
103
|
+
export function projectContextResult(sync, { budgetBytes = DEFAULT_BUDGET_BYTES } = {}) {
|
|
158
104
|
const attention = [...(sync.attention ?? [])]
|
|
159
105
|
.sort((left, right) => left.priority - right.priority
|
|
160
|
-
|| left.sourceId.localeCompare(right.sourceId));
|
|
106
|
+
|| (left.sourceId ?? "").localeCompare(right.sourceId ?? ""));
|
|
107
|
+
const leadsPeerBodies = item => item.priority <= 2 || item.kind === "claim_conflict";
|
|
108
|
+
const urgent = attention.filter(leadsPeerBodies);
|
|
109
|
+
const informational = attention.filter(item => !leadsPeerBodies(item));
|
|
161
110
|
const messages = sync.messages ?? [];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// context budget, crowding out messages actually addressed to the reader.
|
|
168
|
-
// Stale stays: a session that crashed holding a claim is very much news.
|
|
169
|
-
const roster = (sync.roster ?? []).filter(item => item.presence !== "offline");
|
|
170
|
-
const claims = sync.claims ?? [];
|
|
171
|
-
|
|
172
|
-
// Every entry is a group that appears whole or not at all. Single-line groups
|
|
173
|
-
// may still be truncated - there is no fence in them to leave open. `message`
|
|
174
|
-
// is carried so a dropped message can be counted apart from a dropped
|
|
175
|
-
// reminder: the two are not the same news.
|
|
176
|
-
//
|
|
177
|
-
// A peer message sits after "act now" attention (a direct request, an imminent
|
|
178
|
-
// conflict: priority <= 2) and ahead of standing reminders. The order is the
|
|
179
|
-
// fix for a real starvation: an expired-claim line (priority 6) regenerates
|
|
180
|
-
// from state every turn, while a message is delivered once and its receipt
|
|
181
|
-
// then stops it appearing - so a reminder that never clears must not keep
|
|
182
|
-
// pushing a one-time message into the over-budget overflow, turn after turn,
|
|
183
|
-
// where two agents each lost their most important message to it.
|
|
184
|
-
const urgent = attention.filter(item => item.priority <= 2);
|
|
185
|
-
const info = attention.filter(item => item.priority > 2);
|
|
186
|
-
const required = [
|
|
187
|
-
...attentionLines(urgent).map(line => ({ lines: [line], message: false })),
|
|
188
|
-
...peerBlocks(messages).map(block => ({ lines: block, message: true })),
|
|
189
|
-
...attentionLines(info).map(line => ({ lines: [line], message: false })),
|
|
190
|
-
...claimLines(claims).map(line => ({ lines: [line], message: false })),
|
|
111
|
+
const liveOfferedMessageIds = new Set(sync.liveOfferedMessageIds ?? []);
|
|
112
|
+
const groups = [
|
|
113
|
+
...attentionGroups(urgent, { truncatable: true, liveOfferedMessageIds }),
|
|
114
|
+
...messageGroups(messages),
|
|
115
|
+
...attentionGroups(informational, { truncatable: false }),
|
|
191
116
|
];
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// exactly the things worth saying to someone working alone.
|
|
197
|
-
if (sync.solo === true && required.length === 0) return "";
|
|
198
|
-
|
|
199
|
-
// Named by participant, because that is what another agent addresses work to
|
|
200
|
-
// - a session id cannot be used with `--to`. The branch says where they are,
|
|
201
|
-
// which is how a workspace spanning several worktrees stays legible.
|
|
202
|
-
const optional = roster.map(item => {
|
|
203
|
-
const who = item.participantId ?? item.sessionId;
|
|
204
|
-
const place = item.branch === null || item.branch === undefined
|
|
205
|
-
? ""
|
|
206
|
-
: ` on ${item.branch}`;
|
|
207
|
-
return `- ${who}${place} (${item.harness}, ${item.presence})`;
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const header = `${roster.length} session(s); cursor ${sync.cursor}`;
|
|
211
|
-
const lines = [header];
|
|
212
|
-
let used = bytes(header);
|
|
117
|
+
const peers = peerCount(sync);
|
|
118
|
+
if (groups.length === 0 && (sync.solo === true || peers === 0)) {
|
|
119
|
+
return { text: "", offeredMessageIds: [], includedAttentionIds: [] };
|
|
120
|
+
}
|
|
213
121
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
122
|
+
const fullHeader = groups.length === 0 ? ambientHeader(peers) : "ACC (load the acc skill):";
|
|
123
|
+
const header = truncate(fullHeader, budgetBytes);
|
|
124
|
+
const lines = header === "" ? [] : [header];
|
|
125
|
+
let used = header === "" ? 0 : bytes(header) + 1;
|
|
126
|
+
const included = [];
|
|
127
|
+
const droppedMessages = [];
|
|
217
128
|
let droppedOther = 0;
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
129
|
+
|
|
130
|
+
for (const group of groups) {
|
|
131
|
+
if (group.lines.length === 1) {
|
|
132
|
+
const remaining = budgetBytes - used;
|
|
133
|
+
const line = group.truncatable
|
|
134
|
+
? truncate(group.lines[0], remaining)
|
|
135
|
+
: (bytes(group.lines[0]) <= remaining ? group.lines[0] : "");
|
|
136
|
+
if (line !== "" && used + bytes(line) <= budgetBytes) {
|
|
137
|
+
lines.push(line);
|
|
138
|
+
included.push({ group, lineCount: 1 });
|
|
139
|
+
used += bytes(line) + 1;
|
|
140
|
+
} else {
|
|
141
|
+
droppedOther += 1;
|
|
142
|
+
}
|
|
227
143
|
continue;
|
|
228
144
|
}
|
|
229
|
-
const size =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// A dropped message is louder than a dropped reminder: "+N not shown" read as
|
|
237
|
-
// noise to two agents who each lost their most important message to it, so a
|
|
238
|
-
// message that did not fit says so specifically and names the command that
|
|
239
|
-
// recovers it. One note either way, escalated when a message is among the loss.
|
|
240
|
-
// The note is guarded against the budget, not merely reserved for: at a
|
|
241
|
-
// pathologically small budget the header alone can leave less room than the
|
|
242
|
-
// note needs, and a projection that overran the very budget it exists to
|
|
243
|
-
// respect is the bug this whole function is careful about. If the full note
|
|
244
|
-
// will not fit, the shortest imperative that does is still not silence.
|
|
245
|
-
const pushNote = note => {
|
|
246
|
-
const short = "- ⚠ over budget; `acc sync --scope full --json`";
|
|
247
|
-
for (const candidate of [note, short]) {
|
|
248
|
-
if (used + bytes(candidate) + 1 <= budgetBytes) {
|
|
249
|
-
lines.push(candidate);
|
|
250
|
-
used += bytes(candidate) + 1;
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
145
|
+
const size = groupBytes(group);
|
|
146
|
+
if (used + size <= budgetBytes) {
|
|
147
|
+
lines.push(...group.lines);
|
|
148
|
+
included.push({ group, lineCount: group.lines.length });
|
|
149
|
+
used += size;
|
|
150
|
+
} else {
|
|
151
|
+
droppedMessages.push(group.messageId);
|
|
253
152
|
}
|
|
254
|
-
};
|
|
255
|
-
if (droppedMessages > 0) {
|
|
256
|
-
// No count of the other drops: `--scope full` recovers everything.
|
|
257
|
-
pushNote(`- ⚠ ${droppedMessages} message(s) addressed to you did not fit; `
|
|
258
|
-
+ "run `acc sync --scope full --json`");
|
|
259
|
-
} else if (droppedOther > 0) {
|
|
260
|
-
pushNote(`- +${droppedOther} not shown, over budget; read them with `
|
|
261
|
-
+ "`acc sync --scope full --json`");
|
|
262
153
|
}
|
|
263
154
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
155
|
+
if (droppedMessages.length > 0) {
|
|
156
|
+
let note = recoveryNote(droppedMessages);
|
|
157
|
+
while (used + bytes(note) + 1 > budgetBytes && included.length > 0) {
|
|
158
|
+
const removed = included.pop();
|
|
159
|
+
lines.splice(-removed.lineCount, removed.lineCount);
|
|
160
|
+
used = lines.reduce((total, line) => total + bytes(line) + 1, 0);
|
|
161
|
+
if (removed.group.kind === "message") droppedMessages.push(removed.group.messageId);
|
|
162
|
+
else droppedOther += 1;
|
|
163
|
+
note = recoveryNote([...new Set(droppedMessages)]);
|
|
164
|
+
}
|
|
165
|
+
if (used + bytes(note) + 1 > budgetBytes && lines.length > 0) {
|
|
166
|
+
lines.length = 0;
|
|
167
|
+
used = 0;
|
|
168
|
+
}
|
|
169
|
+
const fitted = recoveryNote([...new Set(droppedMessages)]);
|
|
170
|
+
// An incomplete id or command is not a recovery path. When a deliberately
|
|
171
|
+
// tiny budget cannot hold the shortest truthful instruction, say nothing
|
|
172
|
+
// and leave every omitted receipt queued for a later turn.
|
|
173
|
+
if (used + bytes(fitted) + 1 <= budgetBytes) lines.push(fitted);
|
|
174
|
+
} else if (droppedOther > 0) {
|
|
175
|
+
const note = `- +${droppedOther} actionable item(s) omitted; run \`acc status --json\``;
|
|
273
176
|
if (used + bytes(note) + 1 <= budgetBytes) lines.push(note);
|
|
274
177
|
}
|
|
275
178
|
|
|
276
|
-
return
|
|
179
|
+
return {
|
|
180
|
+
text: lines.join("\n"),
|
|
181
|
+
offeredMessageIds: included
|
|
182
|
+
.filter(item => item.group.kind === "message")
|
|
183
|
+
.map(item => item.group.messageId),
|
|
184
|
+
includedAttentionIds: included
|
|
185
|
+
.filter(item => item.group.kind === "attention" && item.group.sourceId !== null)
|
|
186
|
+
.map(item => item.group.sourceId),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function projectContext(sync, options) {
|
|
191
|
+
return projectContextResult(sync, options).text;
|
|
277
192
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, chmod, mkdir, readFile, readdir, rm, writeFile }
|
|
1
|
+
import { access, chmod, lstat, mkdir, readFile, readdir, rm, writeFile }
|
|
2
2
|
from "node:fs/promises";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -147,6 +147,12 @@ export async function writeHookShim({ dir, adapterId, runner = defaultRunner(),
|
|
|
147
147
|
*/
|
|
148
148
|
export async function removeInstalledTree(target, keep = []) {
|
|
149
149
|
if (keep.includes(target)) return false;
|
|
150
|
+
try {
|
|
151
|
+
await lstat(target);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error.code === "ENOENT") return false;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
150
156
|
await rm(target, { recursive: true, force: true });
|
|
151
157
|
return true;
|
|
152
158
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Capability contract, context projection, config ownership, and the binding
|
|
2
2
|
// that survives between two ephemeral hook processes.
|
|
3
3
|
export { CAPABILITY_SHAPE, assertCapabilities, defineAdapter } from "./capabilities.mjs";
|
|
4
|
+
export { effectiveCapabilities, validateCertification } from "./certification.mjs";
|
|
4
5
|
export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
|
|
5
6
|
export { assertRunner, bakeSkillCommand, defaultCli, defaultRunner, removeInstalledTree,
|
|
6
7
|
runnerExists, writeHookShim }
|
|
7
8
|
from "./hook-shim.mjs";
|
|
8
9
|
export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
|
|
9
10
|
from "./toml-block.mjs";
|
|
10
|
-
export { projectContext } from "./context-projector.mjs";
|
|
11
|
+
export { projectContext, projectContextResult } from "./context-projector.mjs";
|
|
11
12
|
export { shellWriteTargets } from "./shell-writes.mjs";
|
|
12
13
|
export { keepOnlyVersion, ownVersion, stampPluginVersion } from "./own-version.mjs";
|
|
13
14
|
export { editJson, readJson } from "./json-text.mjs";
|
|
@@ -18,13 +18,16 @@ const fileFor = (runtimeDir, harnessSessionId) => path.join(runtimeDir, "binding
|
|
|
18
18
|
* detach the exact generation instead of opening a second session.
|
|
19
19
|
*
|
|
20
20
|
* It lives in the runtime directory, never the project, and carries identity
|
|
21
|
-
*
|
|
21
|
+
* plus the exact client facts observed at attach time - no prompt, transcript,
|
|
22
|
+
* or harness state.
|
|
22
23
|
*/
|
|
23
24
|
export async function storeSessionBinding({ runtimeDir, harnessSessionId, accSessionId,
|
|
24
|
-
generation }) {
|
|
25
|
+
generation, clientVersion, platform }) {
|
|
25
26
|
const file = fileFor(runtimeDir, harnessSessionId);
|
|
26
27
|
await mkdir(path.dirname(file), { recursive: true });
|
|
27
28
|
const record = { schemaVersion: SCHEMA_VERSION, harnessSessionId, accSessionId, generation };
|
|
29
|
+
if (typeof clientVersion === "string" && clientVersion !== "") record.clientVersion = clientVersion;
|
|
30
|
+
if (typeof platform === "string" && platform !== "") record.platform = platform;
|
|
28
31
|
const temporary = `${file}.${process.pid}.tmp`;
|
|
29
32
|
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
30
33
|
// Replace rather than append: re-attaching supersedes the old generation, and
|
|
@@ -54,7 +57,10 @@ export async function loadSessionBinding({ runtimeDir, harnessSessionId }) {
|
|
|
54
57
|
throw new AccError(EXIT.DATA, "unknown session binding schemaVersion",
|
|
55
58
|
{ file, schemaVersion: record?.schemaVersion });
|
|
56
59
|
}
|
|
57
|
-
|
|
60
|
+
const binding = { accSessionId: record.accSessionId, generation: record.generation };
|
|
61
|
+
if (typeof record.clientVersion === "string") binding.clientVersion = record.clientVersion;
|
|
62
|
+
if (typeof record.platform === "string") binding.platform = record.platform;
|
|
63
|
+
return binding;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
export async function clearSessionBinding({ runtimeDir, harnessSessionId }) {
|
|
@@ -89,7 +95,10 @@ export async function listSessionBindings({ runtimeDir }) {
|
|
|
89
95
|
.then(JSON.parse).catch(() => null);
|
|
90
96
|
if (record?.schemaVersion !== SCHEMA_VERSION) continue;
|
|
91
97
|
bindings.push({ harnessSessionId: record.harnessSessionId,
|
|
92
|
-
accSessionId: record.accSessionId, generation: record.generation
|
|
98
|
+
accSessionId: record.accSessionId, generation: record.generation,
|
|
99
|
+
...(typeof record.clientVersion === "string"
|
|
100
|
+
? { clientVersion: record.clientVersion } : {}),
|
|
101
|
+
...(typeof record.platform === "string" ? { platform: record.platform } : {}) });
|
|
93
102
|
}
|
|
94
103
|
return bindings;
|
|
95
104
|
}
|
|
@@ -14,7 +14,7 @@ export const COMMANDS = Object.freeze({
|
|
|
14
14
|
detach: { required: ["session", "generation"], optional: [] },
|
|
15
15
|
sync: { required: [], optional: ["session", "cursor", "limit", "scope"] },
|
|
16
16
|
work: { required: [], optional: ["session", "generation", "summary", "mode",
|
|
17
|
-
"state"
|
|
17
|
+
"state"], repeated: ["hint"], flags: ["clear"] },
|
|
18
18
|
claim: { required: ["resource"],
|
|
19
19
|
optional: ["session", "generation", "mode", "enforcement", "reason", "lease"] },
|
|
20
20
|
// Neither is required on its own, because either one names the claim: an id
|
|
@@ -24,34 +24,17 @@ export const COMMANDS = Object.freeze({
|
|
|
24
24
|
release: { required: [],
|
|
25
25
|
optional: ["claim", "resource", "session", "generation", "authority", "reason"] },
|
|
26
26
|
message: { required: ["subject", "body"],
|
|
27
|
-
optional: ["session", "generation", "type", "
|
|
28
|
-
repeated: ["to"]
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
optional: ["session", "generation", "type", "obligation", "client-message-id"],
|
|
28
|
+
repeated: ["to"] },
|
|
29
|
+
inbox: { required: [], optional: ["session", "generation", "message"] },
|
|
30
|
+
reply: { required: ["message", "body"],
|
|
31
|
+
optional: ["session", "generation", "subject", "client-message-id"] },
|
|
32
|
+
// Asking another agent to do something as a message with a reply obligation.
|
|
31
33
|
request: { required: ["to", "title"],
|
|
32
|
-
optional: ["session", "generation", "detail", "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
task: { required: [], optional: ["session", "generation", "workstream", "title",
|
|
37
|
-
"detail", "assignee", "state", "task", "reason"],
|
|
38
|
-
repeated: ["depends-on"], flags: ["take", "decline", "force"] },
|
|
39
|
-
// Create one, or take and hand back the coordination of one that exists.
|
|
40
|
-
// Creating a workstream raised `coordinator_missing` on every turn from then
|
|
41
|
-
// on, and nothing could answer it: the two core operations that do had no
|
|
42
|
-
// surface at all.
|
|
43
|
-
workstream: { required: [], optional: ["session", "generation", "title", "objective",
|
|
44
|
-
"workstream"], flags: ["take", "release"] },
|
|
45
|
-
// Messages not tied to a task need a way to be answered too. Without one a
|
|
46
|
-
// `requiresAck` message raised an attention item nothing could ever clear.
|
|
47
|
-
ack: { required: ["message"], optional: ["session", "generation", "state"] },
|
|
48
|
-
// Recording what was settled, so the next session does not reopen it. The
|
|
49
|
-
// protocol has described this object from the start and nothing could make
|
|
50
|
-
// one: no command, no tool.
|
|
51
|
-
decide: { required: ["title", "outcome"],
|
|
52
|
-
optional: ["session", "generation", "authority", "workstream", "supersedes"],
|
|
53
|
-
repeated: ["decided-by"], flags: ["human"] },
|
|
54
|
-
finish: { required: ["goal"], optional: ["session", "generation", "status", "to"],
|
|
34
|
+
optional: ["session", "generation", "detail", "client-message-id"] },
|
|
35
|
+
ack: { required: ["message"], optional: ["session", "generation"] },
|
|
36
|
+
finish: { required: ["goal"], optional: ["session", "generation", "status", "to",
|
|
37
|
+
"client-message-id"],
|
|
55
38
|
repeated: ["completed", "remaining", "blocker"] },
|
|
56
39
|
status: { required: [], optional: ["participant"], flags: ["all"] },
|
|
57
40
|
doctor: { required: [], optional: ["home"], flags: ["repair"] },
|
|
@@ -66,7 +49,8 @@ export const COMMANDS = Object.freeze({
|
|
|
66
49
|
// `--downgrade` because an older acc first on PATH will otherwise rewire every
|
|
67
50
|
// client to itself, and the only symptom is a guard behaving like the version
|
|
68
51
|
// it came from.
|
|
69
|
-
install: { required: [], optional: ["adapter", "home"
|
|
52
|
+
install: { required: [], optional: ["adapter", "home", "delivery"],
|
|
53
|
+
flags: ["dry-run", "downgrade"] },
|
|
70
54
|
// `--dry-run` on both, because the preview was computed for either action and
|
|
71
55
|
// only `install` could ask for it. Removal is the side that reaches into a
|
|
72
56
|
// client's configuration - including a client that has left the machine.
|
|
@@ -238,6 +238,9 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
238
238
|
// the store was healthy and nothing else.
|
|
239
239
|
const text = [`store healthy; ${describePresence(status.counts)}; `
|
|
240
240
|
+ `protection ${status.protection}; ${installed} of ${adapters.length} adapter(s) installed`,
|
|
241
|
+
...adapters.filter(adapter => (adapter.present || adapter.installed)
|
|
242
|
+
&& typeof adapter.deliveryDiagnostic === "string")
|
|
243
|
+
.map(adapter => ` ${adapter.deliveryDiagnostic}`),
|
|
241
244
|
...data.remediation.map(line => ` ${line}`),
|
|
242
245
|
// `0 of 4` is a true line that reads as a broken machine, and on an
|
|
243
246
|
// MCP-only one it would read that way on every run forever. The server needs
|
|
@@ -13,8 +13,8 @@ import { COMMANDS } from "./args.mjs";
|
|
|
13
13
|
*/
|
|
14
14
|
const GROUPS = Object.freeze([
|
|
15
15
|
["Set up", ["install", "uninstall", "doctor", "config"]],
|
|
16
|
-
["In a session", ["status", "sync", "work", "claim", "release", "
|
|
17
|
-
"
|
|
16
|
+
["In a session", ["status", "sync", "work", "claim", "release", "inbox", "reply",
|
|
17
|
+
"ack", "message", "request", "finish"]],
|
|
18
18
|
["Driven by adapters, not by people", ["attach", "heartbeat", "detach"]],
|
|
19
19
|
["About acc", ["help", "version", "update"]],
|
|
20
20
|
]);
|
|
@@ -31,10 +31,9 @@ const SUMMARY = Object.freeze({
|
|
|
31
31
|
release: "give a claim back",
|
|
32
32
|
ack: "answer a message that asked for one, so it stops asking",
|
|
33
33
|
message: "send a typed message to named participants",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
decide: "record what was settled, so the next session does not reopen it",
|
|
34
|
+
inbox: "read only unresolved messages addressed to this participant",
|
|
35
|
+
reply: "reply to one message and acknowledge it in the same operation",
|
|
36
|
+
request: "ask another agent to do something in a reply-required message",
|
|
38
37
|
finish: "write the handoff and release what this session held",
|
|
39
38
|
attach: "open a session; an adapter calls this, not a person",
|
|
40
39
|
heartbeat: "say the session is still alive",
|