Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1cd30e9
Codex: record the effective profile with each seat (#1504)
fujibee Sep 29, 2026
f2e134d
agmsgd beta: skeleton (schema, owner/lifecycle, control socket, statu…
fujibee Sep 29, 2026
db2a9a3
feat(install): operation lock and completion manifest for agmsgd beta…
fujibee Sep 29, 2026
8e7ebbe
Restore the Codex profile recording lost in the install-lock squash (…
fujibee Sep 29, 2026
78c3fd4
feat: one agmsg command (npm entry dispatches to the runtime, install…
fujibee Sep 29, 2026
c8e2ae9
fix(agmsgd): recheck install state before ownership claim (#1514)
fujibee Sep 29, 2026
cc16cae
tests: keep install cancellation children from holding Bats pipes (#1…
fujibee Sep 29, 2026
96b43b5
Add Codex queue delivery and seat status (#1520)
fujibee Sep 29, 2026
20b71f8
Add daemon beta switching and recovery guidance (#1523)
fujibee Sep 30, 2026
fc84a85
Merge main into integration/agmsgd-beta
fujibee Sep 30, 2026
0ba7bec
Merge main into integration/agmsgd-beta (#1528)
fujibee Sep 30, 2026
815eb2a
fix: handle database readers and immediate stops during daemon startu…
fujibee Sep 30, 2026
55e3cb6
Merge main into integration/agmsgd-beta
fujibee Oct 1, 2026
fb00588
Merge main into integration/agmsgd-beta (#1538)
fujibee Oct 1, 2026
38d54c1
Merge main into integration/agmsgd-beta
fujibee Oct 1, 2026
3a2c88b
Merge main into integration/agmsgd-beta (#1545)
fujibee Oct 1, 2026
2a1aa62
test: wait for lock holder acknowledgement in launcher fixture (#1547)
fujibee Oct 1, 2026
d01c329
feat: allow the measured Windows Codex queue release (#1548)
fujibee Oct 1, 2026
cd05d3b
Merge main into integration/agmsgd-beta
fujibee Oct 2, 2026
f57b32d
Merge main into integration/agmsgd-beta (#1557)
fujibee Oct 2, 2026
06735c0
Merge main into integration/agmsgd-beta (5)
fujibee Oct 3, 2026
5ffb2a9
Merge pull request #1567 from fujibee/sync/main-into-agmsgd-beta-5
fujibee Oct 3, 2026
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 .github/enforced-assertions-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
621
612
1 change: 1 addition & 0 deletions .github/scripts/bats-file-seconds.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,4 @@ test_watch_process_count.bats 17
test_watch_stuck_map.bats 4
test_watch.bats 216
test_where.bats 3
test_agmsgd_switch.bats 10
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,25 @@ Git Bash PATH). There is no PowerShell reimplementation.
Set-Alias bash 'C:\Program Files\Git\bin\bash.exe'
```

### agmsgd beta (1.6.0)

The optional agmsgd beta sends Codex sessions a notice to check their inbox through the Codex queue. It is off by default. It reads the existing SQLite message store; it does not migrate data, deliver message bodies, replace other agents' monitors, or take over remote sync. macOS and Linux are supported. Windows delivery is unsupported until it has been measured on a real Windows session. JSONL storage is unsupported by this beta; keep using the bridge for those sessions.

Requires Node >= 22.13.0 with `node:sqlite` (the experimental SQLite warning is expected). Install Node from [nodejs.org](https://nodejs.org/en/download). Enable and inspect the beta with:

```bash
agmsg daemon enable
agmsg daemon status
```

If `agmsg` is not found, use `<install>/scripts/agmsg daemon enable` (and the same path for `status`, `start`, `stop`, or `disable`); `<install>` is the installation directory printed by the installer. Follow its PATH instructions to make `agmsg` available in your shell.

While enabled, new Codex launches through the shim use plain Codex: no bridge app-server or dispatcher is started. Sessions that already have a bridge keep using it until restarted; agmsgd skips those sessions. `enable` and `start` list the remaining bridge sessions and missing destination records. Restart Codex and run `$agmsg actas <name>` to record a missing destination. Existing remote sync processes remain separate, so this beta does not yet reduce all background work to one process.

If agmsgd stops while enabled, the next ordinary agmsg script operation warns at most once every ten minutes per install. Codex startup also warns, and `daemon status` and `scripts/doctor.sh` report the failure with exit code 1. Run `agmsg daemon start` to recover. For a missing or outdated Node executable, install Node and run `agmsg daemon enable` again. Unread messages are preserved.

To return to bridge notices, run `agmsg daemon disable`, then restart the Codex sessions listed as having no bridge (for example, `codex resume`, followed by `$agmsg actas <name>`). They receive no new notices until restarted. Existing bridge sessions need no restart, and subsequent Codex launches get a bridge. Notices already accepted by the Codex queue are not cancelled and may appear once more. `stop` stops the daemon without removing its resident service; `disable` also unregisters that service.

## First run

Open your project in your agent (Claude Code, Codex, Gemini CLI, etc.) and run:
Expand Down
269 changes: 131 additions & 138 deletions bin/agmsg.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
#!/usr/bin/env node

// agmsg npm bootstrapper.
// agmsg npm entry.
//
// This package does NOT contain the agmsg implementation. It exists to
// reserve the "agmsg" name on npm and to give users a convenient
// `npx agmsg install` entry point that defers to the canonical shell
// installer maintained at https://github.com/fujibee/agmsg.
// This package does NOT contain the agmsg implementation. It is the single
// `agmsg` command's Node entry point, and it does two things only:
//
// All real installation, configuration, and runtime logic lives in the
// canonical setup.sh. This bootstrapper fetches that script to a tempfile
// and exec's it directly — equivalent to the README's
// - `agmsg install` fetches the canonical setup.sh for this package's
// version and runs it (equivalent to the README's
// `bash <(curl -fsSL .../setup.sh)`; process substitution keeps the tty as
// stdin, see agmsg #98).
// - every other command is handed, unchanged, to the bash runtime that an
// install put on disk (scripts/agmsg). Nothing is reimplemented here.
//
// bash <(curl -fsSL https://raw.githubusercontent.com/fujibee/agmsg/main/setup.sh)
//
// form, which is process-substitution and preserves the user's tty as
// stdin. We deliberately do NOT pipe the curl output into bash: piping
// makes the installer's stdin the wrapper script stream, and install.sh's
// interactive command-name prompt would `read -r` the next line of
// setup.sh as the command name. See agmsg #98.
//
// Subcommands:
// install Fetch and run the canonical setup.sh (default if no args).
// --help Print this message and exit 0.
// --version Print the bootstrapper version and exit 0.
// Usage:
// install [options] Fetch and run the canonical setup.sh.
// help, --help Print usage.
// --version Print this package's and the installed runtime's version.
// <anything else> Run the installed runtime's command with the same args.

const { spawnSync } = require('child_process');
const fs = require('fs');
Expand Down Expand Up @@ -56,46 +50,11 @@ function readVersion() {
}
}

// `agmsg <verb>` is the single most common wrong guess about this project,
// and it is a guess the documentation taught: a sweep of docs/design and
// docs/spec found 34 backticked commands written as `agmsg send …`,
// `agmsg key show …`, `agmsg team list …`. There is no such CLI. This package
// installs agmsg; the commands are scripts inside the install.
//
// Saying only "unknown argument" leaves the person exactly where they were.
// It has to name what to type instead.
//
// The verb→script map is a HINT, and the two halves of that are tested
// differently (review P1):
//
// NOT promised: that it is complete. This package does not ship scripts/,
// so it cannot enumerate them. A verb missing from here falls through to
// the general form, which stays correct.
// PROMISED: that every entry present is real. A renamed or deleted script
// would otherwise make this print a specific path that does not exist —
// WORSE than the general form, not merely less precise. The first version
// of this comment claimed only precision could degrade; that was wrong,
// and it was wrong for exactly the entries most likely to rot.
//
// test_bin_agmsg.bats pins every value against the repo's scripts/ — the
// list is asserted non-empty, not at a fixed size, so adding a verb here
// costs nothing while renaming a script fails the suite.
//
// That pin is about THIS repo. It cannot speak for the tree on a user's
// disk, so printNotACommand checks the actual file there before naming it.
const SCRIPT_FOR_VERB = {
send: 'send.sh', history: 'history.sh', inbox: 'inbox.sh', join: 'join.sh',
team: 'team.sh', key: 'key.sh', remote: 'remote.sh', whoami: 'whoami.sh',
leave: 'leave.sh', rename: 'rename.sh', export: 'export.sh', config: 'config.sh',
watch: 'watch.sh', spawn: 'spawn.sh', version: 'version.sh', api: 'api.sh',
};

// The default install location. Checked because this package's whole job is
// to install agmsg, so a person who has never run it reaches this branch too
// (review P1) — and for them every path below is a command that fails.
// Advice that assumes the install is advice they cannot follow.
function defaultSkillDir() {
return path.join(os.homedir(), '.agents', 'skills', 'agmsg');
// The install location. `AGMSG_CMD=<name>` selects an install made with
// `--cmd <name>`; without it the default install is used. A named install that
// is missing is an error -- it never falls back to the default one.
function skillsRoot() {
return path.join(os.homedir(), '.agents', 'skills');
}

function exists(p) {
Expand All @@ -106,82 +65,65 @@ function exists(p) {
}
}

function printNotACommand(verb, skillDirForTest) {
const dir = skillDirForTest || defaultSkillDir();
const skill = '~/.agents/skills/agmsg/scripts';
const script = Object.prototype.hasOwnProperty.call(SCRIPT_FOR_VERB, verb)
? SCRIPT_FOR_VERB[verb]
: null;
const scriptsDir = path.join(dir, 'scripts');

// The contract differs by what is about to be printed, and that is the
// point (review P1):
//
// naming ONE script -> that script file must exist. The repo-side pin
// proves the map matches THIS repo; it says nothing about the tree on
// the user's disk, which can be an old version, a partial update, or a
// broken install. Checking only that scripts/ exists reproduces the
// previous P1 one layer out — a directory is not the file.
// naming the DIRECTORY -> the directory must exist. Nothing more is
// claimed, so nothing more is checked.
const haveScripts = exists(scriptsDir);
const usable = script ? exists(path.join(scriptsDir, script)) : haveScripts;

const lines = ['agmsg: `agmsg ' + verb + '` is not a command.', ''];

if (!usable) {
// Three situations that need different next steps, kept apart: never
// installed, installed but without this command, and installed under
// another name. Folding them together leaves someone without a recovery
// step — which is what the first version of this message did.
if (haveScripts) {
lines.push('Your agmsg install does not contain that command. It may be an');
lines.push('older version — update it:');
} else {
lines.push('agmsg does not look installed on this machine — this package is');
lines.push('the installer for it. Install first:');
// Sets out how to reach the runtime `agmsg` (scripts/agmsg, a bash script that
// install.sh copies into the install). Returns { runtime } on success or
// { error, lines } describing why it cannot be reached.
function resolveRuntime(env, skillsDirForTest) {
const root = skillsDirForTest || skillsRoot();
const named = env.AGMSG_CMD;
if (named !== undefined && named !== '') {
if (!/^[A-Za-z0-9._-]+$/.test(named) || named === '.' || named === '..') {
return { error: true, lines: ['agmsg: AGMSG_CMD must be a plain install name, got "' + named + '".'] };
}
lines.push('');
lines.push(' npx agmsg install');
lines.push('');
lines.push('After that, ' + (script ? 'that command is:' : 'the commands are:'));
lines.push('');
lines.push(script ? ' bash ' + skill + '/' + script + ' …'
: ' bash ' + skill + '/<name>.sh …');
lines.push('');
lines.push('(Already installed under a different name? Substitute it for');
lines.push('`agmsg` in that path — nothing is put on your PATH.)');
} else if (script) {
lines.push('This package only installs agmsg. That one lives in your install:');
lines.push('');
lines.push(' bash ' + skill + '/' + script + ' …');
} else {
lines.push('This package only installs agmsg. The commands live in your install:');
lines.push('');
lines.push(' ls ' + skill + '/');
lines.push(' bash ' + skill + '/<name>.sh …');
return checkInstall(path.join(root, named), 'AGMSG_CMD=' + named);
}
const def = path.join(root, 'agmsg');
const found = checkInstall(def, 'the default install');
if (!found.error) return found;
if (exists(path.join(def, 'scripts'))) return found;
// No default install: list others that have a runtime, without picking one.
let others = [];
try {
others = fs.readdirSync(root).filter((n) => exists(path.join(root, n, 'scripts', 'agmsg')));
} catch (_) { /* no skills directory */ }
if (others.length > 0) {
return {
error: true,
lines: ['agmsg: there is no default install, but these installs have the agmsg command:']
.concat(others.map((n) => ' ' + n))
.concat(['Pick one with AGMSG_CMD=<name> agmsg …'])
};
}
return found;
}

lines.push('');
// The skill command is the path most people actually want — it is what the
// install sets up, and it needs no paths.
lines.push('Or ask your agent: run the agmsg skill command (/agmsg in Claude Code).');
lines.push('`npx agmsg --help` covers what THIS package does.');
console.error(lines.join('\n'));
function checkInstall(dir, label) {
const runtime = path.join(dir, 'scripts', 'agmsg');
if (exists(runtime)) return { runtime, dir };
if (exists(path.join(dir, 'scripts'))) {
return { error: true, lines: [
'agmsg: ' + label + ' has no agmsg command (an older version). Update it:',
' agmsg install'
] };
}
return { error: true, lines: [
'agmsg: ' + label + ' is not installed. Install first:',
' agmsg install'
] };
}

function printHelp() {
process.stdout.write([
'agmsg — npm bootstrapper for cross-agent messaging',
'',
'This package is a thin wrapper. The real installer lives at:',
' ' + REPO_URL,
'agmsg — cross-agent messaging',
'',
'Usage:',
' npx agmsg run the canonical setup.sh (same as `agmsg install`)',
' npx agmsg install run the canonical setup.sh',
' npx agmsg --help show this message',
' npx agmsg --version show this bootstrapper\'s version',
' agmsg install [options] install or update agmsg (runs the canonical setup.sh)',
' agmsg daemon <command> manage the agmsgd beta daemon (start|stop|status|enable|disable)',
' agmsg --version show this package and the installed runtime version',
' agmsg help show this message',
'',
'Every command other than install is run by the agmsg install on this',
'machine (AGMSG_CMD=<name> picks an install made with `--cmd <name>`).',
'',
'After install, restart your agent (Claude Code / Codex / Gemini CLI /',
'Copilot CLI / Antigravity / OpenCode) and run the agmsg skill command',
Expand All @@ -193,6 +135,42 @@ function printHelp() {
].join('\n'));
}

// Runs the runtime with the arguments exactly as given: an absolute path, no
// shell, inherited stdio, and the runtime's own exit status.
function runRuntime(runtime, args) {
const bash = bashCommand();
if (!bash) {
console.error('agmsg: Git for Windows bash was not found. Install Git for Windows, then run this again.');
process.exit(1);
}
const result = spawnSync(bash, [toBashPath(runtime), ...args], { stdio: 'inherit' });
if (result.error) {
console.error('agmsg: failed to launch bash:', result.error.message);
process.exit(1);
}
if (result.signal) {
process.kill(process.pid, result.signal);
return;
}
process.exit(result.status === null ? 1 : result.status);
}

// On Windows a bare `bash` can be the WSL launcher, so only Git for Windows'
// own bash.exe is used there; when it cannot be found this returns null
// instead of falling back to a bare `bash`.
function bashCommand() {
if (process.platform === 'win32') {
const roots = [process.env.ProgramFiles, process.env.ProgramW6432, process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs')]
.filter(Boolean);
for (const root of roots) {
const gitBash = path.join(root, 'Git', 'bin', 'bash.exe');
if (exists(gitBash)) return gitBash;
}
return null;
}
return 'bash';
}

// Normalise a native path to the forward-slash form that bash.exe and
// curl.exe accept on Windows. bash is an MSYS2 program: Windows gives it a
// raw command-line string rather than a real argv[], and MSYS's own argv
Expand All @@ -217,6 +195,11 @@ function runInstaller(passthroughArgs) {
// correctly here is defense-in-depth and lets future interactive prompts
// in setup.sh keep working for real-tty users.
const ref = installRef();
const bash = bashCommand();
if (!bash) {
console.error('agmsg: Git for Windows bash was not found. Install Git for Windows, then run this again.');
process.exit(1);
}
const setupUrl = RAW_BASE + '/' + ref + '/setup.sh';
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agmsg-bootstrap-'));
// os.tmpdir()/path.join() return backslash-separated paths on Windows.
Expand All @@ -236,7 +219,7 @@ function runInstaller(passthroughArgs) {
}

// Pin the clone inside setup.sh to the same ref we fetched it from.
const result = spawnSync('bash', [setupPath, ...passthroughArgs], {
const result = spawnSync(bash, [setupPath, ...passthroughArgs], {
stdio: 'inherit',
env: Object.assign({}, process.env, { AGMSG_REF: ref })
});
Expand All @@ -253,26 +236,36 @@ function runInstaller(passthroughArgs) {
function main() {
const args = process.argv.slice(2);

if (args.length === 0 || args[0] === 'install') {
if (args[0] === 'install') {
// Forward anything after `install` (e.g. `agmsg install --cmd m`) to
// setup.sh, which passes "$@" through to install.sh.
const passthrough = args[0] === 'install' ? args.slice(1) : args;
runInstaller(passthrough);
} else if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
runInstaller(args.slice(1));
} else if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
printHelp();
process.exit(0);
process.exit(args.length === 0 ? 2 : 0);
} else if (args[0] === '--version' || args[0] === '-v') {
process.stdout.write('agmsg bootstrapper ' + readVersion() + '\n');
process.stdout.write('canonical project: ' + REPO_URL + '\n');
process.stdout.write('agmsg ' + readVersion() + ' (npm entry)\n');
const found = resolveRuntime(process.env);
if (found.error) {
process.stdout.write('runtime: not available\n');
} else {
const bash = bashCommand();
const v = bash ? spawnSync(bash, [toBashPath(found.runtime), '--version'], { encoding: 'utf8' }) : null;
process.stdout.write('runtime: ' + (v && v.status === 0 ? v.stdout.trim() : 'unreadable') + '\n');
}
process.exit(0);
} else {
printNotACommand(args[0]);
process.exit(2);
const found = resolveRuntime(process.env);
if (found.error) {
console.error(found.lines.join('\n'));
process.exit(2);
}
runRuntime(found.runtime, args);
}
}

if (require.main === module) {
main();
}

module.exports = { toBashPath, SCRIPT_FOR_VERB, printNotACommand };
module.exports = { toBashPath, resolveRuntime, runRuntime };
2 changes: 2 additions & 0 deletions docs/design/agmsgd-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This is the architecture design for agmsgd: the approved direction in [the RFC](agmsgd-rfc.md), worked down to how each part behaves. It describes the current design only; the reasoning behind each choice lives in the discussion and in the ADRs that will follow. Implementation has not started. The technical design (where the source lives, how it is built and shipped, the main components) comes next.

**1.6.0 beta scope:** the optional, default-off daemon queues inbox notices for Codex sessions against the existing SQLite store, before the 2.0.0 migration. Existing bridges and remote sync processes remain separate; a live bridge continues until its Codex session restarts. `agmsg daemon enable` changes new Codex launches, while `agmsg daemon disable` lists sessions that need restarting to restore bridge notices. If `agmsg` is not found, use `<install>/scripts/agmsg daemon enable|disable`. macOS and Linux are supported. Windows delivery remains unsupported pending measurement, and JSONL storage is unsupported. The beta does not implement the 2.0.0 storage or delivery changes described below.

Status: 2026-09-28. [日本語版](agmsgd-architecture.ja.md)

## 1. Overview
Expand Down
Loading
Loading