agent-guild 1.2.0__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.0 → agent_guild-1.3.0}/CHANGELOG.md +17 -0
- agent_guild-1.2.0/README.md → agent_guild-1.3.0/PKG-INFO +70 -19
- agent_guild-1.2.0/PKG-INFO → agent_guild-1.3.0/README.md +51 -38
- {agent_guild-1.2.0 → agent_guild-1.3.0}/pyproject.toml +3 -3
- {agent_guild-1.2.0 → agent_guild-1.3.0}/src/agent_guild/cli.py +19 -6
- {agent_guild-1.2.0 → agent_guild-1.3.0}/src/agent_guild/installer.py +103 -27
- {agent_guild-1.2.0 → 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.0 → agent_guild-1.3.0}/tests/test_cli.py +203 -16
- {agent_guild-1.2.0 → agent_guild-1.3.0}/uv.lock +1 -1
- agent_guild-1.2.0/templates/agents-rules.md +0 -6
- {agent_guild-1.2.0 → agent_guild-1.3.0}/.github/workflows/ci.yml +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/.github/workflows/release.yml +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/.gitignore +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/LICENSE +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/agents/architect.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/agents/code-grader.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/agents/coder.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/agents/utility.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/development-workflow.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/docs/manual_installation.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/scripts/release_notes.py +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/src/agent_guild/__init__.py +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/src/agent_guild/files.py +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/templates/coders/python.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/templates/coders/swift.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/templates/coders/typescript.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/templates/memory-bank-rules.md +0 -0
- {agent_guild-1.2.0 → agent_guild-1.3.0}/templates/sdd-rules.md +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
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
|
+
|
|
15
|
+
## [1.2.1] - 2026-10-22
|
|
16
|
+
|
|
17
|
+
- The install log says `skipped ... section '<name>' is already in CLAUDE.md or AGENTS.md` instead of the confusing
|
|
18
|
+
`unchanged`, notes when `.claude/skills` is a symlink and where skills are written, and ends with a hint to merge
|
|
19
|
+
skipped files that differ by hand.
|
|
20
|
+
- README: a section on adding agent-guild to a project that already has its own agents, skills and rules.
|
|
21
|
+
|
|
5
22
|
## [1.2.0] - 2026-10-22
|
|
6
23
|
|
|
7
24
|
- Follow the updated sdd-feature skill: Phase 0 has a UI track (wireframe) and a system track (`DESIGN.md` with Mermaid
|
|
@@ -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 |
|
|
@@ -104,6 +137,24 @@ uv run agent-guild /path/to/project --dry-run
|
|
|
104
137
|
From a checkout, the CLI reads `agents/`, `templates/` and `development-workflow.md` at the repo root; the published
|
|
105
138
|
wheel bundles them.
|
|
106
139
|
|
|
140
|
+
## Adding it to a project that already has its own setup
|
|
141
|
+
|
|
142
|
+
The installer never overwrites a file that differs from the shipped one (and skips rules sections that `AGENTS.md`
|
|
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:
|
|
144
|
+
|
|
145
|
+
- **Skipped files are yours.** Anything the log marks `skipped ... differs` keeps your version. If it is an older or
|
|
146
|
+
customised copy of a shipped one (the `sdd-feature` skill, `code-grader`, `jev-memory`), diff it against the shipped
|
|
147
|
+
file and merge what you want by hand. `--force` replaces it instead.
|
|
148
|
+
- **New files can land next to yours.** A skill folder you already have keeps its `SKILL.md`, but the shipped files
|
|
149
|
+
that are missing (templates, scripts) are added beside it. If `.claude/skills` is a symlink to `.cursor/skills`, the
|
|
150
|
+
log says so, and the skills are written there.
|
|
151
|
+
- **Cursor reads `.claude/agents/` too.** The agents installed here work in Cursor without copies in `.cursor/agents/`
|
|
152
|
+
(`.cursor/agents/` wins on a name clash). Cursor ignores `tools`, `disallowedTools` and `effort`, so the read-only
|
|
153
|
+
agents are not read-only there. If your project has a test that requires every `.claude/agents/*.md` to have a
|
|
154
|
+
`.cursor/agents/` twin, relax it to require the twin only for the Cursor agents.
|
|
155
|
+
- **Skip what you already have** with `--without memory-bank` / `jevmem` / `sdd`, for example when your own memory-bank
|
|
156
|
+
skill has a different name.
|
|
157
|
+
|
|
107
158
|
## Design choices
|
|
108
159
|
|
|
109
160
|
- **The orchestrator is the only writer of memory.** Parallel coders writing `memory/` would conflict, and someone has
|
|
@@ -120,7 +171,7 @@ wheel bundles them.
|
|
|
120
171
|
navigation MCPs; in practice it still reached for `grep` and `sed`, because the MCP tools arrive deferred (name
|
|
121
172
|
only) and the brief or its own habits point elsewhere. `coder-python`, `coder-swift` and `coder-typescript`
|
|
122
173
|
(`--with`) are generated from `coder.md` plus a navigation section that names the MCP's tools, and list those tools
|
|
123
|
-
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
|
|
124
175
|
changes.
|
|
125
176
|
- **The grader runs on Opus.** The code it grades was written by Sonnet coders, so a different, stronger model gives
|
|
126
177
|
a more independent review.
|
|
@@ -1,28 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: agent-guild
|
|
3
|
-
Version: 1.2.0
|
|
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 |
|
|
@@ -123,6 +118,24 @@ uv run agent-guild /path/to/project --dry-run
|
|
|
123
118
|
From a checkout, the CLI reads `agents/`, `templates/` and `development-workflow.md` at the repo root; the published
|
|
124
119
|
wheel bundles them.
|
|
125
120
|
|
|
121
|
+
## Adding it to a project that already has its own setup
|
|
122
|
+
|
|
123
|
+
The installer never overwrites a file that differs from the shipped one (and skips rules sections that `AGENTS.md`
|
|
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:
|
|
125
|
+
|
|
126
|
+
- **Skipped files are yours.** Anything the log marks `skipped ... differs` keeps your version. If it is an older or
|
|
127
|
+
customised copy of a shipped one (the `sdd-feature` skill, `code-grader`, `jev-memory`), diff it against the shipped
|
|
128
|
+
file and merge what you want by hand. `--force` replaces it instead.
|
|
129
|
+
- **New files can land next to yours.** A skill folder you already have keeps its `SKILL.md`, but the shipped files
|
|
130
|
+
that are missing (templates, scripts) are added beside it. If `.claude/skills` is a symlink to `.cursor/skills`, the
|
|
131
|
+
log says so, and the skills are written there.
|
|
132
|
+
- **Cursor reads `.claude/agents/` too.** The agents installed here work in Cursor without copies in `.cursor/agents/`
|
|
133
|
+
(`.cursor/agents/` wins on a name clash). Cursor ignores `tools`, `disallowedTools` and `effort`, so the read-only
|
|
134
|
+
agents are not read-only there. If your project has a test that requires every `.claude/agents/*.md` to have a
|
|
135
|
+
`.cursor/agents/` twin, relax it to require the twin only for the Cursor agents.
|
|
136
|
+
- **Skip what you already have** with `--without memory-bank` / `jevmem` / `sdd`, for example when your own memory-bank
|
|
137
|
+
skill has a different name.
|
|
138
|
+
|
|
126
139
|
## Design choices
|
|
127
140
|
|
|
128
141
|
- **The orchestrator is the only writer of memory.** Parallel coders writing `memory/` would conflict, and someone has
|
|
@@ -139,7 +152,7 @@ wheel bundles them.
|
|
|
139
152
|
navigation MCPs; in practice it still reached for `grep` and `sed`, because the MCP tools arrive deferred (name
|
|
140
153
|
only) and the brief or its own habits point elsewhere. `coder-python`, `coder-swift` and `coder-typescript`
|
|
141
154
|
(`--with`) are generated from `coder.md` plus a navigation section that names the MCP's tools, and list those tools
|
|
142
|
-
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
|
|
143
156
|
changes.
|
|
144
157
|
- **The grader runs on Opus.** The code it grades was written by Sonnet coders, so a different, stronger model gives
|
|
145
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,32 +6,36 @@ 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
|
|
12
13
|
|
|
14
|
+
KEPT_HINT = """Files marked "skipped ... differs" keep your version. If a skill or agent of yours is an older or customised
|
|
15
|
+
copy of a shipped one, diff it against the shipped file and merge the changes by hand (--force replaces it)."""
|
|
16
|
+
|
|
13
17
|
NEXT_STEPS = """Next steps:
|
|
14
|
-
- 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.
|
|
15
19
|
- Per machine, once: `uvx jevmem config init` and `uvx --from jevmem jevmem warmup`.
|
|
16
20
|
- With memory-bank: ask Claude to "set up the memory bank".
|
|
17
|
-
- 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."""
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
CODERS_NEXT_STEPS = """Next steps:
|
|
21
25
|
- Review the diff (git status / git diff).
|
|
22
|
-
- Restart
|
|
26
|
+
- Restart your coding tool in the project so the agents and MCP server load."""
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
def suggest_coders(project: Path, chosen: list[str]) -> None:
|
|
26
30
|
"""After an install, point at the language coders that fit the project and are not installed yet."""
|
|
27
31
|
missing = [
|
|
28
32
|
name for name in detect_languages(project)
|
|
29
|
-
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())
|
|
30
34
|
]
|
|
31
35
|
if not missing:
|
|
32
36
|
return
|
|
33
37
|
print("\nThis project looks like it uses code that a language coder can navigate and refactor with a "
|
|
34
|
-
"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):")
|
|
35
39
|
for name in missing:
|
|
36
40
|
spec = SPECIALISTS[name]
|
|
37
41
|
print(f" - {spec.label}: uvx agent-guild {project} --with {name} --coders-only")
|
|
@@ -45,13 +49,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
45
49
|
"unless --force.",
|
|
46
50
|
)
|
|
47
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")
|
|
48
56
|
p.add_argument("--without", action="append", default=[], choices=COMPONENTS, metavar="COMPONENT",
|
|
49
57
|
help=f"leave a component out (repeatable): {', '.join(COMPONENTS)}. "
|
|
50
58
|
"'sdd' also drops the code-grader agent; 'jevmem' drops skill, MCP server, hooks and the auto-memory switch")
|
|
51
59
|
p.add_argument("--with", dest="with_coders", action="append", default=[], choices=sorted(SPECIALISTS),
|
|
52
60
|
metavar="CODER",
|
|
53
61
|
help=f"add a language coder (repeatable): {', '.join(sorted(SPECIALISTS))}. Each is the generic coder plus "
|
|
54
|
-
"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 "
|
|
55
63
|
"are added too. Off by default")
|
|
56
64
|
p.add_argument("--coders-only", action="store_true",
|
|
57
65
|
help="with --with: install only the language coders (agent, MCP server, routing rule) and leave "
|
|
@@ -86,6 +94,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
86
94
|
raise SystemExit(f"error: {project} is not a directory")
|
|
87
95
|
if not (args.assets / "agents").is_dir():
|
|
88
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)
|
|
89
98
|
ctx = Ctx(project=project, dry_run=args.dry_run, force=args.force)
|
|
90
99
|
opts = Options(
|
|
91
100
|
assets=args.assets,
|
|
@@ -99,12 +108,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
99
108
|
import_auto_memory=args.import_auto_memory,
|
|
100
109
|
import_cursor_skills=args.import_cursor_skills,
|
|
101
110
|
git_exclude=args.git_exclude,
|
|
111
|
+
tools=frozenset(tools),
|
|
102
112
|
)
|
|
103
113
|
run_all(ctx, opts)
|
|
104
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'})")
|
|
105
116
|
print("\n".join(ctx.lines))
|
|
106
117
|
for w in ctx.warnings:
|
|
107
118
|
print(f"warning: {w}")
|
|
119
|
+
if any(line.lstrip().startswith("skipped") and "differs" in line for line in ctx.lines):
|
|
120
|
+
print(f"\n{KEPT_HINT}")
|
|
108
121
|
if not args.dry_run:
|
|
109
122
|
print(CODERS_NEXT_STEPS if args.coders_only else NEXT_STEPS)
|
|
110
123
|
for name in sorted(args.with_coders):
|
|
@@ -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,6 +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
|
|
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")
|
|
88
103
|
with tempfile.TemporaryDirectory(prefix="agent-guild-") as tmp:
|
|
89
104
|
for component in wanted:
|
|
90
105
|
url, subpath, skill_name = SKILL_SOURCES[component]
|
|
@@ -93,24 +108,28 @@ def install_skills(ctx: Ctx, opts: Options) -> None:
|
|
|
93
108
|
if not repo.is_dir():
|
|
94
109
|
raise SystemExit(f"error: {repo} not found (expected a clone of {url})")
|
|
95
110
|
elif ctx.dry_run:
|
|
96
|
-
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))
|
|
97
112
|
continue
|
|
98
113
|
else:
|
|
99
114
|
repo = _fetch(url, Path(tmp) / REPO_DIRS[component])
|
|
100
115
|
src = repo / subpath
|
|
101
116
|
if not (src / "SKILL.md").is_file():
|
|
102
117
|
raise SystemExit(f"error: no SKILL.md in {src}")
|
|
103
|
-
|
|
118
|
+
for skills_dir in skills_dirs:
|
|
119
|
+
copy_tree(ctx, src, skills_dir / skill_name)
|
|
104
120
|
|
|
105
121
|
|
|
106
122
|
def _rule_sections(opts: Options) -> list[tuple[str, str]]:
|
|
107
|
-
"""(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."""
|
|
108
124
|
if opts.coders_only:
|
|
109
125
|
return [(SPECIALISTS[n].agent, SPECIALISTS[n].rule_body) for n in sorted(opts.with_coders)]
|
|
110
126
|
sections = []
|
|
111
127
|
for heading, template, component in RULE_SECTIONS:
|
|
112
128
|
if component is None or opts.wants(component):
|
|
113
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))
|
|
114
133
|
sections.append((heading, body))
|
|
115
134
|
if heading == "Agents and delegation":
|
|
116
135
|
sections += [(SPECIALISTS[n].agent, SPECIALISTS[n].rule_body) for n in sorted(opts.with_coders)]
|
|
@@ -118,33 +137,56 @@ def _rule_sections(opts: Options) -> list[tuple[str, str]]:
|
|
|
118
137
|
|
|
119
138
|
|
|
120
139
|
def install_rules(ctx: Ctx, opts: Options) -> None:
|
|
121
|
-
"""
|
|
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
|
+
"""
|
|
122
144
|
agents_md, claude_md = ctx.project / "AGENTS.md", ctx.project / "CLAUDE.md"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 "")
|
|
126
150
|
added = []
|
|
127
|
-
if agents_md.exists() and "@AGENTS.md" not in text:
|
|
128
|
-
text = "@AGENTS.md\n" + (("\n" + text) if text.strip() else "")
|
|
129
|
-
added.append("@AGENTS.md import")
|
|
130
151
|
for heading, body in _rule_sections(opts):
|
|
131
152
|
if re.search(rf"^#+ .*{re.escape(heading)}", known, re.MULTILINE):
|
|
132
|
-
ctx.log("
|
|
153
|
+
ctx.log("skipped", target, f"section '{heading}' is already in AGENTS.md or CLAUDE.md")
|
|
133
154
|
continue
|
|
134
155
|
text = text.rstrip("\n") + ("\n\n" if text.strip() else "") + body + "\n"
|
|
135
156
|
added.append(heading)
|
|
136
157
|
if added:
|
|
137
158
|
if not ctx.dry_run:
|
|
138
|
-
|
|
159
|
+
target.write_text(text, encoding="utf-8")
|
|
139
160
|
verb = "edited" if existed else "created"
|
|
140
|
-
(ctx.edited_existing.append(
|
|
141
|
-
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")
|
|
142
181
|
|
|
143
182
|
|
|
144
183
|
def import_cursor_skills(ctx: Ctx, opts: Options) -> None:
|
|
145
184
|
"""Copy the project's Cursor skills (.cursor/skills/*) into .claude/skills/. Existing skills are never replaced."""
|
|
146
185
|
if not opts.import_cursor_skills:
|
|
147
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
|
|
148
190
|
src_root = ctx.project / ".cursor" / "skills"
|
|
149
191
|
if not src_root.is_dir():
|
|
150
192
|
ctx.log("skipped", ".cursor/skills", "not found")
|
|
@@ -162,6 +204,12 @@ def _has_hook(groups: list, marker: str) -> bool:
|
|
|
162
204
|
|
|
163
205
|
|
|
164
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
|
|
165
213
|
shared_path = ctx.project / ".claude" / "settings.json"
|
|
166
214
|
personal_path = ctx.project / ".claude" / "settings.local.json"
|
|
167
215
|
scalar_path = personal_path if opts.personal_settings else shared_path
|
|
@@ -208,16 +256,10 @@ def install_mcp(ctx: Ctx, opts: Options) -> None:
|
|
|
208
256
|
servers_to_add[spec.server_key] = spec.server
|
|
209
257
|
if not servers_to_add:
|
|
210
258
|
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
servers = data.setdefault("mcpServers", {})
|
|
214
|
-
changes = []
|
|
215
|
-
for key, server in servers_to_add.items():
|
|
216
|
-
if key not in servers:
|
|
217
|
-
servers[key] = server
|
|
218
|
-
changes.append(f"{key} server")
|
|
259
|
+
for tool in opts.selected():
|
|
260
|
+
_write_mcp(ctx, tool, servers_to_add)
|
|
219
261
|
user_config = Path.home() / ".claude.json"
|
|
220
|
-
if user_config.exists():
|
|
262
|
+
if user_config.exists() and "claude" in opts.tools:
|
|
221
263
|
try:
|
|
222
264
|
registered = json.loads(user_config.read_text(encoding="utf-8")).get("mcpServers", {})
|
|
223
265
|
for key in servers_to_add:
|
|
@@ -225,9 +267,43 @@ def install_mcp(ctx: Ctx, opts: Options) -> None:
|
|
|
225
267
|
ctx.warn(f"{key} is also registered at user scope; keep one registration so the server isn't listed twice.")
|
|
226
268
|
except (json.JSONDecodeError, AttributeError):
|
|
227
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")
|
|
228
284
|
write_json(ctx, path, data, indent, changes)
|
|
229
285
|
|
|
230
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
|
+
|
|
231
307
|
def encoded_memory_dir(project: Path) -> Path:
|
|
232
308
|
"""Claude Code names a project's auto-memory folder after its absolute path, every non-alphanumeric char -> '-'."""
|
|
233
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):
|
|
@@ -364,3 +391,163 @@ def test_given_coders_only_without_with_then_it_is_refused(tmp_path, sources):
|
|
|
364
391
|
# when / then
|
|
365
392
|
with pytest.raises(SystemExit):
|
|
366
393
|
run(project, sources, "--coders-only")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_given_edited_agent_when_run_again_then_the_merge_hint_is_printed(tmp_path, sources, capsys):
|
|
397
|
+
# given
|
|
398
|
+
project = tmp_path / "p"
|
|
399
|
+
project.mkdir()
|
|
400
|
+
run(project, sources)
|
|
401
|
+
(project / ".claude/agents/coder.md").write_text("mine")
|
|
402
|
+
capsys.readouterr()
|
|
403
|
+
# when
|
|
404
|
+
run(project, sources)
|
|
405
|
+
# then
|
|
406
|
+
assert "merge the changes by hand" in capsys.readouterr().out
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_given_sections_already_in_agents_md_then_the_log_says_skipped_not_unchanged(tmp_path, sources, capsys):
|
|
410
|
+
# given
|
|
411
|
+
project = tmp_path / "p"
|
|
412
|
+
project.mkdir()
|
|
413
|
+
(project / "AGENTS.md").write_text("## Memory bank\n\n## Phase Gate Protocol\n")
|
|
414
|
+
# when
|
|
415
|
+
run(project, sources)
|
|
416
|
+
# then
|
|
417
|
+
out = capsys.readouterr().out
|
|
418
|
+
assert "skipped AGENTS.md section 'Phase Gate Protocol' is already in AGENTS.md or CLAUDE.md" in out
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def test_given_symlinked_skills_dir_then_the_log_notes_where_skills_are_written(tmp_path, sources, capsys):
|
|
422
|
+
# given
|
|
423
|
+
project = tmp_path / "p"
|
|
424
|
+
(project / ".cursor/skills").mkdir(parents=True)
|
|
425
|
+
(project / ".claude").mkdir()
|
|
426
|
+
try:
|
|
427
|
+
(project / ".claude/skills").symlink_to(project / ".cursor/skills", target_is_directory=True)
|
|
428
|
+
except (OSError, NotImplementedError):
|
|
429
|
+
pytest.skip("symlinks are not available here")
|
|
430
|
+
# when
|
|
431
|
+
run(project, sources)
|
|
432
|
+
# then
|
|
433
|
+
assert "is a symlink to" in capsys.readouterr().out
|
|
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
|