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.
- {agent_guild-1.2.1 → agent_guild-1.3.0}/CHANGELOG.md +10 -0
- agent_guild-1.2.1/README.md → agent_guild-1.3.0/PKG-INFO +53 -20
- agent_guild-1.2.1/PKG-INFO → agent_guild-1.3.0/README.md +34 -39
- {agent_guild-1.2.1 → agent_guild-1.3.0}/pyproject.toml +3 -3
- {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/cli.py +14 -6
- {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/installer.py +103 -30
- {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/specialists.py +1 -1
- agent_guild-1.3.0/src/agent_guild/vendors.py +119 -0
- agent_guild-1.3.0/templates/agents-rules.md +7 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/tests/test_cli.py +163 -17
- {agent_guild-1.2.1 → agent_guild-1.3.0}/uv.lock +1 -1
- agent_guild-1.2.1/templates/agents-rules.md +0 -6
- {agent_guild-1.2.1 → agent_guild-1.3.0}/.github/workflows/ci.yml +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/.github/workflows/release.yml +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/.gitignore +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/LICENSE +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/agents/architect.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/agents/code-grader.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/agents/coder.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/agents/utility.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/development-workflow.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/docs/manual_installation.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/scripts/release_notes.py +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/__init__.py +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/src/agent_guild/files.py +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/coders/python.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/coders/swift.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/coders/typescript.md +0 -0
- {agent_guild-1.2.1 → agent_guild-1.3.0}/templates/memory-bank-rules.md +0 -0
- {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
|
[](https://pypi.org/project/agent-guild/)
|
|
4
23
|
|
|
5
|
-
A ready-made **multi-agent setup for [Claude Code](https://claude.com/claude-code)
|
|
6
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`AGENTS.md`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
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
|
-
| `--
|
|
80
|
-
| `--
|
|
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
|
|
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
|
|
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.
|
|
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
|
[](https://pypi.org/project/agent-guild/)
|
|
23
4
|
|
|
24
|
-
A ready-made **multi-agent setup for [Claude Code](https://claude.com/claude-code)
|
|
25
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`AGENTS.md`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
-
|
|
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
|
-
| `--
|
|
99
|
-
| `--
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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 /
|
|
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
|
|
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
|
|
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
|
-
|
|
75
|
+
_write_agent(ctx, opts, agent.stem, agent.read_bytes())
|
|
65
76
|
write_bytes(
|
|
66
77
|
ctx,
|
|
67
|
-
ctx.project /
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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",
|
|
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
|
-
|
|
159
|
+
target.write_text(text, encoding="utf-8")
|
|
142
160
|
verb = "edited" if existed else "created"
|
|
143
|
-
(ctx.edited_existing.append(
|
|
144
|
-
ctx.log(f"would {verb[:-1]}" if ctx.dry_run else verb,
|
|
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
|
-
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
assert "
|
|
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
|
|
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()
|
|
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
|
-
|
|
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 / "
|
|
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
|
|
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 / "
|
|
188
|
+
text = (project / "AGENTS.md").read_text()
|
|
162
189
|
assert "## Agents and delegation" in text
|
|
163
|
-
assert "## Memory bank"
|
|
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 / "
|
|
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
|
-
|
|
250
|
-
assert f"`{agent}`" in
|
|
251
|
-
assert
|
|
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 / "
|
|
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
|
|
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)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|