agentmash 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/agentmash.mjs +341 -8
- package/crew/README.md +52 -0
- package/crew/bundle.json +223 -0
- package/crew/crew.lock.json +14 -0
- package/crew/install.mjs +210 -0
- package/crew/team-plan.mjs +725 -0
- package/hooks/mcp_launcher.mjs +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -39,9 +39,15 @@ Node 18 or newer. The CLI has no dependencies and needs no checkout of this proj
|
|
|
39
39
|
| `npx agentmash doctor` | Check exactly what is and isn't working |
|
|
40
40
|
| `npx agentmash status` | Who is working on what, from the terminal |
|
|
41
41
|
| `npx agentmash uninstall` | Remove the hooks from this repo |
|
|
42
|
+
| `npx agentmash team add <name> --agents <a,b\|all>` | Put agents from the crew in this repo as a named team |
|
|
43
|
+
| `npx agentmash team remove <name> [--agents <a,b>]` | Remove a team, or only the agents given |
|
|
44
|
+
| `npx agentmash team list [--json]` | The teams installed in this repo |
|
|
45
|
+
| `npx agentmash team update` | Rewrite the team files from the crew version this CLI carries |
|
|
42
46
|
|
|
43
47
|
Flags: `--server <url>` (coordination server), `--repo <path>`, `--label <text>`,
|
|
44
|
-
and `--force` with `init` to replace an existing room.
|
|
48
|
+
and `--force` with `init` to replace an existing room. With `team`: `--agents`, `--slug <slug>`,
|
|
49
|
+
`--dry-run` to see what would change without writing, and `--force` to overwrite files that were
|
|
50
|
+
changed locally or not written by the crew.
|
|
45
51
|
|
|
46
52
|
Reach for `doctor` when someone's events aren't showing up. The hooks fail silently by
|
|
47
53
|
design, so it is the only thing that will tell you why: it resolves the config and shows
|
|
@@ -49,6 +55,28 @@ where each value came from, checks the hooks are registered, measures the round
|
|
|
49
55
|
compares it against what a cold hook needs, validates the room, and lists the teammates it
|
|
50
56
|
can see.
|
|
51
57
|
|
|
58
|
+
## Crew teams
|
|
59
|
+
|
|
60
|
+
The [agentmash crew](https://github.com/IskakovDamir/agentmash-crew) is 18 Claude Code agents
|
|
61
|
+
with Mash, the conductor, in every team. `team add` puts the ones you pick into this repo under a
|
|
62
|
+
name:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx agentmash team add "agentmash crew" --agents pipe,flex,probe
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That writes the agents to `.claude/agents/`, the crew's skills and one skill for the team to
|
|
69
|
+
`.claude/skills/`, a guard hook to `.claude/agentmash-crew/` (registered in `.claude/settings.json`
|
|
70
|
+
beside anything already there), and a record of it all to `.claude/agentmash-crew/teams.json`. It
|
|
71
|
+
prints the `git add` for exactly those files. Commit them, start a new Claude Code session at the
|
|
72
|
+
repo root, and say `agentmash crew, <task>`.
|
|
73
|
+
|
|
74
|
+
Teams have nothing to do with the room. `team` never calls the coordination server, needs no
|
|
75
|
+
`init`, and `uninstall` leaves teams where they are. A file the crew did not write is never
|
|
76
|
+
overwritten without `--force`, and a refusal writes nothing at all. The files are the same bytes
|
|
77
|
+
the crew's own installer and the one-click pull request on agentmash.dev write: all three run the
|
|
78
|
+
crew's planner, which this package carries at the version pinned in `crew/crew.lock.json`.
|
|
79
|
+
|
|
52
80
|
## Running your own server
|
|
53
81
|
|
|
54
82
|
`init` points at a shared server by default. To coordinate on your own infrastructure,
|
package/agentmash.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// npx agentmash status who is working on what, right now
|
|
10
10
|
// npx agentmash git-hook install or remove the post-commit client
|
|
11
11
|
// npx agentmash uninstall remove the hooks from this repo
|
|
12
|
+
// npx agentmash team install agents from the agentmash crew as a named team
|
|
12
13
|
// npx agentmash mcp serve the MCP tools on stdio (what .mcp.json runs)
|
|
13
14
|
|
|
14
15
|
import fs from 'node:fs';
|
|
@@ -1161,6 +1162,8 @@ async function cmdDoctor(args) {
|
|
|
1161
1162
|
info(`install it with ${c.bold('agentmash git-hook install')} (local to this checkout)`);
|
|
1162
1163
|
}
|
|
1163
1164
|
|
|
1165
|
+
await doctorCrew(repo);
|
|
1166
|
+
|
|
1164
1167
|
const me = identity(repo);
|
|
1165
1168
|
ok(`you are ${c.cyan(me.who)} ${c.dim(`(${me.from})`)}`);
|
|
1166
1169
|
|
|
@@ -1340,6 +1343,11 @@ function cmdUninstall(args) {
|
|
|
1340
1343
|
console.log(`\n${c.bold('AgentMash removed')} from ${repo}\n`);
|
|
1341
1344
|
if (git.ok) info(git.message);
|
|
1342
1345
|
else warn(git.message);
|
|
1346
|
+
// Crew teams need no room and are not part of what init installed, so they
|
|
1347
|
+
// stay. Said out loud, or "removed" reads as "everything agentmash wrote".
|
|
1348
|
+
if (pathKind(path.join(repo, CREW_MANIFEST)) !== 'missing') {
|
|
1349
|
+
info(`crew teams stay installed — remove them with ${c.bold('npx agentmash team remove <name>')}`);
|
|
1350
|
+
}
|
|
1343
1351
|
info('your teammates still have it until they pull this change\n');
|
|
1344
1352
|
}
|
|
1345
1353
|
|
|
@@ -1677,6 +1685,295 @@ async function cmdDelete(args) {
|
|
|
1677
1685
|
info('the hooks are still installed and now do nothing — run: agentmash init, or uninstall\n');
|
|
1678
1686
|
}
|
|
1679
1687
|
|
|
1688
|
+
// ── crew teams ─────────────────────────────────────────────────────────────
|
|
1689
|
+
// `agentmash team` puts agents from the agentmash crew into the repo as a named
|
|
1690
|
+
// team: agent files in .claude/agents, the shared skills, one skill per team,
|
|
1691
|
+
// a guard hook, and a record of all of it in .claude/agentmash-crew/teams.json.
|
|
1692
|
+
// It is not part of the room. It never calls the coordination server, needs no
|
|
1693
|
+
// room, and works while the server is closed or unreachable.
|
|
1694
|
+
//
|
|
1695
|
+
// None of the planning happens here. The crew's own installer is vendored at
|
|
1696
|
+
// crew/ by tools/sync-crew.mjs and imported as it is, so a team installed with
|
|
1697
|
+
// this command is byte for byte the team the crew's installer and the
|
|
1698
|
+
// agentmash.dev pull request write: all three run the same planChange.
|
|
1699
|
+
|
|
1700
|
+
const CREW_FILES = ['bundle.json', 'install.mjs', 'team-plan.mjs', 'crew.lock.json'];
|
|
1701
|
+
|
|
1702
|
+
// The bundle's meta.manifestPath. Kept as a constant so uninstall can look for
|
|
1703
|
+
// it without loading a 300 KB bundle; the uninstall test installs a team the
|
|
1704
|
+
// real way, so a bundle that moved the manifest fails that test.
|
|
1705
|
+
const CREW_MANIFEST = path.join('.claude', 'agentmash-crew', 'teams.json');
|
|
1706
|
+
|
|
1707
|
+
/**
|
|
1708
|
+
* The vendored crew: the repo checkout's crew/ in dev, or the copy
|
|
1709
|
+
* sync-hooks.mjs packs into the published package. Checkout first, for the
|
|
1710
|
+
* reason hookSourceDir gives: cli/crew/ is a copy that goes stale the moment
|
|
1711
|
+
* someone syncs a new crew release.
|
|
1712
|
+
*/
|
|
1713
|
+
function crewSourceDir() {
|
|
1714
|
+
for (const candidate of [path.join(HERE, '..', 'crew'), path.join(HERE, 'crew')]) {
|
|
1715
|
+
if (CREW_FILES.every((f) => fs.existsSync(path.join(candidate, f)))) return candidate;
|
|
1716
|
+
}
|
|
1717
|
+
return null;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* The crew's installer module and its verified bundle. Throws instead of
|
|
1722
|
+
* dying, because doctor wants to say this as a warning. loadBundle checks the
|
|
1723
|
+
* sha256 of every file the bundle installs before anything is planned.
|
|
1724
|
+
*/
|
|
1725
|
+
async function loadCrew() {
|
|
1726
|
+
const dir = crewSourceDir();
|
|
1727
|
+
if (!dir) throw new Error('could not locate the crew files to install (broken package?)');
|
|
1728
|
+
const install = await import(pathToFileURL(path.join(dir, 'install.mjs')).href);
|
|
1729
|
+
return { install, bundle: install.loadBundle(path.join(dir, 'bundle.json')) };
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// What each action takes. The rest of the CLI ignores a flag it does not know;
|
|
1733
|
+
// `team` refuses one, because an ignored `--dryrun` is a dry run that writes.
|
|
1734
|
+
const TEAM_ACTIONS = {
|
|
1735
|
+
add: { name: true, flags: ['agents', 'slug', 'repo', 'dry-run', 'force'] },
|
|
1736
|
+
remove: { name: true, flags: ['agents', 'repo', 'dry-run', 'force'] },
|
|
1737
|
+
list: { name: false, flags: ['repo', 'json'] },
|
|
1738
|
+
update: { name: false, flags: ['repo', 'dry-run', 'force'] },
|
|
1739
|
+
};
|
|
1740
|
+
const TEAM_VALUE_FLAGS = ['agents', 'slug', 'repo'];
|
|
1741
|
+
// parseArgs stores booleans under camelCase keys; turn them back into what was typed.
|
|
1742
|
+
const FLAG_SPELLING = { dryRun: 'dry-run', noGitHook: 'no-git-hook', noOpen: 'no-open', forceHooksPath: 'force-hooks-path' };
|
|
1743
|
+
|
|
1744
|
+
const TEAM_ROW = { created: ok, updated: ok, deleted: ok, overwritten: warn, kept: warn, left: warn };
|
|
1745
|
+
|
|
1746
|
+
/** One line per file the plan touched (or would touch); unchanged files are only counted. */
|
|
1747
|
+
function printTeamPlan(plan, dryRun) {
|
|
1748
|
+
const rows = plan.changes.filter((change) => change.action !== 'unchanged');
|
|
1749
|
+
if (plan.settings.action !== 'unchanged') rows.push({ path: plan.settings.path, action: plan.settings.action });
|
|
1750
|
+
if (plan.manifestAction !== 'unchanged') rows.push({ path: plan.manifestPath, action: plan.manifestAction });
|
|
1751
|
+
const width = Math.max(0, ...rows.map((row) => row.action.length));
|
|
1752
|
+
// A dry run's rows are what would happen, so none of them gets a ✓.
|
|
1753
|
+
for (const row of rows) {
|
|
1754
|
+
const mark = dryRun ? info : (TEAM_ROW[row.action] ?? info);
|
|
1755
|
+
mark(`${row.action.padEnd(width)} ${c.dim(row.path)}`);
|
|
1756
|
+
}
|
|
1757
|
+
const same = plan.changes.filter((change) => change.action === 'unchanged').length;
|
|
1758
|
+
if (same > 0) info(`${same} file${same === 1 ? '' : 's'} unchanged`);
|
|
1759
|
+
for (const message of plan.warnings) warn(message);
|
|
1760
|
+
if (dryRun) info('dry run: nothing was written');
|
|
1761
|
+
else if (!plan.changed) info('nothing to change');
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function printTeamSteps(steps) {
|
|
1765
|
+
console.log(`\n ${c.bold('next:')}`);
|
|
1766
|
+
steps.forEach((step, i) => console.log(` ${i + 1}. ${step}`));
|
|
1767
|
+
console.log('');
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Claude Code reads the project's .claude/settings.json, and with it the guard
|
|
1771
|
+
// hook, only from the directory it starts in, while agents and skills also load
|
|
1772
|
+
// from a subfolder. So the restart step names the repository root.
|
|
1773
|
+
const TEAM_RESTART =
|
|
1774
|
+
'start a new Claude Code session at the repository root, so it loads the agents, skills and guard hook (the hook does not load when Claude Code starts in a subfolder)';
|
|
1775
|
+
|
|
1776
|
+
/** Quotes a path for a POSIX shell, and for cmd and PowerShell too when the path allows plain double quotes. */
|
|
1777
|
+
function shellQuote(value) {
|
|
1778
|
+
if (/^[\w@%+=:,./-]+$/.test(value)) return value;
|
|
1779
|
+
if (!/["$`!\\]/.test(value)) return `"${value}"`;
|
|
1780
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* The commit step, as the crew's own installer prints it. It stages exactly
|
|
1785
|
+
* the files the installer manages after this run (also those an earlier,
|
|
1786
|
+
* uncommitted run wrote) and the ones it deleted, so other work under .claude/
|
|
1787
|
+
* — including AgentMash's own hooks — and the files the plan kept or left
|
|
1788
|
+
* because someone changed them stay out of the commit. Deletions go through
|
|
1789
|
+
* `git rm --cached --ignore-unmatch`, because `git add` fails on a path that is
|
|
1790
|
+
* neither on disk nor tracked (a team removed before it was ever committed).
|
|
1791
|
+
* `git -C <root>` makes the command work from any folder.
|
|
1792
|
+
*/
|
|
1793
|
+
function teamCommitStep(plan, root, message) {
|
|
1794
|
+
const managed = plan.teams.length > 0;
|
|
1795
|
+
const added = [];
|
|
1796
|
+
const removed = [];
|
|
1797
|
+
const note = (rel, action) => {
|
|
1798
|
+
if (action === 'deleted') removed.push(rel);
|
|
1799
|
+
else if (['created', 'updated', 'overwritten'].includes(action) || (action === 'unchanged' && managed)) added.push(rel);
|
|
1800
|
+
};
|
|
1801
|
+
for (const change of plan.changes) note(change.path, change.action);
|
|
1802
|
+
note(plan.settings.path, plan.settings.action);
|
|
1803
|
+
note(plan.manifestPath, plan.manifestAction);
|
|
1804
|
+
|
|
1805
|
+
const git = path.resolve(process.cwd()) === root ? 'git' : `git -C ${shellQuote(root)}`;
|
|
1806
|
+
const steps = [];
|
|
1807
|
+
if (added.length > 0) steps.push(`${git} add -- ${added.sort().join(' ')}`);
|
|
1808
|
+
if (removed.length > 0) steps.push(`${git} rm -q --cached --ignore-unmatch -- ${removed.sort().join(' ')}`);
|
|
1809
|
+
steps.push(`${git} commit -m "${message}"`);
|
|
1810
|
+
return steps.join(' && ');
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
const splitAgents = (value) => value.split(/[\s,]+/).filter(Boolean);
|
|
1814
|
+
|
|
1815
|
+
async function cmdTeam(args) {
|
|
1816
|
+
const action = args._[0];
|
|
1817
|
+
const spec = Object.hasOwn(TEAM_ACTIONS, action ?? '') ? TEAM_ACTIONS[action] : null;
|
|
1818
|
+
if (!spec) {
|
|
1819
|
+
die(
|
|
1820
|
+
`${action ? `unknown team command "${action}" — ` : ''}usage: agentmash team <add|remove|list|update>\n` +
|
|
1821
|
+
` e.g. npx agentmash team add "agentmash crew" --agents pipe,flex,probe`
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
for (const key of Object.keys(args)) {
|
|
1825
|
+
if (key === '_') continue;
|
|
1826
|
+
const flag = FLAG_SPELLING[key] ?? key;
|
|
1827
|
+
if (!spec.flags.includes(flag)) die(`--${flag} does not apply to team ${action}`);
|
|
1828
|
+
}
|
|
1829
|
+
for (const flag of TEAM_VALUE_FLAGS) {
|
|
1830
|
+
const value = args[flag];
|
|
1831
|
+
if (Object.hasOwn(args, flag) && (typeof value !== 'string' || value.startsWith('--') || !value.trim())) {
|
|
1832
|
+
die(`--${flag} needs a value`);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const rest = args._.slice(1);
|
|
1836
|
+
if (spec.name && rest.length === 0) die(`team ${action} needs a team name, e.g. npx agentmash team ${action} "agentmash crew"`);
|
|
1837
|
+
if (rest.length > (spec.name ? 1 : 0)) {
|
|
1838
|
+
die(
|
|
1839
|
+
spec.name
|
|
1840
|
+
? `the team name must be one argument; quote it: npx agentmash team ${action} "${rest.join(' ')}"`
|
|
1841
|
+
: `team ${action} takes no arguments (got ${rest.join(' ')})`
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
if (action === 'add' && args.agents === undefined) {
|
|
1845
|
+
die('team add needs --agents, e.g. npx agentmash team add "agentmash crew" --agents pipe,flex,probe');
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Any folder inside the repo will do; the crew's installer walks up to the
|
|
1849
|
+
// root itself, and refuses a home directory that happens to be a repo.
|
|
1850
|
+
const start = args.repo ? path.resolve(args.repo) : process.cwd();
|
|
1851
|
+
if (!findRepoRoot(start)) die("not inside a git repository — run this in your team's repo, or pass --repo <path>");
|
|
1852
|
+
|
|
1853
|
+
let crew;
|
|
1854
|
+
try {
|
|
1855
|
+
crew = await loadCrew();
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
die(err.message);
|
|
1858
|
+
}
|
|
1859
|
+
const { install, bundle } = crew;
|
|
1860
|
+
const dryRun = Boolean(args.dryRun);
|
|
1861
|
+
const force = Boolean(args.force);
|
|
1862
|
+
|
|
1863
|
+
// The installer validates everything before its first write and throws with
|
|
1864
|
+
// a message meant for people: a foreign file, a bad name, an unknown agent.
|
|
1865
|
+
const attempt = (fn) => {
|
|
1866
|
+
try {
|
|
1867
|
+
return fn();
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
die(err.message);
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
if (action === 'list') {
|
|
1874
|
+
const { root, teams } = attempt(() => install.listTeams({ repo: start, bundle }));
|
|
1875
|
+
if (args.json) {
|
|
1876
|
+
console.log(JSON.stringify({ teams }, null, 2));
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
if (teams.length === 0) {
|
|
1880
|
+
console.log(`\n no agentmash teams in ${c.dim(root)}\n`);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
console.log('');
|
|
1884
|
+
for (const team of teams) {
|
|
1885
|
+
console.log(` ${c.bold(team.name)} ${c.dim(`(${team.slug})`)}: ${team.members.map((m) => m.split('-')[0]).join(', ')}`);
|
|
1886
|
+
}
|
|
1887
|
+
console.log('');
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
let op;
|
|
1892
|
+
if (action === 'add') op = { type: 'add', name: rest[0], slug: args.slug, members: splitAgents(args.agents), force };
|
|
1893
|
+
else if (action === 'remove') {
|
|
1894
|
+
op = { type: 'remove', team: rest[0], members: args.agents === undefined ? undefined : splitAgents(args.agents), force };
|
|
1895
|
+
} else op = { type: 'update', force };
|
|
1896
|
+
|
|
1897
|
+
const { root, plan } = attempt(() => install.runTeamOp({ repo: start, bundle, op, dryRun }));
|
|
1898
|
+
const team = plan.target;
|
|
1899
|
+
if (action === 'update') {
|
|
1900
|
+
const count = plan.teams.length;
|
|
1901
|
+
console.log(`\n${c.bold(`${count} team${count === 1 ? '' : 's'}`)}, agentmash-crew ${bundle.meta.version}\n`);
|
|
1902
|
+
} else if (team.members === null) {
|
|
1903
|
+
console.log(`\n${c.bold(`removed team ${team.name}`)} ${c.dim(`(${team.slug})`)}\n`);
|
|
1904
|
+
} else {
|
|
1905
|
+
console.log(`\n${c.bold(`team ${team.name}`)} ${c.dim(`(${team.slug})`)}: ${team.members.join(', ')}\n`);
|
|
1906
|
+
}
|
|
1907
|
+
printTeamPlan(plan, dryRun);
|
|
1908
|
+
if (dryRun || !plan.changed) {
|
|
1909
|
+
console.log('');
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
if (action === 'add') {
|
|
1914
|
+
printTeamSteps([
|
|
1915
|
+
teamCommitStep(plan, root, `chore: ${plan.targetIsNew ? 'add' : 'update'} agentmash team ${team.name}`),
|
|
1916
|
+
TEAM_RESTART,
|
|
1917
|
+
`then say: ${team.name}, <your task> (or type: /team-${team.slug} <your task>)`,
|
|
1918
|
+
]);
|
|
1919
|
+
} else if (action === 'remove') {
|
|
1920
|
+
const message = team.members === null ? `chore: remove agentmash team ${team.name}` : `chore: update agentmash team ${team.name}`;
|
|
1921
|
+
printTeamSteps([teamCommitStep(plan, root, message), 'restart Claude Code in this repo so it reloads the agents']);
|
|
1922
|
+
} else {
|
|
1923
|
+
printTeamSteps([teamCommitStep(plan, root, `chore: update agentmash teams to ${bundle.meta.version}`), TEAM_RESTART]);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* The crew section of doctor: whether teams.json parses, how many teams it
|
|
1929
|
+
* records, and which of the files it records no longer have the bytes it
|
|
1930
|
+
* wrote. Crew teams work without a room or a server, so nothing here is an
|
|
1931
|
+
* AgentMash problem: warnings at most, and doctor's verdict never changes.
|
|
1932
|
+
*/
|
|
1933
|
+
async function doctorCrew(repo) {
|
|
1934
|
+
if (pathKind(path.join(repo, CREW_MANIFEST)) === 'missing') return;
|
|
1935
|
+
let state;
|
|
1936
|
+
let bundle;
|
|
1937
|
+
try {
|
|
1938
|
+
const crew = await loadCrew();
|
|
1939
|
+
bundle = crew.bundle;
|
|
1940
|
+
// `update` is the operation that touches every file teams.json lists, so
|
|
1941
|
+
// its snapshot hashes exactly the files there are to compare.
|
|
1942
|
+
state = crew.install.readState(repo, bundle, { type: 'update' });
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
warn(`crew teams: ${err.message}`);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
const { manifest } = state;
|
|
1948
|
+
if (!manifest) return; // removed between the look and the read
|
|
1949
|
+
const version = typeof manifest.source?.version === 'string' ? manifest.source.version : 'unknown version';
|
|
1950
|
+
const names = manifest.teams.map((team) => team.name).join(', ');
|
|
1951
|
+
ok(`crew teams: ${manifest.teams.length} ${c.dim(`${names || 'none'} · agentmash-crew ${version}`)}`);
|
|
1952
|
+
|
|
1953
|
+
const drifted = Object.keys(manifest.files).filter((file) => state.onDisk[file] !== manifest.files[file]);
|
|
1954
|
+
const missing = drifted.filter((file) => state.onDisk[file] === null);
|
|
1955
|
+
const changed = drifted.filter((file) => state.onDisk[file] !== null);
|
|
1956
|
+
if (missing.length > 0) {
|
|
1957
|
+
warn(`${missing.length} crew file${missing.length === 1 ? ' is' : 's are'} missing: ${missing.join(', ')}`);
|
|
1958
|
+
info(`${c.bold('npx agentmash team update')} puts ${missing.length === 1 ? 'it' : 'them'} back`);
|
|
1959
|
+
}
|
|
1960
|
+
if (changed.length > 0) {
|
|
1961
|
+
warn(`${changed.length} crew file${changed.length === 1 ? '' : 's'} changed since install: ${changed.join(', ')}`);
|
|
1962
|
+
info(`fine if that was on purpose; ${c.bold('npx agentmash team update --force')} puts the installed version back`);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
let guarded = false;
|
|
1966
|
+
try {
|
|
1967
|
+
const settings = JSON.parse(state.settingsText ?? '{}');
|
|
1968
|
+
guarded = JSON.stringify(settings?.hooks?.[bundle.meta.hook.event] ?? []).includes(bundle.meta.hook.marker);
|
|
1969
|
+
} catch {
|
|
1970
|
+
/* an unparseable settings.json has no guard in it either */
|
|
1971
|
+
}
|
|
1972
|
+
if (manifest.teams.length > 0 && !guarded) {
|
|
1973
|
+
warn(`the crew guard hook is not in .claude/settings.json — ${c.bold('npx agentmash team update')} adds it back`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1680
1977
|
function usage() {
|
|
1681
1978
|
console.log(`
|
|
1682
1979
|
${c.bold('agentmash')} — let your team see each other's work on one repository
|
|
@@ -1690,6 +1987,13 @@ ${c.bold('agentmash')} — let your team see each other's work on one repository
|
|
|
1690
1987
|
the post-commit client, for teammates
|
|
1691
1988
|
who are not using Claude Code
|
|
1692
1989
|
${c.bold('npx agentmash uninstall')} remove the hooks from this repo
|
|
1990
|
+
${c.bold('npx agentmash team add')} <name> --agents <a,b|all>
|
|
1991
|
+
put agents from the crew in this repo as a
|
|
1992
|
+
named team; Mash conducts every team
|
|
1993
|
+
${c.bold('npx agentmash team remove')} <name> [--agents <a,b>]
|
|
1994
|
+
remove a team, or only the agents given
|
|
1995
|
+
${c.bold('npx agentmash team list')} the teams installed here ${c.dim('(--json)')}
|
|
1996
|
+
${c.bold('npx agentmash team update')} rewrite the team files from this version of the crew
|
|
1693
1997
|
${c.bold('npx agentmash-digest')} the week in review, to a channel or your terminal
|
|
1694
1998
|
${c.dim('npx agentmash mcp')} serve the MCP tools on stdio ${c.dim('(what .mcp.json runs)')}
|
|
1695
1999
|
|
|
@@ -1707,6 +2011,10 @@ ${c.bold('agentmash')} — let your team see each other's work on one repository
|
|
|
1707
2011
|
${c.dim('--repo <path>')} target repository ${c.dim('(default: enclosing git repo)')}
|
|
1708
2012
|
${c.dim('--label <text>')} name for the room ${c.dim('(default: repo folder name)')}
|
|
1709
2013
|
${c.dim('--force')} with init: replace an existing room
|
|
2014
|
+
with team: overwrite files changed locally or not written by the crew
|
|
2015
|
+
${c.dim('--agents <a,b>')} with team: agents by short or full name (pipe, pipe-backend), or all
|
|
2016
|
+
${c.dim('--slug <slug>')} with team add: folder name for the team skill ${c.dim('(default: from the name)')}
|
|
2017
|
+
${c.dim('--dry-run')} with team: show what would change and write nothing
|
|
1710
2018
|
${c.dim('--no-git-hook')} with init/join: skip the post-commit client
|
|
1711
2019
|
${c.dim('--no-open')} with init/join/demo: do not open the browser
|
|
1712
2020
|
${c.dim('--force-hooks-path')} install into a core.hooksPath inside the working tree
|
|
@@ -1717,18 +2025,42 @@ ${c.bold('agentmash')} — let your team see each other's work on one repository
|
|
|
1717
2025
|
`);
|
|
1718
2026
|
}
|
|
1719
2027
|
|
|
2028
|
+
/**
|
|
2029
|
+
* The flags that take no value, as typed, and the key each is kept under.
|
|
2030
|
+
* Booleans have to be listed here: anything else takes the next token as its
|
|
2031
|
+
* value, so `team add --dry-run "agentmash crew"` would lose the name.
|
|
2032
|
+
*/
|
|
2033
|
+
const BOOLEAN_FLAGS = {
|
|
2034
|
+
'--force': 'force',
|
|
2035
|
+
'--no-git-hook': 'noGitHook',
|
|
2036
|
+
'--no-open': 'noOpen',
|
|
2037
|
+
'--force-hooks-path': 'forceHooksPath',
|
|
2038
|
+
'--yes': 'yes',
|
|
2039
|
+
'--local': 'local',
|
|
2040
|
+
'--dry-run': 'dryRun',
|
|
2041
|
+
'--json': 'json',
|
|
2042
|
+
'--help': 'help',
|
|
2043
|
+
'-h': 'help',
|
|
2044
|
+
};
|
|
2045
|
+
|
|
1720
2046
|
function parseArgs(argv) {
|
|
1721
2047
|
const args = { _: [] };
|
|
2048
|
+
// The crew's own command refuses a flag given twice, and keeping the last
|
|
2049
|
+
// one would install a smaller team than was typed: --agents pipe --agents flex.
|
|
2050
|
+
const set = (key, value) => {
|
|
2051
|
+
if (Object.hasOwn(args, key)) die(`--${key} is given twice`);
|
|
2052
|
+
args[key] = value;
|
|
2053
|
+
};
|
|
1722
2054
|
for (let i = 0; i < argv.length; i++) {
|
|
1723
2055
|
const token = argv[i];
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
else if (
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
else if (token.startsWith('--'))
|
|
2056
|
+
const eq = token.startsWith('--') ? token.indexOf('=') : -1;
|
|
2057
|
+
if (Object.hasOwn(BOOLEAN_FLAGS, token)) args[BOOLEAN_FLAGS[token]] = true;
|
|
2058
|
+
// --agents=all as well as --agents all, as the crew's own command reads it.
|
|
2059
|
+
// A boolean spelled that way is refused, so --dry-run=false is not a yes.
|
|
2060
|
+
else if (eq > 2) {
|
|
2061
|
+
if (Object.hasOwn(BOOLEAN_FLAGS, token.slice(0, eq))) die(`${token.slice(0, eq)} takes no value`);
|
|
2062
|
+
set(token.slice(2, eq), token.slice(eq + 1));
|
|
2063
|
+
} else if (token.startsWith('--')) set(token.slice(2), argv[++i]);
|
|
1732
2064
|
else args._.push(token);
|
|
1733
2065
|
}
|
|
1734
2066
|
return args;
|
|
@@ -1786,6 +2118,7 @@ const commands = {
|
|
|
1786
2118
|
purge: cmdPurge,
|
|
1787
2119
|
delete: cmdDelete,
|
|
1788
2120
|
uninstall: async (a) => cmdUninstall(a),
|
|
2121
|
+
team: cmdTeam,
|
|
1789
2122
|
mcp: cmdMcp,
|
|
1790
2123
|
};
|
|
1791
2124
|
|
package/crew/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# crew
|
|
2
|
+
|
|
3
|
+
The [agentmash crew](https://github.com/IskakovDamir/agentmash-crew): 18 Claude Code agents, Mash the conductor among them, vendored at [v1.2.0](https://github.com/IskakovDamir/agentmash-crew/tree/v1.2.0) (commit `5ad6818f6fcb926ea66f7389fd00a0041d4fc1c7`). `npx agentmash team`, the one-click pull request on agentmash.dev and the /agents pages all read these files, so a team installed any of those ways is byte for byte the team the crew's own installer writes. The install format is specified upstream in [docs/bundle-format.md](https://github.com/IskakovDamir/agentmash-crew/blob/v1.2.0/docs/bundle-format.md).
|
|
4
|
+
|
|
5
|
+
| file | where it comes from | what it is for |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `bundle.json` | `dist/bundle.json` | every file an install writes, the team skill template and the guard hook |
|
|
8
|
+
| `catalog.json` | `catalog.json` | the /agents pages, and the agent names the server routes accept |
|
|
9
|
+
| `team-plan.mjs` | `lib/team-plan.mjs` | the pure planner (`planChange`) the CLI and the server both run |
|
|
10
|
+
| `install.mjs` | `lib/install.mjs` | the file-system side the CLI runs (`runTeamOp`, `listTeams`, `loadBundle`) |
|
|
11
|
+
| `team-plan.d.mts` | this repo, `tools/crew-team-plan.d.mts` | TypeScript declarations for `team-plan.mjs` |
|
|
12
|
+
| `crew.lock.json` | the sync | the tag, the commit and the sha256 of every other file here |
|
|
13
|
+
|
|
14
|
+
## Never edit by hand
|
|
15
|
+
|
|
16
|
+
Everything here is written by `tools/sync-crew.mjs`, and `cli/test/crew-lock.test.mjs` re-hashes it against `crew.lock.json`, so a hand edit fails the tests. A change to an agent, the bundle or the planner goes upstream, gets a tag, and comes here by a sync. The declarations are the one file kept in this repo: edit `tools/crew-team-plan.d.mts` and sync again.
|
|
17
|
+
|
|
18
|
+
## Updating
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git -C ../agentmash-crew fetch --tags
|
|
22
|
+
git -C ../agentmash-crew checkout v<new version>
|
|
23
|
+
node tools/sync-crew.mjs --from ../agentmash-crew
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The script refuses a checkout that has local changes or is not exactly at the tag, and checks every file against the blob in the tagged commit. Review the diff of `crew/`, run the tests, and commit `crew/` with its lock. The CLI package carries its own copy of these files, so people running `npx agentmash` get a new crew version with the next CLI release.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
`bundle.json`, `catalog.json`, `team-plan.mjs` and `install.mjs` are copied from [agentmash-crew](https://github.com/IskakovDamir/agentmash-crew) and are under its MIT License:
|
|
31
|
+
|
|
32
|
+
MIT License
|
|
33
|
+
|
|
34
|
+
Copyright (c) 2026 Damir Iskakov
|
|
35
|
+
|
|
36
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
37
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
38
|
+
in the Software without restriction, including without limitation the rights
|
|
39
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
40
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
41
|
+
furnished to do so, subject to the following conditions:
|
|
42
|
+
|
|
43
|
+
The above copyright notice and this permission notice shall be included in all
|
|
44
|
+
copies or substantial portions of the Software.
|
|
45
|
+
|
|
46
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
47
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
48
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
49
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
50
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
51
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
52
|
+
SOFTWARE.
|