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 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
- if (token === '--force') args.force = true;
1725
- else if (token === '--no-git-hook') args.noGitHook = true;
1726
- else if (token === '--no-open') args.noOpen = true;
1727
- else if (token === '--force-hooks-path') args.forceHooksPath = true;
1728
- else if (token === '--yes') args.yes = true;
1729
- else if (token === '--local') args.local = true;
1730
- else if (token === '--help' || token === '-h') args.help = true;
1731
- else if (token.startsWith('--')) args[token.slice(2)] = argv[++i];
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.