agent-guild 1.2.1__tar.gz → 1.3.0__tar.gz

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.
Files changed (30) hide show
  1. {agent_guild-1.2.1 → agent_guild-1.3.0}/CHANGELOG.md +10 -0
  2. agent_guild-1.2.1/README.md → agent_guild-1.3.0/PKG-INFO +53 -20
  3. agent_guild-1.2.1/PKG-INFO → agent_guild-1.3.0/README.md +34 -39
  4. {agent_guild-1.2.1 → agent_guild-1.3.0}/pyproject.toml +3 -3
  5. {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/cli.py +14 -6
  6. {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/installer.py +103 -30
  7. {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/specialists.py +1 -1
  8. agent_guild-1.3.0/src/agent_guild/vendors.py +119 -0
  9. agent_guild-1.3.0/templates/agents-rules.md +7 -0
  10. {agent_guild-1.2.1 → agent_guild-1.3.0}/tests/test_cli.py +163 -17
  11. {agent_guild-1.2.1 → agent_guild-1.3.0}/uv.lock +1 -1
  12. agent_guild-1.2.1/templates/agents-rules.md +0 -6
  13. {agent_guild-1.2.1 → agent_guild-1.3.0}/.github/workflows/ci.yml +0 -0
  14. {agent_guild-1.2.1 → agent_guild-1.3.0}/.github/workflows/release.yml +0 -0
  15. {agent_guild-1.2.1 → agent_guild-1.3.0}/.gitignore +0 -0
  16. {agent_guild-1.2.1 → agent_guild-1.3.0}/LICENSE +0 -0
  17. {agent_guild-1.2.1 → agent_guild-1.3.0}/agents/architect.md +0 -0
  18. {agent_guild-1.2.1 → agent_guild-1.3.0}/agents/code-grader.md +0 -0
  19. {agent_guild-1.2.1 → agent_guild-1.3.0}/agents/coder.md +0 -0
  20. {agent_guild-1.2.1 → agent_guild-1.3.0}/agents/utility.md +0 -0
  21. {agent_guild-1.2.1 → agent_guild-1.3.0}/development-workflow.md +0 -0
  22. {agent_guild-1.2.1 → agent_guild-1.3.0}/docs/manual_installation.md +0 -0
  23. {agent_guild-1.2.1 → agent_guild-1.3.0}/scripts/release_notes.py +0 -0
  24. {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/__init__.py +0 -0
  25. {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/files.py +0 -0
  26. {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/coders/python.md +0 -0
  27. {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/coders/swift.md +0 -0
  28. {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/coders/typescript.md +0 -0
  29. {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/memory-bank-rules.md +0 -0
  30. {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/sdd-rules.md +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.3.0] - 2026-10-22
6
+
7
+ - Cursor and Codex support. The installer detects the tools a project uses (`.claude/` or `CLAUDE.md`, `.cursor/`,
8
+ `.codex/` or `.agents/`; Claude Code when none) and installs for each one, or for `--tools claude,cursor,codex`.
9
+ Agents are written in each tool's format (Cursor `readonly`, Codex TOML), skills go to `.cursor/skills/` and
10
+ `.codex/skills/`, and the jevmem and language-coder MCP servers to `.cursor/mcp.json` and `.codex/config.toml`.
11
+ Hooks, auto-memory and the model setting stay Claude Code only.
12
+ - The rules sections now go to `AGENTS.md` (created if missing; `CLAUDE.md` only when it is the sole file). With Claude
13
+ Code, `CLAUDE.md` gets an `@AGENTS.md` import.
14
+
5
15
  ## [1.2.1] - 2026-10-22
6
16
 
7
17
  - The install log says `skipped ... section '<name>' is already in CLAUDE.md or AGENTS.md` instead of the confusing
@@ -1,9 +1,29 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-guild
3
+ Version: 1.3.0
4
+ Summary: A multi-agent setup for Claude Code, Cursor and Codex (orchestrator + architect, coder, grader, utility subagents) with memory-bank, jevmem and Spec-Driven Development wired in, installed into a project with one command.
5
+ Project-URL: Homepage, https://github.com/illescasDaniel/agent-guild
6
+ Project-URL: Issues, https://github.com/illescasDaniel/agent-guild/issues
7
+ Author-email: Daniel Illescas Romero <illescas.daniel@protonmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: claude,claude-code,codex,cursor,memory,multi-agent,orchestrator,sdd,spec-driven-development,subagents
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Topic :: Software Development
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+
1
20
  # agent-guild
2
21
 
3
22
  [![PyPI](https://img.shields.io/pypi/v/agent-guild)](https://pypi.org/project/agent-guild/)
4
23
 
5
- A ready-made **multi-agent setup for [Claude Code](https://claude.com/claude-code)**, installed into a project with one
6
- command. A Sonnet **orchestrator** delegates to specialist subagents (Opus architect and grader, Sonnet coders, Haiku
24
+ A ready-made **multi-agent setup for [Claude Code](https://claude.com/claude-code), [Cursor](https://cursor.com) and
25
+ [Codex](https://developers.openai.com/codex)**, installed into a project with one command. (The Sonnet/Opus/Haiku
26
+ tiering below is Claude Code's; Cursor and Codex agents inherit the session model.) A Sonnet **orchestrator** delegates to specialist subagents (Opus architect and grader, Sonnet coders, Haiku
7
27
  utility), and three features come wired in:
8
28
 
9
29
  - **Multiple agents**: `architect`, `coder` (parallel-safe), `code-grader` and `utility`, plus the orchestrator rules
@@ -38,7 +58,7 @@ uvx agent-guild /path/to/project
38
58
 
39
59
  ## Files
40
60
 
41
- | File | Install to | Role |
61
+ | File | Install to (Claude Code) | Role |
42
62
  |---|---|---|
43
63
  | [development-workflow.md](development-workflow.md) | imported from `~/.claude/CLAUDE.md` | Orchestrator rules: knowledge layers, routing, SDD phase → agent map, briefs, memory protocol |
44
64
  | [agents/architect.md](agents/architect.md) | `~/.claude/agents/` | Opus, read-only. Plans, SDD Phase 1 design decisions, Phase 2 interfaces, root causes |
@@ -46,6 +66,8 @@ uvx agent-guild /path/to/project
46
66
  | [agents/code-grader.md](agents/code-grader.md) | `~/.claude/agents/` | Opus, read-only. SDD Phase 5. Body identical to the sdd-feature skill's grader; only the frontmatter differs |
47
67
  | [agents/utility.md](agents/utility.md) | `~/.claude/agents/` | Haiku. Checks, git, moves, bulk edits |
48
68
 
69
+ These are the Claude Code sources; the installer converts them for Cursor and Codex.
70
+
49
71
  ## Install
50
72
 
51
73
  ### Quick: the installer (per project)
@@ -61,26 +83,37 @@ uvx agent-guild /path/to/project --dry-run
61
83
  uvx agent-guild /path/to/project
62
84
  ```
63
85
 
64
- It does, in the project:
65
-
66
- - copies `agents/*.md` and `development-workflow.md` into `.claude/`;
67
- - copies the `memory-bank`, `jev-memory` and `sdd-feature` skills into `.claude/skills/` (cloned from GitHub, or from
68
- local clones with `--sources-dir DIR`);
69
- - adds three removable sections to `CLAUDE.md`: *Agents and delegation*, *Memory bank* and *Phase Gate Protocol*. They
70
- live in `CLAUDE.md`, not `AGENTS.md`, because they are rules for Claude Code and its subagents; `AGENTS.md` stays
71
- tool-neutral. If the project has an `AGENTS.md`, `CLAUDE.md` starts with `@AGENTS.md`. A section that `CLAUDE.md` or
72
- `AGENTS.md` already has is not added again;
73
- - adds the pinned jevmem server to `.mcp.json` and the jevmem hooks to `.claude/settings.json`, merging into existing
74
- files;
75
- - sets `autoMemoryEnabled: false` and the project's model to `sonnet`.
86
+ It does, in the project, for each coding tool it finds (see **Tools** below):
87
+
88
+ - copies the agents in that tool's format and folder (`.claude/agents/*.md`; `.cursor/agents/*.md` with `readonly`
89
+ instead of `disallowedTools`; `.codex/agents/*.toml`), and one shared `development-workflow.md` next to the first
90
+ tool's agents;
91
+ - copies the `memory-bank`, `jev-memory` and `sdd-feature` skills into that tool's skills folder (`.claude/skills/`,
92
+ `.cursor/skills/`, `.codex/skills/` for Codex), cloned from GitHub, or from local clones with `--sources-dir DIR`;
93
+ - adds three removable sections to `AGENTS.md`: *Agents and delegation*, *Memory bank* and *Phase Gate Protocol*. If the
94
+ project has only a `CLAUDE.md`, they go there instead; with neither, `AGENTS.md` is created. When Claude Code is one
95
+ of the tools, `CLAUDE.md` gets an `@AGENTS.md` import (Claude Code doesn't read `AGENTS.md`). A section that either
96
+ file already has is not added again;
97
+ - adds the pinned jevmem server to the tool's MCP config (`.mcp.json`, `.cursor/mcp.json`, `.codex/config.toml`),
98
+ merging into existing files;
99
+ - for Claude Code only: adds the jevmem hooks to `.claude/settings.json`, sets `autoMemoryEnabled: false` and the
100
+ project's model to `sonnet`.
101
+
102
+ **Tools.** The installer detects what the project uses: `.claude/` or `CLAUDE.md` → Claude Code, `.cursor/` → Cursor,
103
+ `.codex/` or `.agents/` → Codex; with several, it installs for all of them, and with none, for Claude Code. Override
104
+ with `--tools claude,cursor,codex`. Cursor and Codex agents don't carry the Claude model tiering (Cursor gets
105
+ `model: inherit`, Codex inherits the session model) and Codex has no read-only switch for an agent, so the read-only
106
+ agents say it in their instructions. Cursor also reads `.claude/` and `.agents/` folders, so with several tools
107
+ installed it may list a skill twice.
76
108
 
77
109
  | Flag | Effect |
78
110
  |---|---|
79
- | `--with coder-python` / `coder-swift` / `coder-typescript` | Add a language coder (repeatable): the generic coder, required to use that language's code-navigation MCP ([codenav-mcp](https://github.com/illescasDaniel/codenav-mcp), [codenav-swift-mcp](https://github.com/illescasDaniel/codenav-swift-mcp), [webnav-ts-mcp](https://github.com/illescasDaniel/webnav-ts-mcp)). Also adds the MCP server to `.mcp.json` (an existing entry is kept) and a `CLAUDE.md` rule that routes that language's work to it. The MCP's own prerequisites (uv, a Swift toolchain, Node) are printed after the install |
80
- | `--coders-only` | With `--with`: install only the language coders (agent, MCP server, `CLAUDE.md` rule) and nothing else. After a normal install, the tool detects Python, Swift and TypeScript/JS projects and prints this command for the coders you don't have yet |
111
+ | `--tools claude,cursor,codex` | Install for these tools instead of the detected ones |
112
+ | `--with coder-python` / `coder-swift` / `coder-typescript` | Add a language coder (repeatable): the generic coder, required to use that language's code-navigation MCP ([codenav-mcp](https://github.com/illescasDaniel/codenav-mcp), [codenav-swift-mcp](https://github.com/illescasDaniel/codenav-swift-mcp), [webnav-ts-mcp](https://github.com/illescasDaniel/webnav-ts-mcp)). Also adds the MCP server to `.mcp.json` (an existing entry is kept) and an `AGENTS.md` rule that routes that language's work to it. The MCP's own prerequisites (uv, a Swift toolchain, Node) are printed after the install |
113
+ | `--coders-only` | With `--with`: install only the language coders (agent, MCP server, `AGENTS.md` rule) and nothing else. After a normal install, the tool detects Python, Swift and TypeScript/JS projects and prints this command for the coders you don't have yet |
81
114
  | `--without sdd` / `memory-bank` / `jevmem` | Leave a component out. `sdd` also drops `code-grader` and the gate section; `jevmem` drops its skill, server, hooks and the auto-memory switch |
82
115
  | `--model ALIAS` | Project main-session model; default `sonnet` (the per-project alternative to the global setting below), `none` leaves it unset |
83
- | `--import-cursor-skills` | Also copy the project's `.cursor/skills/*` into `.claude/skills/`; skills that already exist are kept |
116
+ | `--import-cursor-skills` | Also copy the project's `.cursor/skills/*` into `.claude/skills/` (needs `claude` among the tools); skills that already exist are kept |
84
117
  | `--personal-settings` | Put `--model` and `autoMemoryEnabled` in the git-ignored `.claude/settings.local.json` |
85
118
  | `--import-auto-memory` | First import the project's existing auto-memory notes into jevmem |
86
119
  | `--keep-auto-memory` | Don't switch auto-memory off |
@@ -106,7 +139,7 @@ wheel bundles them.
106
139
 
107
140
  ## Adding it to a project that already has its own setup
108
141
 
109
- The installer never overwrites a file that differs from the shipped one (and skips CLAUDE.md sections that `AGENTS.md`
142
+ The installer never overwrites a file that differs from the shipped one (and skips rules sections that `AGENTS.md`
110
143
  or `CLAUDE.md` already has), so it is safe to run on a project with its own agents, skills and rules. After it runs:
111
144
 
112
145
  - **Skipped files are yours.** Anything the log marks `skipped ... differs` keeps your version. If it is an older or
@@ -138,7 +171,7 @@ or `CLAUDE.md` already has), so it is safe to run on a project with its own agen
138
171
  navigation MCPs; in practice it still reached for `grep` and `sed`, because the MCP tools arrive deferred (name
139
172
  only) and the brief or its own habits point elsewhere. `coder-python`, `coder-swift` and `coder-typescript`
140
173
  (`--with`) are generated from `coder.md` plus a navigation section that names the MCP's tools, and list those tools
141
- in `tools:` so they load up front. A `CLAUDE.md` rule routes that language's work to them. Without `--with`, nothing
174
+ in `tools:` so they load up front. An `AGENTS.md` rule routes that language's work to them. Without `--with`, nothing
142
175
  changes.
143
176
  - **The grader runs on Opus.** The code it grades was written by Sonnet coders, so a different, stronger model gives
144
177
  a more independent review.
@@ -1,28 +1,10 @@
1
- Metadata-Version: 2.5
2
- Name: agent-guild
3
- Version: 1.2.1
4
- Summary: A multi-agent setup for Claude Code (orchestrator + architect, coder, grader, utility subagents) with memory-bank, jevmem and Spec-Driven Development wired in, installed into a project with one command.
5
- Project-URL: Homepage, https://github.com/illescasDaniel/agent-guild
6
- Project-URL: Issues, https://github.com/illescasDaniel/agent-guild/issues
7
- Author-email: Daniel Illescas Romero <illescas.daniel@protonmail.com>
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Keywords: claude,claude-code,memory,multi-agent,orchestrator,sdd,spec-driven-development,subagents
11
- Classifier: Development Status :: 5 - Production/Stable
12
- Classifier: Environment :: Console
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Topic :: Software Development
17
- Requires-Python: >=3.11
18
- Description-Content-Type: text/markdown
19
-
20
1
  # agent-guild
21
2
 
22
3
  [![PyPI](https://img.shields.io/pypi/v/agent-guild)](https://pypi.org/project/agent-guild/)
23
4
 
24
- A ready-made **multi-agent setup for [Claude Code](https://claude.com/claude-code)**, installed into a project with one
25
- command. A Sonnet **orchestrator** delegates to specialist subagents (Opus architect and grader, Sonnet coders, Haiku
5
+ A ready-made **multi-agent setup for [Claude Code](https://claude.com/claude-code), [Cursor](https://cursor.com) and
6
+ [Codex](https://developers.openai.com/codex)**, installed into a project with one command. (The Sonnet/Opus/Haiku
7
+ tiering below is Claude Code's; Cursor and Codex agents inherit the session model.) A Sonnet **orchestrator** delegates to specialist subagents (Opus architect and grader, Sonnet coders, Haiku
26
8
  utility), and three features come wired in:
27
9
 
28
10
  - **Multiple agents**: `architect`, `coder` (parallel-safe), `code-grader` and `utility`, plus the orchestrator rules
@@ -57,7 +39,7 @@ uvx agent-guild /path/to/project
57
39
 
58
40
  ## Files
59
41
 
60
- | File | Install to | Role |
42
+ | File | Install to (Claude Code) | Role |
61
43
  |---|---|---|
62
44
  | [development-workflow.md](development-workflow.md) | imported from `~/.claude/CLAUDE.md` | Orchestrator rules: knowledge layers, routing, SDD phase → agent map, briefs, memory protocol |
63
45
  | [agents/architect.md](agents/architect.md) | `~/.claude/agents/` | Opus, read-only. Plans, SDD Phase 1 design decisions, Phase 2 interfaces, root causes |
@@ -65,6 +47,8 @@ uvx agent-guild /path/to/project
65
47
  | [agents/code-grader.md](agents/code-grader.md) | `~/.claude/agents/` | Opus, read-only. SDD Phase 5. Body identical to the sdd-feature skill's grader; only the frontmatter differs |
66
48
  | [agents/utility.md](agents/utility.md) | `~/.claude/agents/` | Haiku. Checks, git, moves, bulk edits |
67
49
 
50
+ These are the Claude Code sources; the installer converts them for Cursor and Codex.
51
+
68
52
  ## Install
69
53
 
70
54
  ### Quick: the installer (per project)
@@ -80,26 +64,37 @@ uvx agent-guild /path/to/project --dry-run
80
64
  uvx agent-guild /path/to/project
81
65
  ```
82
66
 
83
- It does, in the project:
84
-
85
- - copies `agents/*.md` and `development-workflow.md` into `.claude/`;
86
- - copies the `memory-bank`, `jev-memory` and `sdd-feature` skills into `.claude/skills/` (cloned from GitHub, or from
87
- local clones with `--sources-dir DIR`);
88
- - adds three removable sections to `CLAUDE.md`: *Agents and delegation*, *Memory bank* and *Phase Gate Protocol*. They
89
- live in `CLAUDE.md`, not `AGENTS.md`, because they are rules for Claude Code and its subagents; `AGENTS.md` stays
90
- tool-neutral. If the project has an `AGENTS.md`, `CLAUDE.md` starts with `@AGENTS.md`. A section that `CLAUDE.md` or
91
- `AGENTS.md` already has is not added again;
92
- - adds the pinned jevmem server to `.mcp.json` and the jevmem hooks to `.claude/settings.json`, merging into existing
93
- files;
94
- - sets `autoMemoryEnabled: false` and the project's model to `sonnet`.
67
+ It does, in the project, for each coding tool it finds (see **Tools** below):
68
+
69
+ - copies the agents in that tool's format and folder (`.claude/agents/*.md`; `.cursor/agents/*.md` with `readonly`
70
+ instead of `disallowedTools`; `.codex/agents/*.toml`), and one shared `development-workflow.md` next to the first
71
+ tool's agents;
72
+ - copies the `memory-bank`, `jev-memory` and `sdd-feature` skills into that tool's skills folder (`.claude/skills/`,
73
+ `.cursor/skills/`, `.codex/skills/` for Codex), cloned from GitHub, or from local clones with `--sources-dir DIR`;
74
+ - adds three removable sections to `AGENTS.md`: *Agents and delegation*, *Memory bank* and *Phase Gate Protocol*. If the
75
+ project has only a `CLAUDE.md`, they go there instead; with neither, `AGENTS.md` is created. When Claude Code is one
76
+ of the tools, `CLAUDE.md` gets an `@AGENTS.md` import (Claude Code doesn't read `AGENTS.md`). A section that either
77
+ file already has is not added again;
78
+ - adds the pinned jevmem server to the tool's MCP config (`.mcp.json`, `.cursor/mcp.json`, `.codex/config.toml`),
79
+ merging into existing files;
80
+ - for Claude Code only: adds the jevmem hooks to `.claude/settings.json`, sets `autoMemoryEnabled: false` and the
81
+ project's model to `sonnet`.
82
+
83
+ **Tools.** The installer detects what the project uses: `.claude/` or `CLAUDE.md` → Claude Code, `.cursor/` → Cursor,
84
+ `.codex/` or `.agents/` → Codex; with several, it installs for all of them, and with none, for Claude Code. Override
85
+ with `--tools claude,cursor,codex`. Cursor and Codex agents don't carry the Claude model tiering (Cursor gets
86
+ `model: inherit`, Codex inherits the session model) and Codex has no read-only switch for an agent, so the read-only
87
+ agents say it in their instructions. Cursor also reads `.claude/` and `.agents/` folders, so with several tools
88
+ installed it may list a skill twice.
95
89
 
96
90
  | Flag | Effect |
97
91
  |---|---|
98
- | `--with coder-python` / `coder-swift` / `coder-typescript` | Add a language coder (repeatable): the generic coder, required to use that language's code-navigation MCP ([codenav-mcp](https://github.com/illescasDaniel/codenav-mcp), [codenav-swift-mcp](https://github.com/illescasDaniel/codenav-swift-mcp), [webnav-ts-mcp](https://github.com/illescasDaniel/webnav-ts-mcp)). Also adds the MCP server to `.mcp.json` (an existing entry is kept) and a `CLAUDE.md` rule that routes that language's work to it. The MCP's own prerequisites (uv, a Swift toolchain, Node) are printed after the install |
99
- | `--coders-only` | With `--with`: install only the language coders (agent, MCP server, `CLAUDE.md` rule) and nothing else. After a normal install, the tool detects Python, Swift and TypeScript/JS projects and prints this command for the coders you don't have yet |
92
+ | `--tools claude,cursor,codex` | Install for these tools instead of the detected ones |
93
+ | `--with coder-python` / `coder-swift` / `coder-typescript` | Add a language coder (repeatable): the generic coder, required to use that language's code-navigation MCP ([codenav-mcp](https://github.com/illescasDaniel/codenav-mcp), [codenav-swift-mcp](https://github.com/illescasDaniel/codenav-swift-mcp), [webnav-ts-mcp](https://github.com/illescasDaniel/webnav-ts-mcp)). Also adds the MCP server to `.mcp.json` (an existing entry is kept) and an `AGENTS.md` rule that routes that language's work to it. The MCP's own prerequisites (uv, a Swift toolchain, Node) are printed after the install |
94
+ | `--coders-only` | With `--with`: install only the language coders (agent, MCP server, `AGENTS.md` rule) and nothing else. After a normal install, the tool detects Python, Swift and TypeScript/JS projects and prints this command for the coders you don't have yet |
100
95
  | `--without sdd` / `memory-bank` / `jevmem` | Leave a component out. `sdd` also drops `code-grader` and the gate section; `jevmem` drops its skill, server, hooks and the auto-memory switch |
101
96
  | `--model ALIAS` | Project main-session model; default `sonnet` (the per-project alternative to the global setting below), `none` leaves it unset |
102
- | `--import-cursor-skills` | Also copy the project's `.cursor/skills/*` into `.claude/skills/`; skills that already exist are kept |
97
+ | `--import-cursor-skills` | Also copy the project's `.cursor/skills/*` into `.claude/skills/` (needs `claude` among the tools); skills that already exist are kept |
103
98
  | `--personal-settings` | Put `--model` and `autoMemoryEnabled` in the git-ignored `.claude/settings.local.json` |
104
99
  | `--import-auto-memory` | First import the project's existing auto-memory notes into jevmem |
105
100
  | `--keep-auto-memory` | Don't switch auto-memory off |
@@ -125,7 +120,7 @@ wheel bundles them.
125
120
 
126
121
  ## Adding it to a project that already has its own setup
127
122
 
128
- The installer never overwrites a file that differs from the shipped one (and skips CLAUDE.md sections that `AGENTS.md`
123
+ The installer never overwrites a file that differs from the shipped one (and skips rules sections that `AGENTS.md`
129
124
  or `CLAUDE.md` already has), so it is safe to run on a project with its own agents, skills and rules. After it runs:
130
125
 
131
126
  - **Skipped files are yours.** Anything the log marks `skipped ... differs` keeps your version. If it is an older or
@@ -157,7 +152,7 @@ or `CLAUDE.md` already has), so it is safe to run on a project with its own agen
157
152
  navigation MCPs; in practice it still reached for `grep` and `sed`, because the MCP tools arrive deferred (name
158
153
  only) and the brief or its own habits point elsewhere. `coder-python`, `coder-swift` and `coder-typescript`
159
154
  (`--with`) are generated from `coder.md` plus a navigation section that names the MCP's tools, and list those tools
160
- in `tools:` so they load up front. A `CLAUDE.md` rule routes that language's work to them. Without `--with`, nothing
155
+ in `tools:` so they load up front. An `AGENTS.md` rule routes that language's work to them. Without `--with`, nothing
161
156
  changes.
162
157
  - **The grader runs on Opus.** The code it grades was written by Sonnet coders, so a different, stronger model gives
163
158
  a more independent review.
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "agent-guild"
3
- version = "1.2.1"
4
- description = "A multi-agent setup for Claude Code (orchestrator + architect, coder, grader, utility subagents) with memory-bank, jevmem and Spec-Driven Development wired in, installed into a project with one command."
3
+ version = "1.3.0"
4
+ description = "A multi-agent setup for Claude Code, Cursor and Codex (orchestrator + architect, coder, grader, utility subagents) with memory-bank, jevmem and Spec-Driven Development wired in, installed into a project with one command."
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
8
8
  authors = [{ name = "Daniel Illescas Romero", email = "illescas.daniel@protonmail.com" }]
9
- keywords = ["claude-code", "claude", "subagents", "multi-agent", "orchestrator", "memory", "spec-driven-development", "sdd"]
9
+ keywords = ["claude-code", "cursor", "codex", "claude", "subagents", "multi-agent", "orchestrator", "memory", "spec-driven-development", "sdd"]
10
10
  classifiers = [
11
11
  "Development Status :: 5 - Production/Stable",
12
12
  "Environment :: Console",
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from .files import Ctx
7
7
  from .installer import COMPONENTS, Options, run_all
8
8
  from .specialists import SPECIALISTS, detect_languages
9
+ from .vendors import TOOLS, VENDORS, agent_filename, detect_tools, parse_tools
9
10
 
10
11
  _BUNDLED = Path(__file__).resolve().parent / "_assets" # present in installed wheels
11
12
  DEFAULT_ASSETS = _BUNDLED if _BUNDLED.is_dir() else Path(__file__).resolve().parents[2] # else: a source checkout
@@ -14,27 +15,27 @@ KEPT_HINT = """Files marked "skipped ... differs" keep your version. If a skill
14
15
  copy of a shipped one, diff it against the shipped file and merge the changes by hand (--force replaces it)."""
15
16
 
16
17
  NEXT_STEPS = """Next steps:
17
- - Review the diff (git status / git diff); edit or delete any copied file or CLAUDE.md section you don't want.
18
+ - Review the diff (git status / git diff); edit or delete any copied file or AGENTS.md section you don't want.
18
19
  - Per machine, once: `uvx jevmem config init` and `uvx --from jevmem jevmem warmup`.
19
20
  - With memory-bank: ask Claude to "set up the memory bank".
20
- - Restart Claude Code in the project so the agents, skills, hooks and MCP server load."""
21
+ - Restart your coding tool (Claude Code, Cursor, Codex) in the project so the agents, skills, hooks and MCP server load."""
21
22
 
22
23
 
23
24
  CODERS_NEXT_STEPS = """Next steps:
24
25
  - Review the diff (git status / git diff).
25
- - Restart Claude Code in the project so the agents and MCP server load."""
26
+ - Restart your coding tool in the project so the agents and MCP server load."""
26
27
 
27
28
 
28
29
  def suggest_coders(project: Path, chosen: list[str]) -> None:
29
30
  """After an install, point at the language coders that fit the project and are not installed yet."""
30
31
  missing = [
31
32
  name for name in detect_languages(project)
32
- if name not in chosen and not (project / ".claude" / "agents" / f"{name}.md").exists()
33
+ if name not in chosen and not any((project / v.agents_dir / agent_filename(t, name)).exists() for t, v in VENDORS.items())
33
34
  ]
34
35
  if not missing:
35
36
  return
36
37
  print("\nThis project looks like it uses code that a language coder can navigate and refactor with a "
37
- "code-navigation MCP (optional; adds only the agent, the MCP server and a CLAUDE.md routing rule):")
38
+ "code-navigation MCP (optional; adds only the agent, the MCP server and an AGENTS.md routing rule):")
38
39
  for name in missing:
39
40
  spec = SPECIALISTS[name]
40
41
  print(f" - {spec.label}: uvx agent-guild {project} --with {name} --coders-only")
@@ -48,13 +49,17 @@ def build_parser() -> argparse.ArgumentParser:
48
49
  "unless --force.",
49
50
  )
50
51
  p.add_argument("project", type=Path, help="path to the project's root")
52
+ p.add_argument("--tools", metavar="LIST",
53
+ help=f"comma-separated tools to install for: {', '.join(TOOLS)}. Default: the ones the project already "
54
+ "uses (.claude/ or CLAUDE.md, .cursor/, .codex/ or .agents/), or claude when none is found. Each gets "
55
+ "agents and skills in its own format and folder, and an MCP config")
51
56
  p.add_argument("--without", action="append", default=[], choices=COMPONENTS, metavar="COMPONENT",
52
57
  help=f"leave a component out (repeatable): {', '.join(COMPONENTS)}. "
53
58
  "'sdd' also drops the code-grader agent; 'jevmem' drops skill, MCP server, hooks and the auto-memory switch")
54
59
  p.add_argument("--with", dest="with_coders", action="append", default=[], choices=sorted(SPECIALISTS),
55
60
  metavar="CODER",
56
61
  help=f"add a language coder (repeatable): {', '.join(sorted(SPECIALISTS))}. Each is the generic coder plus "
57
- "a code-navigation MCP it must use for that language; the MCP server and a CLAUDE.md routing rule "
62
+ "a code-navigation MCP it must use for that language; the MCP server and an AGENTS.md routing rule "
58
63
  "are added too. Off by default")
59
64
  p.add_argument("--coders-only", action="store_true",
60
65
  help="with --with: install only the language coders (agent, MCP server, routing rule) and leave "
@@ -89,6 +94,7 @@ def main(argv: list[str] | None = None) -> int:
89
94
  raise SystemExit(f"error: {project} is not a directory")
90
95
  if not (args.assets / "agents").is_dir():
91
96
  raise SystemExit(f"error: no agents/ in {args.assets}; run from a checkout of agent-guild or pass --assets")
97
+ tools = parse_tools(args.tools) if args.tools else detect_tools(project)
92
98
  ctx = Ctx(project=project, dry_run=args.dry_run, force=args.force)
93
99
  opts = Options(
94
100
  assets=args.assets,
@@ -102,9 +108,11 @@ def main(argv: list[str] | None = None) -> int:
102
108
  import_auto_memory=args.import_auto_memory,
103
109
  import_cursor_skills=args.import_cursor_skills,
104
110
  git_exclude=args.git_exclude,
111
+ tools=frozenset(tools),
105
112
  )
106
113
  run_all(ctx, opts)
107
114
  print(f"{'Dry run for' if args.dry_run else 'Installed into'} {project}")
115
+ print(f" tools: {', '.join(tools)} ({'--tools' if args.tools else 'detected'})")
108
116
  print("\n".join(ctx.lines))
109
117
  for w in ctx.warnings:
110
118
  print(f"warning: {w}")
@@ -8,11 +8,13 @@ import re
8
8
  import shutil
9
9
  import subprocess
10
10
  import tempfile
11
+ import tomllib
11
12
  from dataclasses import dataclass
12
13
  from pathlib import Path
13
14
 
14
15
  from .files import Ctx, copy_tree, read_json, write_bytes, write_json
15
16
  from .specialists import SPECIALISTS, render
17
+ from .vendors import TOOLS, VENDORS, agent_filename, codex_server_block, convert_agent, cursor_server, workflow_path
16
18
 
17
19
  JEVMEM_SPEC = "jevmem>=0.4.0,<0.5"
18
20
  JEVMEM_SERVER = {"type": "stdio", "command": "uvx", "args": ["--from", JEVMEM_SPEC, "jevmem-mcp"]}
@@ -20,7 +22,6 @@ JEVMEM_HOOKS = {
20
22
  "SessionStart": ("jevmem hook session-start", 15),
21
23
  "UserPromptSubmit": ("jevmem hook user-prompt", 20),
22
24
  }
23
- WORKFLOW_IMPORT = "@.claude/development-workflow.md"
24
25
 
25
26
  # name -> (git url, skill folder inside the repo, name under .claude/skills/)
26
27
  SKILL_SOURCES = {
@@ -52,26 +53,36 @@ class Options:
52
53
  import_auto_memory: bool = False
53
54
  import_cursor_skills: bool = False
54
55
  git_exclude: bool = False
56
+ tools: frozenset[str] = frozenset({"claude"})
57
+
58
+ def selected(self) -> list[str]:
59
+ return [t for t in TOOLS if t in self.tools]
55
60
 
56
61
  def wants(self, component: str) -> bool:
57
62
  return component not in self.without
58
63
 
59
64
 
65
+ def _write_agent(ctx: Ctx, opts: Options, name: str, claude_format: bytes) -> None:
66
+ for tool in opts.selected():
67
+ dest = ctx.project / VENDORS[tool].agents_dir / agent_filename(tool, name)
68
+ write_bytes(ctx, dest, convert_agent(tool, claude_format))
69
+
70
+
60
71
  def install_agents(ctx: Ctx, opts: Options) -> None:
61
72
  for agent in sorted((opts.assets / "agents").glob("*.md")):
62
73
  if agent.name == "code-grader.md" and not opts.wants("sdd"):
63
74
  continue
64
- write_bytes(ctx, ctx.project / ".claude" / "agents" / agent.name, agent.read_bytes())
75
+ _write_agent(ctx, opts, agent.stem, agent.read_bytes())
65
76
  write_bytes(
66
77
  ctx,
67
- ctx.project / ".claude" / "development-workflow.md",
78
+ ctx.project / workflow_path(opts.tools),
68
79
  (opts.assets / "development-workflow.md").read_bytes(),
69
80
  )
70
81
 
71
82
 
72
83
  def install_specialists(ctx: Ctx, opts: Options) -> None:
73
84
  for name in sorted(opts.with_coders):
74
- write_bytes(ctx, ctx.project / ".claude" / "agents" / f"{name}.md", render(SPECIALISTS[name], opts.assets))
85
+ _write_agent(ctx, opts, name, render(SPECIALISTS[name], opts.assets))
75
86
 
76
87
 
77
88
  def _fetch(url: str, into: Path) -> Path:
@@ -85,9 +96,10 @@ def install_skills(ctx: Ctx, opts: Options) -> None:
85
96
  wanted = [c for c in COMPONENTS if opts.wants(c)]
86
97
  if not wanted:
87
98
  return
88
- skills_dir = ctx.project / ".claude" / "skills"
89
- if skills_dir.is_symlink():
90
- ctx.log("note", skills_dir, f"is a symlink to {os.readlink(skills_dir)}: skills are written there")
99
+ skills_dirs = [ctx.project / VENDORS[t].skills_dir for t in opts.selected()]
100
+ for skills_dir in skills_dirs:
101
+ if skills_dir.is_symlink():
102
+ ctx.log("note", skills_dir, f"is a symlink to {os.readlink(skills_dir)}: skills are written there")
91
103
  with tempfile.TemporaryDirectory(prefix="agent-guild-") as tmp:
92
104
  for component in wanted:
93
105
  url, subpath, skill_name = SKILL_SOURCES[component]
@@ -96,24 +108,28 @@ def install_skills(ctx: Ctx, opts: Options) -> None:
96
108
  if not repo.is_dir():
97
109
  raise SystemExit(f"error: {repo} not found (expected a clone of {url})")
98
110
  elif ctx.dry_run:
99
- ctx.log("would fetch", url, f"-> .claude/skills/{skill_name}")
111
+ ctx.log("would fetch", url, "-> " + ", ".join(f"{d.relative_to(ctx.project).as_posix()}/{skill_name}" for d in skills_dirs))
100
112
  continue
101
113
  else:
102
114
  repo = _fetch(url, Path(tmp) / REPO_DIRS[component])
103
115
  src = repo / subpath
104
116
  if not (src / "SKILL.md").is_file():
105
117
  raise SystemExit(f"error: no SKILL.md in {src}")
106
- copy_tree(ctx, src, ctx.project / ".claude" / "skills" / skill_name)
118
+ for skills_dir in skills_dirs:
119
+ copy_tree(ctx, src, skills_dir / skill_name)
107
120
 
108
121
 
109
122
  def _rule_sections(opts: Options) -> list[tuple[str, str]]:
110
- """(heading that marks the section as present, body) for every CLAUDE.md section to add, in order."""
123
+ """(heading that marks the section as present, body) for every rules section to add, in order."""
111
124
  if opts.coders_only:
112
125
  return [(SPECIALISTS[n].agent, SPECIALISTS[n].rule_body) for n in sorted(opts.with_coders)]
113
126
  sections = []
114
127
  for heading, template, component in RULE_SECTIONS:
115
128
  if component is None or opts.wants(component):
116
129
  body = (opts.assets / "templates" / template).read_text(encoding="utf-8").strip()
130
+ names = [f"`{VENDORS[t].agents_dir}/`" for t in opts.selected()]
131
+ dirs = ", ".join(names[:-1]) + (" and " if len(names) > 1 else "") + names[-1]
132
+ body = body.replace("{{AGENTS_DIRS}}", dirs).replace("{{WORKFLOW}}", workflow_path(opts.tools))
117
133
  sections.append((heading, body))
118
134
  if heading == "Agents and delegation":
119
135
  sections += [(SPECIALISTS[n].agent, SPECIALISTS[n].rule_body) for n in sorted(opts.with_coders)]
@@ -121,33 +137,56 @@ def _rule_sections(opts: Options) -> list[tuple[str, str]]:
121
137
 
122
138
 
123
139
  def install_rules(ctx: Ctx, opts: Options) -> None:
124
- """The new sections live in CLAUDE.md (Claude-only rules); AGENTS.md stays tool-neutral and is just imported."""
140
+ """Add the rules sections to AGENTS.md (read by Codex, Cursor and others); CLAUDE.md only when it is the sole file.
141
+
142
+ Claude Code does not read AGENTS.md, so when it is a selected tool CLAUDE.md gets an `@AGENTS.md` import.
143
+ """
125
144
  agents_md, claude_md = ctx.project / "AGENTS.md", ctx.project / "CLAUDE.md"
126
- existed = claude_md.exists()
127
- text = claude_md.read_text(encoding="utf-8") if existed else ""
128
- known = text + (agents_md.read_text(encoding="utf-8") if agents_md.exists() else "")
145
+ target = claude_md if claude_md.exists() and not agents_md.exists() else agents_md
146
+ other = claude_md if target == agents_md else agents_md
147
+ existed = target.exists()
148
+ text = target.read_text(encoding="utf-8") if existed else ""
149
+ known = text + (other.read_text(encoding="utf-8") if other.exists() else "")
129
150
  added = []
130
- if agents_md.exists() and "@AGENTS.md" not in text:
131
- text = "@AGENTS.md\n" + (("\n" + text) if text.strip() else "")
132
- added.append("@AGENTS.md import")
133
151
  for heading, body in _rule_sections(opts):
134
152
  if re.search(rf"^#+ .*{re.escape(heading)}", known, re.MULTILINE):
135
- ctx.log("skipped", claude_md, f"section '{heading}' is already in CLAUDE.md or AGENTS.md")
153
+ ctx.log("skipped", target, f"section '{heading}' is already in AGENTS.md or CLAUDE.md")
136
154
  continue
137
155
  text = text.rstrip("\n") + ("\n\n" if text.strip() else "") + body + "\n"
138
156
  added.append(heading)
139
157
  if added:
140
158
  if not ctx.dry_run:
141
- claude_md.write_text(text, encoding="utf-8")
159
+ target.write_text(text, encoding="utf-8")
142
160
  verb = "edited" if existed else "created"
143
- (ctx.edited_existing.append(claude_md) if existed else ctx.track_new(claude_md))
144
- ctx.log(f"would {verb[:-1]}" if ctx.dry_run else verb, claude_md, "+ " + ", ".join(added))
161
+ (ctx.edited_existing.append(target) if existed else ctx.track_new(target))
162
+ ctx.log(f"would {verb[:-1]}" if ctx.dry_run else verb, target, "+ " + ", ".join(added))
163
+ if target == agents_md and "claude" in opts.tools:
164
+ _import_agents_md(ctx, claude_md)
165
+ if target == claude_md and opts.tools - {"claude"}:
166
+ ctx.warn("this project only has a CLAUDE.md, which Codex and Cursor don't read: rename it to AGENTS.md "
167
+ "(and keep a CLAUDE.md containing `@AGENTS.md`) so every tool gets the rules.")
168
+
169
+
170
+ def _import_agents_md(ctx: Ctx, claude_md: Path) -> None:
171
+ existed = claude_md.exists()
172
+ text = claude_md.read_text(encoding="utf-8") if existed else ""
173
+ if "@AGENTS.md" in text:
174
+ return
175
+ text = "@AGENTS.md\n" + (("\n" + text) if text.strip() else "")
176
+ if not ctx.dry_run:
177
+ claude_md.write_text(text, encoding="utf-8")
178
+ (ctx.edited_existing.append(claude_md) if existed else ctx.track_new(claude_md))
179
+ verb = "edit" if existed else "create"
180
+ ctx.log(f"would {verb}" if ctx.dry_run else ("edited" if existed else "created"), claude_md, "+ @AGENTS.md import")
145
181
 
146
182
 
147
183
  def import_cursor_skills(ctx: Ctx, opts: Options) -> None:
148
184
  """Copy the project's Cursor skills (.cursor/skills/*) into .claude/skills/. Existing skills are never replaced."""
149
185
  if not opts.import_cursor_skills:
150
186
  return
187
+ if "claude" not in opts.tools:
188
+ ctx.log("skipped", ".cursor/skills", "--import-cursor-skills copies into .claude/skills, and claude is not a selected tool")
189
+ return
151
190
  src_root = ctx.project / ".cursor" / "skills"
152
191
  if not src_root.is_dir():
153
192
  ctx.log("skipped", ".cursor/skills", "not found")
@@ -165,6 +204,12 @@ def _has_hook(groups: list, marker: str) -> bool:
165
204
 
166
205
 
167
206
  def install_settings(ctx: Ctx, opts: Options) -> None:
207
+ """Claude Code only: hooks, auto-memory switch and model are its settings."""
208
+ if "claude" not in opts.tools:
209
+ if opts.wants("jevmem"):
210
+ ctx.warn("jevmem's session hooks are only set up for Claude Code; in Cursor and Codex the agents call "
211
+ "memory_recall themselves.")
212
+ return
168
213
  shared_path = ctx.project / ".claude" / "settings.json"
169
214
  personal_path = ctx.project / ".claude" / "settings.local.json"
170
215
  scalar_path = personal_path if opts.personal_settings else shared_path
@@ -211,16 +256,10 @@ def install_mcp(ctx: Ctx, opts: Options) -> None:
211
256
  servers_to_add[spec.server_key] = spec.server
212
257
  if not servers_to_add:
213
258
  return
214
- path = ctx.project / ".mcp.json"
215
- data, indent = read_json(path)
216
- servers = data.setdefault("mcpServers", {})
217
- changes = []
218
- for key, server in servers_to_add.items():
219
- if key not in servers:
220
- servers[key] = server
221
- changes.append(f"{key} server")
259
+ for tool in opts.selected():
260
+ _write_mcp(ctx, tool, servers_to_add)
222
261
  user_config = Path.home() / ".claude.json"
223
- if user_config.exists():
262
+ if user_config.exists() and "claude" in opts.tools:
224
263
  try:
225
264
  registered = json.loads(user_config.read_text(encoding="utf-8")).get("mcpServers", {})
226
265
  for key in servers_to_add:
@@ -228,9 +267,43 @@ def install_mcp(ctx: Ctx, opts: Options) -> None:
228
267
  ctx.warn(f"{key} is also registered at user scope; keep one registration so the server isn't listed twice.")
229
268
  except (json.JSONDecodeError, AttributeError):
230
269
  pass
270
+
271
+
272
+ def _write_mcp(ctx: Ctx, tool: str, servers_to_add: dict) -> None:
273
+ path = ctx.project / VENDORS[tool].mcp_file
274
+ if tool == "codex":
275
+ _write_codex_mcp(ctx, path, servers_to_add)
276
+ return
277
+ data, indent = read_json(path)
278
+ servers = data.setdefault("mcpServers", {})
279
+ changes = []
280
+ for key, server in servers_to_add.items():
281
+ if key not in servers:
282
+ servers[key] = cursor_server(server) if tool == "cursor" else server
283
+ changes.append(f"{key} server")
231
284
  write_json(ctx, path, data, indent, changes)
232
285
 
233
286
 
287
+ def _write_codex_mcp(ctx: Ctx, path: Path, servers_to_add: dict) -> None:
288
+ """Append [mcp_servers.<key>] tables to .codex/config.toml; existing entries and the rest of the file are kept."""
289
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
290
+ try:
291
+ registered = tomllib.loads(text).get("mcp_servers", {})
292
+ except tomllib.TOMLDecodeError as exc:
293
+ raise SystemExit(f"error: {path} is not valid TOML ({exc}); fix it or move it away and re-run")
294
+ blocks = [codex_server_block(k, v) for k, v in servers_to_add.items() if k not in registered]
295
+ if not blocks:
296
+ ctx.log("unchanged", path)
297
+ return
298
+ existed = path.exists()
299
+ if not ctx.dry_run:
300
+ path.parent.mkdir(parents=True, exist_ok=True)
301
+ path.write_text(text.rstrip("\n") + ("\n\n" if text.strip() else "") + "\n".join(blocks), encoding="utf-8")
302
+ (ctx.edited_existing.append(path) if existed else ctx.track_new(path))
303
+ changes = "+ " + ", ".join(f"{k} server" for k in servers_to_add if k not in registered)
304
+ ctx.log(f"would {'edit' if existed else 'create'}" if ctx.dry_run else ("edited" if existed else "created"), path, changes)
305
+
306
+
234
307
  def encoded_memory_dir(project: Path) -> Path:
235
308
  """Claude Code names a project's auto-memory folder after its absolute path, every non-alphanumeric char -> '-'."""
236
309
  return Path.home() / ".claude" / "projects" / re.sub(r"[^A-Za-z0-9]", "-", str(project))
@@ -73,7 +73,7 @@ SPECIALISTS = {
73
73
  server={"type": "stdio", "command": "uvx", "args": ["--from", "codenav-mcp>=0.2.1,<0.3", "codenav-mcp"]},
74
74
  tools=PYTHON_TOOLS,
75
75
  prerequisite="coder-python: needs uv and Python 3.11+ (codenav-mcp runs through uvx). For a src layout, "
76
- "set CODENAV_MCP_SOURCE_ROOT in the codenav entry of .mcp.json.",
76
+ "set CODENAV_MCP_SOURCE_ROOT on the codenav MCP server.",
77
77
  fragment="python.md",
78
78
  ),
79
79
  Specialist(
@@ -0,0 +1,119 @@
1
+ """The coding tools the setup can be installed for, where each one keeps its files, and the format it expects.
2
+
3
+ Agents are authored in Claude Code's format (`agents/*.md`); the other tools get a converted copy:
4
+
5
+ - Cursor: Markdown + YAML frontmatter, but with `readonly` instead of `disallowedTools`, no `color`/`effort`, and
6
+ `model: inherit` (Cursor's model ids are not Claude's aliases). https://cursor.com/docs/context/subagents
7
+ - Codex: one TOML file per agent with `name`, `description` and `developer_instructions`; it has no read-only switch
8
+ for an agent, so that is stated in the instructions. https://learn.chatgpt.com/docs/agent-configuration/subagents
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ TOOLS = ("claude", "cursor", "codex")
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Vendor:
22
+ name: str
23
+ agents_dir: str
24
+ skills_dir: str
25
+ agent_suffix: str
26
+ mcp_file: str
27
+
28
+
29
+ VENDORS = {
30
+ "claude": Vendor("claude", ".claude/agents", ".claude/skills", ".md", ".mcp.json"),
31
+ "cursor": Vendor("cursor", ".cursor/agents", ".cursor/skills", ".md", ".cursor/mcp.json"),
32
+ "codex": Vendor("codex", ".codex/agents", ".codex/skills", ".toml", ".codex/config.toml"),
33
+ }
34
+
35
+
36
+ def detect_tools(project: Path) -> list[str]:
37
+ """Tools the project already uses, judged by their folders; Claude Code when nothing is found."""
38
+ signs = {
39
+ "claude": (".claude", "CLAUDE.md", ".mcp.json"),
40
+ "cursor": (".cursor",),
41
+ "codex": (".codex", ".agents"),
42
+ }
43
+ found = [t for t in TOOLS if any((project / s).exists() for s in signs[t])]
44
+ return found or ["claude"]
45
+
46
+
47
+ def parse_tools(value: str) -> list[str]:
48
+ names = [n.strip().lower() for n in value.split(",") if n.strip()]
49
+ unknown = [n for n in names if n not in TOOLS]
50
+ if unknown or not names:
51
+ raise SystemExit(f"error: --tools takes a comma-separated list of {', '.join(TOOLS)} (got {value!r})")
52
+ return [t for t in TOOLS if t in names]
53
+
54
+
55
+ def agent_filename(tool: str, agent: str) -> str:
56
+ return agent + VENDORS[tool].agent_suffix
57
+
58
+
59
+ def workflow_path(tools: list[str] | frozenset[str]) -> str:
60
+ """One shared copy of the workflow doc, inside the first selected tool's folder."""
61
+ first = next(t for t in TOOLS if t in tools)
62
+ return f"{Path(VENDORS[first].agents_dir).parent.as_posix()}/development-workflow.md"
63
+
64
+
65
+ def _split(source: str) -> tuple[dict[str, str], str]:
66
+ _, frontmatter, body = source.replace("\r\n", "\n").split("---\n", 2)
67
+ fields = {}
68
+ for line in frontmatter.splitlines():
69
+ key, _, value = line.partition(":")
70
+ fields[key.strip()] = value.strip()
71
+ return fields, body
72
+
73
+
74
+ def _is_read_only(fields: dict[str, str]) -> bool:
75
+ return "Edit" in {t.strip() for t in fields.get("disallowedTools", "").split(",")}
76
+
77
+
78
+ def convert_agent(tool: str, source: bytes) -> bytes:
79
+ """Turn a Claude Code agent file into `tool`'s format."""
80
+ if tool == "claude":
81
+ return source
82
+ fields, body = _split(source.decode("utf-8"))
83
+ read_only = _is_read_only(fields)
84
+ if tool == "cursor":
85
+ head = [f"name: {fields['name']}", f"description: {json.dumps(fields['description'], ensure_ascii=False)}",
86
+ "model: inherit"]
87
+ if read_only:
88
+ head.append("readonly: true")
89
+ return ("---\n" + "\n".join(head) + "\n---\n" + body).encode("utf-8")
90
+ if tool == "codex":
91
+ instructions = body.strip("\n")
92
+ if read_only:
93
+ instructions = "You are read-only: never edit, create or delete files.\n\n" + instructions
94
+ escaped = instructions.replace("\\", "\\\\").replace('"""', '\\"\\"\\"')
95
+ lines = [f"name = {json.dumps(fields['name'])}",
96
+ f"description = {json.dumps(fields['description'], ensure_ascii=False)}"]
97
+ if fields.get("effort") in ("low", "medium", "high", "xhigh", "max"):
98
+ lines.append(f'model_reasoning_effort = "{"high" if fields["effort"] in ("xhigh", "max") else fields["effort"]}"')
99
+ lines.append(f'developer_instructions = """\n{escaped}\n"""')
100
+ return ("\n".join(lines) + "\n").encode("utf-8")
101
+ raise ValueError(tool)
102
+
103
+
104
+ def cursor_server(server: dict) -> dict:
105
+ return {k: v for k, v in server.items() if k != "type"}
106
+
107
+
108
+ def _toml_value(value) -> str:
109
+ if isinstance(value, dict):
110
+ return "{ " + ", ".join(f"{json.dumps(k)} = {_toml_value(v)}" for k, v in value.items()) + " }"
111
+ if isinstance(value, list):
112
+ return "[" + ", ".join(_toml_value(v) for v in value) + "]"
113
+ return json.dumps(value, ensure_ascii=False)
114
+
115
+
116
+ def codex_server_block(key: str, server: dict) -> str:
117
+ lines = [f"[mcp_servers.{key}]"]
118
+ lines += [f"{k} = {_toml_value(v)}" for k, v in server.items() if k != "type"]
119
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,7 @@
1
+ ## Agents and delegation
2
+
3
+ You are the orchestrator: delegate to the `architect`, `coder`, `code-grader` and `utility` subagents in
4
+ {{AGENTS_DIRS}}, and own every approval gate and memory write. Routing, briefs and the memory protocol (read this file
5
+ before delegating):
6
+
7
+ @{{WORKFLOW}}
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
  import pytest
5
5
 
6
6
  from agent_guild.cli import main
7
+ from agent_guild.vendors import convert_agent
7
8
 
8
9
  REPOS = {"memory-bank": "skills/memory-bank", "jev-mem": ".claude/skills/jev-memory",
9
10
  "sdd-feature-skill": "skills/sdd-feature"}
@@ -40,16 +41,17 @@ def test_given_empty_project_when_installed_then_everything_is_copied(tmp_path,
40
41
  assert (project / ".claude/development-workflow.md").is_file()
41
42
  for skill in ("memory-bank", "jev-memory", "sdd-feature"):
42
43
  assert (project / ".claude/skills" / skill / "SKILL.md").is_file()
43
- claude_md = (project / "CLAUDE.md").read_text()
44
- assert "@.claude/development-workflow.md" in claude_md
45
- assert "## Memory bank" in claude_md and "Phase Gate Protocol" in claude_md
44
+ assert (project / "CLAUDE.md").read_text() == "@AGENTS.md\n"
45
+ agents_md = (project / "AGENTS.md").read_text()
46
+ assert "@.claude/development-workflow.md" in agents_md
47
+ assert "## Memory bank" in agents_md and "Phase Gate Protocol" in agents_md
46
48
  settings = json.loads((project / ".claude/settings.json").read_text())
47
49
  assert settings["model"] == "sonnet" and settings["autoMemoryEnabled"] is False
48
50
  assert set(settings["hooks"]) == {"SessionStart", "UserPromptSubmit"}
49
51
  assert "jevmem" in json.loads((project / ".mcp.json").read_text())["mcpServers"]
50
52
 
51
53
 
52
- def test_given_agents_md_only_when_installed_then_claude_md_imports_it_and_holds_the_sections(tmp_path, sources):
54
+ def test_given_agents_md_only_when_installed_then_it_holds_the_sections_and_claude_md_imports_it(tmp_path, sources):
53
55
  # given
54
56
  project = tmp_path / "p"
55
57
  project.mkdir()
@@ -57,9 +59,34 @@ def test_given_agents_md_only_when_installed_then_claude_md_imports_it_and_holds
57
59
  # when
58
60
  run(project, sources)
59
61
  # then
60
- assert (project / "CLAUDE.md").read_text().startswith("@AGENTS.md\n")
62
+ assert (project / "CLAUDE.md").read_text() == "@AGENTS.md\n"
63
+ assert (project / "AGENTS.md").read_text().startswith("# Rules\n")
64
+ assert "## Agents and delegation" in (project / "AGENTS.md").read_text()
65
+
66
+
67
+ def test_given_claude_md_only_when_installed_then_the_sections_go_there(tmp_path, sources):
68
+ # given
69
+ project = tmp_path / "p"
70
+ project.mkdir()
71
+ (project / "CLAUDE.md").write_text("# Mine\n")
72
+ # when
73
+ run(project, sources)
74
+ # then
75
+ assert not (project / "AGENTS.md").exists()
61
76
  assert "## Agents and delegation" in (project / "CLAUDE.md").read_text()
62
- assert (project / "AGENTS.md").read_text() == "# Rules\n"
77
+
78
+
79
+ def test_given_both_files_when_installed_then_agents_md_gets_the_sections_and_claude_md_keeps_its_text(tmp_path, sources):
80
+ # given
81
+ project = tmp_path / "p"
82
+ project.mkdir()
83
+ (project / "AGENTS.md").write_text("# Rules\n")
84
+ (project / "CLAUDE.md").write_text("# Claude only\n")
85
+ # when
86
+ run(project, sources)
87
+ # then
88
+ assert "## Agents and delegation" in (project / "AGENTS.md").read_text()
89
+ assert (project / "CLAUDE.md").read_text() == "@AGENTS.md\n\n# Claude only\n"
63
90
 
64
91
 
65
92
  def test_given_installed_project_when_run_again_then_nothing_changes(tmp_path, sources):
@@ -100,7 +127,7 @@ def test_given_without_sdd_and_jevmem_then_those_parts_are_left_out(tmp_path, so
100
127
  assert not (project / ".claude/skills/jev-memory").exists()
101
128
  assert not (project / ".mcp.json").exists()
102
129
  assert json.loads((project / ".claude/settings.json").read_text()) == {"model": "sonnet"}
103
- assert "Phase Gate" not in (project / "CLAUDE.md").read_text()
130
+ assert "Phase Gate" not in (project / "AGENTS.md").read_text()
104
131
 
105
132
 
106
133
  def test_given_existing_settings_when_installed_then_they_are_merged_with_indent_kept(tmp_path, sources):
@@ -150,7 +177,7 @@ def test_given_model_none_then_model_is_not_set(tmp_path, sources):
150
177
  assert "model" not in json.loads((project / ".claude/settings.json").read_text())
151
178
 
152
179
 
153
- def test_given_sections_already_in_agents_md_then_claude_md_does_not_repeat_them(tmp_path, sources):
180
+ def test_given_sections_already_in_agents_md_then_they_are_not_repeated(tmp_path, sources):
154
181
  # given
155
182
  project = tmp_path / "p"
156
183
  project.mkdir()
@@ -158,9 +185,9 @@ def test_given_sections_already_in_agents_md_then_claude_md_does_not_repeat_them
158
185
  # when
159
186
  run(project, sources)
160
187
  # then
161
- text = (project / "CLAUDE.md").read_text()
188
+ text = (project / "AGENTS.md").read_text()
162
189
  assert "## Agents and delegation" in text
163
- assert "## Memory bank" not in text and "Phase Gate" not in text
190
+ assert text.count("## Memory bank") == 1 and text.count("Phase Gate") == 1
164
191
 
165
192
 
166
193
  def test_given_cursor_skills_when_importing_then_copied_without_replacing_existing(tmp_path, sources):
@@ -171,7 +198,7 @@ def test_given_cursor_skills_when_importing_then_copied_without_replacing_existi
171
198
  (project / ".cursor/skills/sdd-feature").mkdir()
172
199
  (project / ".cursor/skills/sdd-feature/SKILL.md").write_text("cursor version")
173
200
  # when
174
- run(project, sources, "--import-cursor-skills")
201
+ run(project, sources, "--tools", "claude,cursor", "--import-cursor-skills")
175
202
  # then
176
203
  assert (project / ".claude/skills/mine/SKILL.md").read_text() == "mine"
177
204
  assert (project / ".claude/skills/sdd-feature/SKILL.md").read_text() != "cursor version"
@@ -222,7 +249,7 @@ def test_given_no_with_flag_then_no_language_coder_or_navigation_server_is_insta
222
249
  # then
223
250
  assert sorted(p.name for p in (project / ".claude/agents").glob("coder-*.md")) == []
224
251
  assert set(json.loads((project / ".mcp.json").read_text())["mcpServers"]) == {"jevmem"}
225
- assert "coder-python" not in (project / "CLAUDE.md").read_text()
252
+ assert "coder-python" not in (project / "AGENTS.md").read_text()
226
253
 
227
254
 
228
255
  @pytest.mark.parametrize("agent, server_key, tool", [
@@ -246,9 +273,9 @@ def test_given_with_coder_then_agent_server_and_routing_rule_are_installed(tmp_p
246
273
  assert "model: sonnet" in frontmatter
247
274
  assert "## Rules" in body and "navigation (" in body
248
275
  assert server_key in json.loads((project / ".mcp.json").read_text())["mcpServers"]
249
- claude_md = (project / "CLAUDE.md").read_text()
250
- assert f"`{agent}`" in claude_md
251
- assert claude_md.index(f"`{agent}`") < claude_md.index("## Memory bank")
276
+ agents_md = (project / "AGENTS.md").read_text()
277
+ assert f"`{agent}`" in agents_md
278
+ assert agents_md.index(f"`{agent}`") < agents_md.index("## Memory bank")
252
279
 
253
280
 
254
281
  def test_given_with_coder_when_run_again_then_nothing_changes_and_rule_is_not_repeated(tmp_path, sources):
@@ -261,7 +288,7 @@ def test_given_with_coder_when_run_again_then_nothing_changes_and_rule_is_not_re
261
288
  run(project, sources, "--with", "coder-python", "--with", "coder-swift")
262
289
  # then
263
290
  assert {p: p.read_bytes() for p in project.rglob("*") if p.is_file()} == before
264
- assert (project / "CLAUDE.md").read_text().count("## Python code goes to") == 1
291
+ assert (project / "AGENTS.md").read_text().count("## Python code goes to") == 1
265
292
 
266
293
 
267
294
  def test_given_existing_server_entry_when_with_coder_then_it_is_kept(tmp_path, sources):
@@ -388,7 +415,7 @@ def test_given_sections_already_in_agents_md_then_the_log_says_skipped_not_uncha
388
415
  run(project, sources)
389
416
  # then
390
417
  out = capsys.readouterr().out
391
- assert "skipped CLAUDE.md section 'Phase Gate Protocol' is already in CLAUDE.md or AGENTS.md" in out
418
+ assert "skipped AGENTS.md section 'Phase Gate Protocol' is already in AGENTS.md or CLAUDE.md" in out
392
419
 
393
420
 
394
421
  def test_given_symlinked_skills_dir_then_the_log_notes_where_skills_are_written(tmp_path, sources, capsys):
@@ -405,3 +432,122 @@ def test_given_symlinked_skills_dir_then_the_log_notes_where_skills_are_written(
405
432
  # then
406
433
  assert "is a symlink to" in capsys.readouterr().out
407
434
  assert (project / ".cursor/skills/sdd-feature/SKILL.md").is_file()
435
+
436
+
437
+ def _tree(project):
438
+ return sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file())
439
+
440
+
441
+ def test_given_cursor_folder_when_installed_then_cursor_gets_its_own_format_and_no_claude_files(tmp_path, sources):
442
+ # given
443
+ project = tmp_path / "p"
444
+ (project / ".cursor").mkdir(parents=True)
445
+ # when
446
+ run(project, sources)
447
+ # then
448
+ files = _tree(project)
449
+ assert not any(f.startswith(".claude/") or f in ("CLAUDE.md", ".mcp.json") for f in files)
450
+ architect = (project / ".cursor/agents/architect.md").read_text()
451
+ assert 'readonly: true' in architect and "model: inherit" in architect
452
+ assert "disallowedTools" not in architect and "color:" not in architect and "effort:" not in architect
453
+ assert "readonly" not in (project / ".cursor/agents/coder.md").read_text()
454
+ for skill in ("memory-bank", "jev-memory", "sdd-feature"):
455
+ assert (project / ".cursor/skills" / skill / "SKILL.md").is_file()
456
+ assert (project / ".cursor/development-workflow.md").is_file()
457
+ assert "@.cursor/development-workflow.md" in (project / "AGENTS.md").read_text()
458
+ assert "jevmem" in json.loads((project / ".cursor/mcp.json").read_text())["mcpServers"]
459
+ assert "type" not in json.loads((project / ".cursor/mcp.json").read_text())["mcpServers"]["jevmem"]
460
+
461
+
462
+ def test_given_codex_folder_when_installed_then_agents_are_toml_and_skills_go_to_codex_skills(tmp_path, sources, capsys):
463
+ # given
464
+ import tomllib
465
+ project = tmp_path / "p"
466
+ (project / ".codex").mkdir(parents=True)
467
+ # when
468
+ run(project, sources, "--with", "coder-python")
469
+ # then
470
+ for name in ("architect", "coder", "code-grader", "utility", "coder-python"):
471
+ agent = tomllib.loads((project / ".codex/agents" / f"{name}.toml").read_text())
472
+ assert agent["name"] == name and agent["description"] and agent["developer_instructions"].strip()
473
+ architect = tomllib.loads((project / ".codex/agents/architect.toml").read_text())
474
+ assert architect["model_reasoning_effort"] == "high"
475
+ assert architect["developer_instructions"].startswith("You are read-only")
476
+ assert not tomllib.loads((project / ".codex/agents/coder.toml").read_text())["developer_instructions"].startswith("You are read-only")
477
+ assert (project / ".codex/skills/sdd-feature/SKILL.md").is_file()
478
+ assert not (project / ".agents").exists()
479
+ config = tomllib.loads((project / ".codex/config.toml").read_text())
480
+ assert config["mcp_servers"]["jevmem"]["command"] == "uvx"
481
+ assert config["mcp_servers"]["codenav"]["args"][0] == "--from"
482
+ assert not (project / ".claude").exists()
483
+ assert "jevmem's session hooks are only set up for Claude Code" in capsys.readouterr().out
484
+
485
+
486
+ def test_given_several_tools_detected_then_all_of_them_are_installed_and_share_one_workflow_doc(tmp_path, sources, capsys):
487
+ # given
488
+ project = tmp_path / "p"
489
+ for folder in (".claude", ".cursor", ".codex"):
490
+ (project / folder).mkdir(parents=True)
491
+ # when
492
+ run(project, sources)
493
+ # then
494
+ files = _tree(project)
495
+ assert ".claude/agents/coder.md" in files and ".cursor/agents/coder.md" in files and ".codex/agents/coder.toml" in files
496
+ assert [f for f in files if f.endswith("development-workflow.md")] == [".claude/development-workflow.md"]
497
+ assert "tools: claude, cursor, codex (detected)" in capsys.readouterr().out
498
+ rules = (project / "AGENTS.md").read_text()
499
+ assert "`.claude/agents/`, `.cursor/agents/` and `.codex/agents/`" in rules
500
+
501
+
502
+ def test_given_tools_flag_then_it_overrides_detection(tmp_path, sources):
503
+ # given
504
+ project = tmp_path / "p"
505
+ (project / ".cursor").mkdir(parents=True)
506
+ # when
507
+ run(project, sources, "--tools", "codex")
508
+ # then
509
+ assert (project / ".codex/agents/coder.toml").is_file()
510
+ assert not (project / ".cursor/agents").exists() and not (project / ".claude").exists()
511
+
512
+
513
+ def test_given_unknown_tool_then_it_is_refused(tmp_path, sources):
514
+ # given
515
+ project = tmp_path / "p"
516
+ project.mkdir()
517
+ # when / then
518
+ with pytest.raises(SystemExit):
519
+ run(project, sources, "--tools", "emacs")
520
+
521
+
522
+ def test_given_codex_config_with_other_settings_then_servers_are_appended_and_run_again_changes_nothing(tmp_path, sources):
523
+ # given
524
+ project = tmp_path / "p"
525
+ (project / ".codex").mkdir(parents=True)
526
+ (project / ".codex/config.toml").write_text('model = "x"\n\n[mcp_servers.jevmem]\ncommand = "mine"\n')
527
+ # when
528
+ run(project, sources, "--with", "coder-swift")
529
+ run(project, sources, "--with", "coder-swift")
530
+ # then
531
+ import tomllib
532
+ config = tomllib.loads((project / ".codex/config.toml").read_text())
533
+ assert config["model"] == "x" and config["mcp_servers"]["jevmem"]["command"] == "mine"
534
+ assert config["mcp_servers"]["codenav-swift"]["env"] == {"CODENAV_SWIFT_WRITE": "1"}
535
+ assert (project / ".codex/config.toml").read_text().count("[mcp_servers.codenav-swift]") == 1
536
+
537
+
538
+ def test_given_claude_md_only_and_codex_then_a_warning_says_codex_will_not_read_it(tmp_path, sources, capsys):
539
+ # given
540
+ project = tmp_path / "p"
541
+ (project / ".codex").mkdir(parents=True)
542
+ (project / "CLAUDE.md").write_text("# Mine\n")
543
+ # when
544
+ run(project, sources)
545
+ # then
546
+ assert "only has a CLAUDE.md" in capsys.readouterr().out
547
+
548
+
549
+ def test_given_crlf_agent_source_when_converting_then_same_result_as_lf():
550
+ # Windows checkouts may turn the shipped agent files into CRLF
551
+ lf = b'---\nname: x\ndescription: d\ndisallowedTools: Edit, Write\n---\nBody\n'
552
+ for tool in ("cursor", "codex"):
553
+ assert convert_agent(tool, lf.replace(b"\n", b"\r\n")) == convert_agent(tool, lf)
@@ -4,7 +4,7 @@ requires-python = ">=3.11"
4
4
 
5
5
  [[package]]
6
6
  name = "agent-guild"
7
- version = "1.2.1"
7
+ version = "1.3.0"
8
8
  source = { editable = "." }
9
9
 
10
10
  [package.dev-dependencies]
@@ -1,6 +0,0 @@
1
- ## Agents and delegation
2
-
3
- You are the orchestrator: delegate to the `architect`, `coder`, `code-grader` and `utility` subagents in
4
- `.claude/agents/`, and own every approval gate and memory write. Routing, briefs and the memory protocol:
5
-
6
- @.claude/development-workflow.md
File without changes
File without changes
File without changes