Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "agentcomm",
"source": "./",
"description": "Multi-agent mailbox CLI — any git remote is a bus (git+ssh/github/sqlite/s3/gcs/postgres). register/send/inbox/wait/claim/log/channels so Claude coordinates with other agents or sessions; auto-selects the repo bus inside a git repo.",
"version": "0.14.0",
"version": "0.14.1",
"author": {
"name": "Yoni Davidson"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentcomm",
"description": "Multi-agent mailbox CLI — any git remote is a bus (git+ssh/github/sqlite/s3/gcs/postgres). register/send/inbox/wait/claim/log/channels so Claude coordinates with other agents or sessions; auto-selects the repo bus inside a git repo.",
"version": "0.14.0",
"version": "0.14.1",
"author": {
"name": "Yoni Davidson",
"email": "yonidavidson@tabnine.com"
Expand Down
6 changes: 6 additions & 0 deletions dist/backends/git.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,10 @@ export declare class GitBackend implements Backend, Claimable {
*/
claim(queue: string, _owner: string): Promise<Message | null>;
}
/**
* The commit message IS the human feed: on git buses the branch page should
* read like a timeline, not like storage. Bus content is already in-repo,
* so echoing names/subjects/statuses adds visibility, not exposure.
*/
export declare function describePut(key: string, data: Buffer): string;
//# sourceMappingURL=git.d.ts.map
2 changes: 1 addition & 1 deletion dist/backends/git.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion dist/backends/git.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/backends/git.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/backends/github.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion dist/backends/github.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/backends/github.js.map

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions hooks/prompt-digest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ await fs.writeFile(rosterFile, JSON.stringify(names)).catch(() => {});
const joined = known.length ? names.filter((n) => !known.includes(n)) : [];
const activeAgents = roster.filter((a) => Date.now() - Date.parse(a.lastSeen) < 10 * 60_000);
const alias0 = aliasFrom(peek?.stderr);
// status adoption: an agent with no declared status is invisible to
// coordination — nudge it (gently: at most once per 30min per repo)
let statusNudge = null;
const myRec = roster.find((a) => a.name === alias0);
if (myRec && !myRec.status) {
const nudgeStamp = path.join(os.tmpdir(), `agentcomm-nudge-${id}`);
let due = true;
try {
due = Date.now() - (await fs.stat(nudgeStamp)).mtimeMs > 30 * 60_000;
} catch { /* first */ }
if (due) {
statusNudge =
'You carry no bus status — declare what you are working on now: `agentcomm register --status "<short task>"` (or "blocked: <need>" to recruit help).';
await fs.writeFile(nudgeStamp, '').catch(() => {});
}
}
const { lines: activity } = await activitySince(
cwd,
alias0,
Expand Down Expand Up @@ -76,7 +92,7 @@ for (const a of asks.slice(0, 3)) {
'Otherwise continue your own task.',
);
}
if (!pending && joined.length === 0 && ctas.length === 0 && activity.length === 0)
if (!pending && joined.length === 0 && ctas.length === 0 && activity.length === 0 && !statusNudge)
process.exit(0); // no news, no noise

process.stdout.write(
Expand All @@ -86,7 +102,8 @@ process.stdout.write(
additionalContext:
`agentcomm digest: ${bits.join(' · ')}.` +
(activity.length ? `\nbus activity since last digest:\n ${activity.join('\n ')}` : '') +
(ctas.length ? `\n${ctas.join('\n')}` : ''),
(ctas.length ? `\n${ctas.join('\n')}` : '') +
(statusNudge ? `\n${statusNudge}` : ''),
},
}),
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentcomm",
"version": "0.14.0",
"version": "0.14.1",
"description": "A tiny mailbox/message bus for AI agents that shell out to one CLI. Pluggable storage backends behind a single Backend interface.",
"type": "module",
"license": "MIT",
Expand Down
23 changes: 22 additions & 1 deletion src/backends/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export class GitBackend implements Backend, Claimable {
const blob = await this.writeBlob(data);
for (let attempt = 1; attempt <= 6; attempt++) {
const tip = await this.tip();
if (await this.commitAndPush(tip, `agentcomm: put ${this.k(key)}`, { add: [{ key, blob }] })) return;
if (await this.commitAndPush(tip, describePut(this.k(key), data), { add: [{ key, blob }] })) return;
await sleep(30 * attempt + Math.floor(Math.random() * 80));
}
throw new Error(`agentcomm: git put ${key} kept losing push races — extremely contended bus?`);
Expand Down Expand Up @@ -306,3 +306,24 @@ function notFound(key: string): NodeJS.ErrnoException {
function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}

/**
* The commit message IS the human feed: on git buses the branch page should
* read like a timeline, not like storage. Bus content is already in-repo,
* so echoing names/subjects/statuses adds visibility, not exposure.
*/
export function describePut(key: string, data: Buffer): string {
try {
const o = JSON.parse(data.toString('utf8')) as Record<string, unknown>;
if (key.startsWith('agents/')) {
const name = String(o.name ?? key.slice(7).replace(/\.json$/, ''));
return o.status
? `agentcomm: ${name} — ${String(o.status)}`
: `agentcomm: ${name} is on the bus`;
}
if (key.startsWith('inbox/') && o.from && o.to) {
return `agentcomm: ${String(o.from)} → ${String(o.to)}${o.subject ? ` [${String(o.subject)}]` : ''}`;
}
} catch { /* not JSON — fall through */ }
return `agentcomm: put ${key}`;
}
3 changes: 2 additions & 1 deletion src/backends/github.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describePut } from './git.js';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { type Backend } from '../types.js';
Expand Down Expand Up @@ -159,7 +160,7 @@ export class GithubBackend implements Backend {
let sha = await this.shaOf(key);
for (let attempt = 1; ; attempt++) {
const res = await this.api('PUT', this.contentsUrl(key), {
message: `agentcomm: put ${this.k(key)}`,
message: describePut(this.k(key), data),
content: data.toString('base64'),
branch: this.branch,
...(sha ? { sha } : {}),
Expand Down
23 changes: 23 additions & 0 deletions test/git.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,27 @@ describe('GitBackend (local bare remotes — same code path as any host)', () =>
expect(bad.code).toBe(1);
expect(bad.stderr).toMatch(/unsupported query parameter/);
}, 60000);

describe('commit messages are the feed', () => {
it('statuses and sends read as a timeline in the bus branch history', async () => {
const { uri, cache } = await bareRemote();
const backend = await open(uri, cache);
const bare = uri.replace('git+file://', '');
try {
await backend.put(
'agents/worker-1.json',
Buffer.from(JSON.stringify({ name: 'worker-1', registeredAt: 'x', lastSeen: 'x', status: 'reviewing PR 12' })),
);
await backend.put(
'inbox/planner/00001-abc.json',
Buffer.from(JSON.stringify({ id: 'abc', from: 'worker-1', to: 'planner', subject: 'done', body: 'shipped', ts: 'x' })),
);
const log = execFileSync('git', ['-C', bare, 'log', '--format=%s', 'agentcomm'], { encoding: 'utf8' });
expect(log).toContain('agentcomm: worker-1 — reviewing PR 12');
expect(log).toContain('agentcomm: worker-1 → planner [done]');
} finally {
await backend.close?.();
}
});
});
});
21 changes: 20 additions & 1 deletion test/hooks.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ describe('plugin hooks: bus discipline made mechanical', () => {

it('prompt digest: news-only, throttled, silent when quiet', async () => {
const dir = await markedRepo();
cliSync(['register'], dir);
cliSync(['register', '--status', 'quiet work'], dir); // status declared → no nudge; quiet means quiet
cliSync(['register'], dir, 'sender');

// quiet bus (roster snapshot primes on first run) → silence
Expand Down Expand Up @@ -410,6 +410,25 @@ describe('plugin hooks: bus discipline made mechanical', () => {
expect(ctx).toContain('planner → worker-1 [done]: "auth module is done, tests green"');
});

it('digest nudges an agent that carries no status (once per 30min)', async () => {
const dir = await markedRepo();
cliSync(['register'], dir); // no --status

const first = await runHook('prompt-digest.mjs', { cwd: dir }, dir);
const out = JSON.parse(first.stdout) as { hookSpecificOutput: { additionalContext: string } };
expect(out.hookSpecificOutput.additionalContext).toContain('You carry no bus status');

// declared → clear throttles → no nudge, and (quiet bus) full silence
cliSync(['register', '--status', 'building the feed'], dir);
for (const f of await fs.readdir(dir)) {
if (f.startsWith('agentcomm-digest-') && !f.includes('roster') && !f.includes('acts'))
await fs.rm(path.join(dir, f));
if (f.startsWith('agentcomm-nudge-')) await fs.rm(path.join(dir, f));
}
const second = await runHook('prompt-digest.mjs', { cwd: dir }, dir);
expect(second.stdout).toBe('');
});

it('stop guard honors stop_hook_active (no loops) and throttles repeat checks', async () => {
const dir = await markedRepo();
cliSync(['register'], dir);
Expand Down
Loading