@event4u/agent-config 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent-src/rules/no-cheap-questions.md +11 -2
- package/.agent-src/skills/readme-writing-package/SKILL.md +24 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +69 -0
- package/README.md +71 -6
- package/docs/DISTRIBUTION_CHECKLIST.md +7 -6
- package/docs/architecture.md +1 -1
- package/docs/contracts/tier-3-contrib-plugin.md +129 -0
- package/docs/decisions/ADR-007-agent-discovery-scopes.md +286 -0
- package/docs/decisions/ADR-008-installed-tools-manifest.md +160 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +32 -0
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +135 -0
- package/docs/installation.md +83 -27
- package/docs/setup/per-ide/aider.md +1 -1
- package/docs/setup/per-ide/claude-code.md +1 -1
- package/docs/setup/per-ide/claude-desktop.md +8 -4
- package/docs/setup/per-ide/cline.md +2 -2
- package/docs/setup/per-ide/codex.md +1 -1
- package/docs/setup/per-ide/copilot.md +2 -2
- package/docs/setup/per-ide/cursor.md +2 -2
- package/docs/setup/per-ide/gemini-cli.md +1 -1
- package/docs/setup/per-ide/windsurf.md +2 -2
- package/docs/troubleshooting.md +4 -4
- package/package.json +1 -1
- package/scripts/_cli/cmd_export.py +157 -0
- package/scripts/_cli/cmd_sync.py +162 -0
- package/scripts/_cli/cmd_update.py +23 -1
- package/scripts/_cli/cmd_validate.py +164 -0
- package/scripts/_lib/installed_lock.py +160 -0
- package/scripts/_lib/installed_tools.py +237 -0
- package/scripts/agent-config +78 -1
- package/scripts/install +43 -10
- package/scripts/install.py +975 -14
- package/templates/agent-config-wrapper.sh +1 -1
- package/templates/consumer-settings/README.md +1 -1
- package/templates/marketing-copy.yml +6 -5
package/docs/installation.md
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
# Installation
|
|
2
2
|
|
|
3
|
-
**Principle:**
|
|
4
|
-
|
|
3
|
+
**Principle:** Global-first install (cross-project, in `~/.claude/`,
|
|
4
|
+
`~/.cursor/`, …), opt-in project export when a team wants the config
|
|
5
|
+
committed to a repo. No Task, no Make, no build tools required.
|
|
6
|
+
|
|
7
|
+
> **v2.1+** — the installer detects intent. Running `npx
|
|
8
|
+
> @event4u/agent-config init` in `~/` or any directory without a
|
|
9
|
+
> project manifest defaults to **global**. Running it inside a project
|
|
10
|
+
> (`package.json` / `composer.json` / `pyproject.toml` / etc.) defaults
|
|
11
|
+
> to **project**. Pass `--scope=global` or `--scope=project` to override
|
|
12
|
+
> detection. See `--scope` in the CLI help for the full matrix.
|
|
13
|
+
|
|
14
|
+
A global install records itself in `~/.config/agent-config/installed.lock`
|
|
15
|
+
(schema_version, agent_config_version, installed_at, tools[]). `npx
|
|
16
|
+
@event4u/agent-config update` keeps that manifest in lockstep
|
|
17
|
+
with the project pin in `.agent-settings.yml`. A version-mismatched
|
|
18
|
+
re-run of `init --scope=global` is refused with exit code 1 until you
|
|
19
|
+
`update` or pass `--force`.
|
|
20
|
+
|
|
21
|
+
To commit a specific tool's config into a project repo, use:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
agent-config export --tool=<id> --output=<path>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
(Idempotent; `--force` overrides drift. `--list` enumerates supported
|
|
28
|
+
tool ids. See [`docs/contracts/command-clusters.md`](contracts/command-clusters.md)
|
|
29
|
+
for the export contract.)
|
|
5
30
|
|
|
6
31
|
## Per-IDE setup — quick index
|
|
7
32
|
|
|
@@ -12,16 +37,16 @@ section below this index is reference material for advanced installs
|
|
|
12
37
|
|
|
13
38
|
| Surface | One-liner | Per-IDE page |
|
|
14
39
|
|---|---|---|
|
|
15
|
-
| **Claude Code** | `npx @event4u/
|
|
40
|
+
| **Claude Code** | `npx @event4u/agent-config init --tools=claude-code` | [`per-ide/claude-code.md`](setup/per-ide/claude-code.md) |
|
|
16
41
|
| **Claude Desktop** | (uses `~/.claude/skills/` from Claude Code global install) | [`per-ide/claude-desktop.md`](setup/per-ide/claude-desktop.md) |
|
|
17
|
-
| **Cursor** | `npx @event4u/
|
|
18
|
-
| **Windsurf** | `npx @event4u/
|
|
19
|
-
| **Cline** | `npx @event4u/
|
|
20
|
-
| **Aider** | `npx @event4u/
|
|
21
|
-
| **Codex CLI** | `npx @event4u/
|
|
22
|
-
| **Gemini CLI** | `npx @event4u/
|
|
23
|
-
| **GitHub Copilot** | `npx @event4u/
|
|
24
|
-
| **All surfaces** | `npx @event4u/
|
|
42
|
+
| **Cursor** | `npx @event4u/agent-config init --tools=cursor` | [`per-ide/cursor.md`](setup/per-ide/cursor.md) |
|
|
43
|
+
| **Windsurf** | `npx @event4u/agent-config init --tools=windsurf` | [`per-ide/windsurf.md`](setup/per-ide/windsurf.md) |
|
|
44
|
+
| **Cline** | `npx @event4u/agent-config init --tools=cline` | [`per-ide/cline.md`](setup/per-ide/cline.md) |
|
|
45
|
+
| **Aider** | `npx @event4u/agent-config init --tools=aider` | [`per-ide/aider.md`](setup/per-ide/aider.md) |
|
|
46
|
+
| **Codex CLI** | `npx @event4u/agent-config init --tools=codex` | [`per-ide/codex.md`](setup/per-ide/codex.md) |
|
|
47
|
+
| **Gemini CLI** | `npx @event4u/agent-config init --tools=gemini` | [`per-ide/gemini-cli.md`](setup/per-ide/gemini-cli.md) |
|
|
48
|
+
| **GitHub Copilot** | `npx @event4u/agent-config init --tools=copilot` | [`per-ide/copilot.md`](setup/per-ide/copilot.md) |
|
|
49
|
+
| **All surfaces** | `npx @event4u/agent-config init` (default) | (each page above applies) |
|
|
25
50
|
|
|
26
51
|
Combine surfaces by comma-separating: `--tools=claude-code,cursor,windsurf`.
|
|
27
52
|
|
|
@@ -81,7 +106,7 @@ the per-IDE index above.
|
|
|
81
106
|
> 2. `scripts/install.py` — bridge files (`.agent-settings.yml`, VSCode /
|
|
82
107
|
> Augment / Copilot JSON descriptors).
|
|
83
108
|
>
|
|
84
|
-
> `npx @event4u/
|
|
109
|
+
> `npx @event4u/agent-config init` and `setup.sh` (curl-based)
|
|
85
110
|
> are thin wrappers that delegate to `scripts/install`. Both underlying
|
|
86
111
|
> stages remain callable directly for advanced use; see their `--help`.
|
|
87
112
|
>
|
|
@@ -123,23 +148,23 @@ same flags, no extra state.
|
|
|
123
148
|
|
|
124
149
|
```bash
|
|
125
150
|
# Pick tools interactively (TTY checkbox prompt)
|
|
126
|
-
npx @event4u/
|
|
151
|
+
npx @event4u/agent-config init
|
|
127
152
|
|
|
128
153
|
# Pick tools explicitly, non-interactive
|
|
129
|
-
npx @event4u/
|
|
154
|
+
npx @event4u/agent-config init --tools=claude-code,cursor --yes
|
|
130
155
|
|
|
131
156
|
# Install everything (the default — backward-compatible)
|
|
132
|
-
npx @event4u/
|
|
157
|
+
npx @event4u/agent-config init --tools=all --yes
|
|
133
158
|
|
|
134
159
|
# Test a specific git ref (branch, tag, sha) instead of the latest npm tag
|
|
135
|
-
npx @event4u/
|
|
160
|
+
npx @event4u/agent-config init --ref=main --yes
|
|
136
161
|
```
|
|
137
162
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
`npx @event4u/agent-config init` fetches the latest tarball, runs
|
|
164
|
+
`bash scripts/install --target <cwd> …`, and the install script handles
|
|
165
|
+
its own cleanup. The same package exposes every other `agent-config`
|
|
166
|
+
subcommand (`sync`, `validate`, `mcp:render`, `roadmap:progress`, …) —
|
|
167
|
+
see `npx @event4u/agent-config help`.
|
|
143
168
|
|
|
144
169
|
### `curl | bash` (no Node required)
|
|
145
170
|
|
|
@@ -177,12 +202,12 @@ The package is versioned with the project. Settings are committed once.
|
|
|
177
202
|
### npx (recommended for any project)
|
|
178
203
|
|
|
179
204
|
```bash
|
|
180
|
-
npx @event4u/
|
|
205
|
+
npx @event4u/agent-config init --tools=claude-code,cursor
|
|
181
206
|
```
|
|
182
207
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
208
|
+
`npx` fetches the latest `@event4u/agent-config` tarball and runs
|
|
209
|
+
`scripts/install` with the selected tools. Nothing is added to
|
|
210
|
+
`package.json`.
|
|
186
211
|
|
|
187
212
|
### Global CLI (one install per machine)
|
|
188
213
|
|
|
@@ -248,6 +273,7 @@ After initial setup, commit these files:
|
|
|
248
273
|
|
|
249
274
|
```
|
|
250
275
|
.agent-settings.yml ← shared profile (e.g., cost_profile: minimal)
|
|
276
|
+
agents/installed-tools.lock ← AI bill of materials (ADR-008, Phase 3)
|
|
251
277
|
.augment/ ← rules, skills, commands (symlinks)
|
|
252
278
|
.cursor/rules/ ← Cursor rules (symlinks)
|
|
253
279
|
.claude/ ← Claude rules, skills (symlinks)
|
|
@@ -255,7 +281,37 @@ AGENTS.md ← Copilot/Gemini instructions
|
|
|
255
281
|
.github/copilot-instructions.md ← GitHub Copilot instructions
|
|
256
282
|
```
|
|
257
283
|
|
|
258
|
-
|
|
284
|
+
`agents/installed-tools.lock` lists every AI tool the project expects,
|
|
285
|
+
its scope (`global` or `project`), and its bridge marker path. Written
|
|
286
|
+
by `init`, replayed by `sync`, checked by `validate`. Schema and
|
|
287
|
+
workflow: [`docs/guidelines/agent-infra/installed-tools-manifest.md`](guidelines/agent-infra/installed-tools-manifest.md).
|
|
288
|
+
|
|
289
|
+
### Team onboarding — clone → sync → done
|
|
290
|
+
|
|
291
|
+
New team members get every AI bridge online with a single command:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
git clone <repo>
|
|
295
|
+
cd <repo>
|
|
296
|
+
npx @event4u/agent-config sync
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`sync` reads `agents/installed-tools.lock` and re-runs the installer
|
|
300
|
+
for every tool whose bridge marker is missing locally. Idempotent —
|
|
301
|
+
re-running after every clone is safe. Tools with markers already in
|
|
302
|
+
place are skipped.
|
|
303
|
+
|
|
304
|
+
Pair it with a CI gate to catch drift in PRs:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
npx @event4u/agent-config validate
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`validate` is read-only. Exit 1 on any of: marker missing, scope
|
|
311
|
+
divergence (manifest says `project` but marker only exists at the
|
|
312
|
+
global anchor, or vice versa), version drift (manifest's
|
|
313
|
+
`agent_config_version` ≠ installed package). Full drift catalog and
|
|
314
|
+
fix table: [`installed-tools-manifest.md § Drift detection`](guidelines/agent-infra/installed-tools-manifest.md#drift-detection-ci-gate).
|
|
259
315
|
|
|
260
316
|
---
|
|
261
317
|
|
|
@@ -559,7 +615,7 @@ When a new version of the package is published:
|
|
|
559
615
|
|
|
560
616
|
```bash
|
|
561
617
|
# npx (one-shot, recommended) — always uses the latest tarball
|
|
562
|
-
npx @event4u/
|
|
618
|
+
npx @event4u/agent-config init --tools=claude-code,cursor
|
|
563
619
|
|
|
564
620
|
# Global CLI
|
|
565
621
|
npm install -g @event4u/agent-config@latest
|
|
@@ -15,7 +15,7 @@ projects to those paths during install.
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
# Inside an existing repo:
|
|
18
|
-
npx @event4u/
|
|
18
|
+
npx @event4u/agent-config init --tools=claude-code
|
|
19
19
|
|
|
20
20
|
# Or with the curl entrypoint:
|
|
21
21
|
curl -sSL https://raw.githubusercontent.com/event4u-app/agent-config/main/setup.sh \
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
The fastest path to running our skills, rules, and (optionally) the MCP
|
|
4
4
|
server inside Claude Desktop. macOS / Windows / Linux. ~5 minutes.
|
|
5
5
|
|
|
6
|
-
> **TL;DR** —
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
6
|
+
> **TL;DR** — Claude Desktop reads from `~/.claude/` (global only, no
|
|
7
|
+
> project-local discovery on macOS). Run `npx @event4u/agent-config
|
|
8
|
+
> global --tools=claude-desktop` once per user, or
|
|
9
|
+
> `npx @event4u/agent-config init --tools=claude-code` per project
|
|
10
|
+
> (Claude Code's project install also covers Desktop on macOS via the
|
|
11
|
+
> shared `~/.claude/` location seeded during `init`). The v1 npm /
|
|
12
|
+
> composer install scheme is retired; the new global-first scheme is
|
|
13
|
+
> ADR-007 and writes through `~/.config/agent-config/installed.lock`.
|
|
10
14
|
|
|
11
15
|
## Prerequisites
|
|
12
16
|
|
|
@@ -11,7 +11,7 @@ and `AGENTS.md`.
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx @event4u/
|
|
14
|
+
npx @event4u/agent-config init --tools=cline
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Populates:
|
|
@@ -35,7 +35,7 @@ automatically. Run `/help` in the chat to verify rule loading.
|
|
|
35
35
|
| Symptom | Fix |
|
|
36
36
|
|---|---|
|
|
37
37
|
| Rules not picked up | Reload VS Code window after `task generate-tools`. |
|
|
38
|
-
| `.clinerules` missing | Re-run `npx @event4u/
|
|
38
|
+
| `.clinerules` missing | Re-run `npx @event4u/agent-config init --tools=cline`. |
|
|
39
39
|
|
|
40
40
|
## Cross-references
|
|
41
41
|
|
|
@@ -13,7 +13,7 @@ falls back to `AGENTS.md` where supported.
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx @event4u/
|
|
16
|
+
npx @event4u/agent-config init --tools=copilot
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Populates:
|
|
@@ -69,7 +69,7 @@ gh copilot --version # if you want CLI plugin
|
|
|
69
69
|
| Symptom | Fix |
|
|
70
70
|
|---|---|
|
|
71
71
|
| Copilot ignores the file | Reload the IDE window after install. |
|
|
72
|
-
| File missing after install | Re-run `npx @event4u/
|
|
72
|
+
| File missing after install | Re-run `npx @event4u/agent-config init --tools=copilot`. |
|
|
73
73
|
| Copilot PR review too noisy | See the `copilot-config` skill for suppression patterns. |
|
|
74
74
|
|
|
75
75
|
## Cross-references
|
|
@@ -19,7 +19,7 @@ The package ships **both** so you don't have to pick.
|
|
|
19
19
|
## Project install
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx @event4u/
|
|
22
|
+
npx @event4u/agent-config init --tools=cursor
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
This populates:
|
|
@@ -32,7 +32,7 @@ This populates:
|
|
|
32
32
|
Combine surfaces if you use both Cursor and Claude Code:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npx @event4u/
|
|
35
|
+
npx @event4u/agent-config init --tools=cursor,claude-code
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Global install
|
|
@@ -19,7 +19,7 @@ The package ships **both**.
|
|
|
19
19
|
## Project install
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx @event4u/
|
|
22
|
+
npx @event4u/agent-config init --tools=windsurf
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Populates:
|
|
@@ -32,7 +32,7 @@ Populates:
|
|
|
32
32
|
Combine with other surfaces:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npx @event4u/
|
|
35
|
+
npx @event4u/agent-config init --tools=windsurf,claude-code,cursor
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Wave-8 frontmatter
|
package/docs/troubleshooting.md
CHANGED
|
@@ -34,7 +34,7 @@ bash scripts/install --verbose
|
|
|
34
34
|
# or, to regenerate everything (overwrites existing bridge files):
|
|
35
35
|
bash scripts/install --force
|
|
36
36
|
# or, for one-shot installs without a local node_modules tree:
|
|
37
|
-
npx @event4u/
|
|
37
|
+
npx @event4u/agent-config init --tools=claude-code,cursor
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
### Check 2: Does your agent actually read these directories?
|
|
@@ -70,7 +70,7 @@ orchestrator explicitly inside the project root:
|
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
# One-shot, no local checkout required (recommended)
|
|
73
|
-
npx @event4u/
|
|
73
|
+
npx @event4u/agent-config init --tools=claude-code,cursor
|
|
74
74
|
|
|
75
75
|
# When the global CLI is installed
|
|
76
76
|
agent-config install --tools=claude-code,cursor
|
|
@@ -84,7 +84,7 @@ When the package version changes, symlinks that pointed to the old
|
|
|
84
84
|
package path may break. Re-run the installer — it is idempotent:
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
|
-
npx @event4u/
|
|
87
|
+
npx @event4u/agent-config init --tools=claude-code,cursor
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
The installer replaces stale symlinks with fresh ones pointing at the
|
|
@@ -99,7 +99,7 @@ and Unix-style symlinks. Recommended setup:
|
|
|
99
99
|
|
|
100
100
|
1. **WSL2** (preferred): install Ubuntu or a distribution of your choice,
|
|
101
101
|
clone the project inside the WSL filesystem, and run
|
|
102
|
-
`npx @event4u/
|
|
102
|
+
`npx @event4u/agent-config init` from WSL.
|
|
103
103
|
2. **Git Bash**: works for the basic install, but symlinks require
|
|
104
104
|
Developer Mode (Windows 10 1703+) or admin privileges. Without either,
|
|
105
105
|
Git Bash falls back to copies, which means updates will not propagate
|
package/package.json
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""``agent-config export`` — eject a tool's canonical content into the project.
|
|
2
|
+
|
|
3
|
+
Phase 1.5 of road-to-global-first-install.md (ADR-007 D3). Replaces the
|
|
4
|
+
rejected symlink-bridge subcommand: writes a real file with the resolved
|
|
5
|
+
content for a named tool into a user-chosen path so it can be committed,
|
|
6
|
+
shared with the team, or customized in place. Idempotent by default;
|
|
7
|
+
``--force`` overrides content drift. No canonical-path defaults.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import hashlib
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Callable, Optional
|
|
16
|
+
|
|
17
|
+
from scripts.install import (
|
|
18
|
+
AIDER_MARKER,
|
|
19
|
+
CLAUDE_DESKTOP_MARKER,
|
|
20
|
+
CODEX_MARKER,
|
|
21
|
+
CONTINUE_MARKER,
|
|
22
|
+
JETBRAINS_MARKER,
|
|
23
|
+
KILOCODE_MARKER,
|
|
24
|
+
KIRO_MARKER,
|
|
25
|
+
ROOCODE_MARKER,
|
|
26
|
+
ZED_MARKER,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
30
|
+
TEMPLATES_DIR = PACKAGE_ROOT / ".agent-src" / "templates"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _from_template(rel: str) -> Callable[[], str]:
|
|
34
|
+
def _read() -> str:
|
|
35
|
+
path = TEMPLATES_DIR / rel
|
|
36
|
+
if not path.is_file():
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
f"template missing from package: {path} "
|
|
39
|
+
f"(reinstall @event4u/agent-config or report a bug)"
|
|
40
|
+
)
|
|
41
|
+
return path.read_text(encoding="utf-8")
|
|
42
|
+
return _read
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _from_constant(value: str) -> Callable[[], str]:
|
|
46
|
+
def _read() -> str:
|
|
47
|
+
return value
|
|
48
|
+
return _read
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# tool_id → (description, content_provider).
|
|
52
|
+
EXPORT_REGISTRY: "dict[str, tuple[str, Callable[[], str]]]" = {
|
|
53
|
+
"roocode": ("Roo Code marker (.roo/rules/agent-config.md body)",
|
|
54
|
+
_from_constant(ROOCODE_MARKER)),
|
|
55
|
+
"claude-desktop": ("Claude Desktop marker (informational, global-scope tool)",
|
|
56
|
+
_from_constant(CLAUDE_DESKTOP_MARKER)),
|
|
57
|
+
"aider": ("Aider marker (manual `read:` wiring documented inline)",
|
|
58
|
+
_from_constant(AIDER_MARKER)),
|
|
59
|
+
"codex": ("Codex CLI marker (informational — AGENTS.md is canonical)",
|
|
60
|
+
_from_constant(CODEX_MARKER)),
|
|
61
|
+
"continue": ("Continue.dev marker (.continue/rules/agent-config.md body)",
|
|
62
|
+
_from_constant(CONTINUE_MARKER)),
|
|
63
|
+
"kilocode": ("Kilo Code marker (.kilocode/rules/agent-config.md body)",
|
|
64
|
+
_from_constant(KILOCODE_MARKER)),
|
|
65
|
+
"zed": ("Zed marker (informational — .rules at repo root is canonical)",
|
|
66
|
+
_from_constant(ZED_MARKER)),
|
|
67
|
+
"jetbrains": ("JetBrains AI Assistant marker (.jetbrains/agent-config.md body)",
|
|
68
|
+
_from_constant(JETBRAINS_MARKER)),
|
|
69
|
+
"kiro": ("Kiro marker (.kiro/steering/agent-config.md body)",
|
|
70
|
+
_from_constant(KIRO_MARKER)),
|
|
71
|
+
"agents-md": ("AGENTS.md template (Thin-Root entry point — consumer scaffold)",
|
|
72
|
+
_from_template("AGENTS.md")),
|
|
73
|
+
"copilot-instructions": ("GitHub Copilot Code Review instructions template",
|
|
74
|
+
_from_template("copilot-instructions.md")),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _list_tools(out) -> int:
|
|
79
|
+
print("Available tools for `agent-config export --tool <id>`:", file=out)
|
|
80
|
+
width = max(len(t) for t in EXPORT_REGISTRY) + 2
|
|
81
|
+
for tool_id, (desc, _) in sorted(EXPORT_REGISTRY.items()):
|
|
82
|
+
print(f" {tool_id:<{width}}{desc}", file=out)
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _hash(content: str) -> str:
|
|
87
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _rel(path: Path) -> Path:
|
|
91
|
+
try:
|
|
92
|
+
return path.relative_to(Path.cwd())
|
|
93
|
+
except ValueError:
|
|
94
|
+
return path
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _write(output: Path, content: str, *, force: bool, out, err) -> int:
|
|
98
|
+
if output.exists():
|
|
99
|
+
existing = output.read_text(encoding="utf-8")
|
|
100
|
+
if _hash(existing) == _hash(content):
|
|
101
|
+
print(f"ℹ️ {_rel(output)} already exported (content matches).", file=out)
|
|
102
|
+
return 0
|
|
103
|
+
if not force:
|
|
104
|
+
print(
|
|
105
|
+
f"❌ refusing to overwrite {output} — content differs. "
|
|
106
|
+
f"Pass --force to replace.",
|
|
107
|
+
file=err,
|
|
108
|
+
)
|
|
109
|
+
return 1
|
|
110
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
output.write_text(content, encoding="utf-8")
|
|
112
|
+
print(f"✅ exported to {_rel(output)}", file=out)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main(argv: Optional[list[str]] = None, *, out=sys.stdout, err=sys.stderr) -> int:
|
|
117
|
+
parser = argparse.ArgumentParser(
|
|
118
|
+
prog="agent-config export",
|
|
119
|
+
description="Eject a tool's resolved content into a user-chosen path.",
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument("--tool", metavar="ID",
|
|
122
|
+
help="Tool to export (see --list for the catalog).")
|
|
123
|
+
parser.add_argument("--output", metavar="PATH",
|
|
124
|
+
help="Destination path (relative to CWD).")
|
|
125
|
+
parser.add_argument("--force", action="store_true",
|
|
126
|
+
help="Overwrite an existing file with non-matching content.")
|
|
127
|
+
parser.add_argument("--list", action="store_true",
|
|
128
|
+
help="Print supported tool IDs with descriptions and exit.")
|
|
129
|
+
args = parser.parse_args(argv)
|
|
130
|
+
|
|
131
|
+
if args.list:
|
|
132
|
+
return _list_tools(out)
|
|
133
|
+
if not args.tool:
|
|
134
|
+
print("❌ --tool is required (see --list for the catalog).", file=err)
|
|
135
|
+
return 2
|
|
136
|
+
if not args.output:
|
|
137
|
+
print("❌ --output is required (no canonical-path defaults).", file=err)
|
|
138
|
+
return 2
|
|
139
|
+
|
|
140
|
+
entry = EXPORT_REGISTRY.get(args.tool)
|
|
141
|
+
if entry is None:
|
|
142
|
+
print(f"❌ unknown tool: {args.tool} (see --list)", file=err)
|
|
143
|
+
return 2
|
|
144
|
+
|
|
145
|
+
_, provider = entry
|
|
146
|
+
try:
|
|
147
|
+
content = provider()
|
|
148
|
+
except FileNotFoundError as exc:
|
|
149
|
+
print(f"❌ {exc}", file=err)
|
|
150
|
+
return 1
|
|
151
|
+
|
|
152
|
+
output = Path(args.output).expanduser().resolve()
|
|
153
|
+
return _write(output, content, force=args.force, out=out, err=err)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__": # pragma: no cover
|
|
157
|
+
sys.exit(main())
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""``agent-config sync`` — replay the installed-tools manifest (ADR-008).
|
|
2
|
+
|
|
3
|
+
Phase 3.3 of road-to-global-first-install.md. Reads
|
|
4
|
+
``agents/installed-tools.lock``, then re-runs the bridge install for every
|
|
5
|
+
tool whose ``bridge_marker`` is missing on disk. Tools whose marker already
|
|
6
|
+
exists are skipped — the typical clone-and-sync flow is therefore idempotent
|
|
7
|
+
on the second invocation.
|
|
8
|
+
|
|
9
|
+
Sync never edits the manifest itself; ``init`` is the only writer. Sync only
|
|
10
|
+
calls the installer with ``--scope`` / ``--tools`` derived from the manifest
|
|
11
|
+
entries, so the manifest is the single source of truth.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Iterable
|
|
20
|
+
|
|
21
|
+
from scripts._lib import installed_tools
|
|
22
|
+
from scripts.install import main as install_main
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _marker_exists(project_root: Path, bridge_marker: str, scope: str) -> bool:
|
|
26
|
+
if not bridge_marker:
|
|
27
|
+
return True # substrate-only entries (rare); treat as present
|
|
28
|
+
if scope == "global":
|
|
29
|
+
target = Path(bridge_marker).expanduser()
|
|
30
|
+
else:
|
|
31
|
+
# Project-scope: relative to the project root unless absolute.
|
|
32
|
+
candidate = Path(bridge_marker)
|
|
33
|
+
target = candidate if candidate.is_absolute() else (project_root / candidate)
|
|
34
|
+
return target.exists()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _group_by_scope(
|
|
38
|
+
entries: Iterable[dict],
|
|
39
|
+
project_root: Path,
|
|
40
|
+
) -> tuple[dict[str, list[str]], list[tuple[str, str]]]:
|
|
41
|
+
"""Return ({scope: [tool_names]}, [(name, marker_path)]) for missing tools.
|
|
42
|
+
|
|
43
|
+
The second list is the human-readable summary of what will be replayed.
|
|
44
|
+
"""
|
|
45
|
+
missing: dict[str, list[str]] = {"project": [], "global": []}
|
|
46
|
+
surfaced: list[tuple[str, str]] = []
|
|
47
|
+
for entry in entries:
|
|
48
|
+
name = str(entry.get("name", "")).strip()
|
|
49
|
+
scope = str(entry.get("scope", "")).strip()
|
|
50
|
+
bridge_marker = str(entry.get("bridge_marker", "")).strip()
|
|
51
|
+
if not name or scope not in ("project", "global"):
|
|
52
|
+
continue
|
|
53
|
+
if _marker_exists(project_root, bridge_marker, scope):
|
|
54
|
+
continue
|
|
55
|
+
missing[scope].append(name)
|
|
56
|
+
surfaced.append((name, bridge_marker))
|
|
57
|
+
return missing, surfaced
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _run_install(scope: str, tools: list[str], project_root: Path, *, force: bool, dry_run: bool) -> int:
|
|
61
|
+
if not tools:
|
|
62
|
+
return 0
|
|
63
|
+
argv = [f"--scope={scope}", f"--tools={','.join(sorted(set(tools)))}"]
|
|
64
|
+
if scope == "project":
|
|
65
|
+
argv += [f"--project={project_root}", "--no-smoke"]
|
|
66
|
+
if force:
|
|
67
|
+
argv.append("--force")
|
|
68
|
+
if dry_run:
|
|
69
|
+
argv.append("--skip-bridges")
|
|
70
|
+
return install_main(argv)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse(argv: list[str]) -> argparse.Namespace:
|
|
74
|
+
parser = argparse.ArgumentParser(
|
|
75
|
+
prog="agent-config sync",
|
|
76
|
+
description=(
|
|
77
|
+
"Replay agents/installed-tools.lock — re-installs any tool whose "
|
|
78
|
+
"bridge marker is missing locally. Idempotent."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--project",
|
|
83
|
+
default=None,
|
|
84
|
+
help="Override the project root (defaults to PROJECT_ROOT or cwd).",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--dry-run",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="Print the planned replay set without touching bridges.",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--force",
|
|
93
|
+
action="store_true",
|
|
94
|
+
help="Forwarded to the installer (overwrites existing bridge files).",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--quiet",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="Suppress non-essential output.",
|
|
100
|
+
)
|
|
101
|
+
return parser.parse_args(argv)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _emit(quiet: bool, msg: str) -> None:
|
|
105
|
+
if not quiet:
|
|
106
|
+
print(msg)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main(argv: list[str]) -> int:
|
|
110
|
+
opts = _parse(argv)
|
|
111
|
+
project_root = Path(
|
|
112
|
+
opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()
|
|
113
|
+
).resolve()
|
|
114
|
+
manifest = installed_tools.manifest_path(project_root)
|
|
115
|
+
data = installed_tools.read_manifest(manifest)
|
|
116
|
+
|
|
117
|
+
if data is None:
|
|
118
|
+
_emit(opts.quiet, f"❌ No manifest found at {manifest}")
|
|
119
|
+
_emit(opts.quiet, " Run `./agent-config init --tools=<id>` to create one.")
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
entries = list(data.get("tools") or [])
|
|
123
|
+
if not entries:
|
|
124
|
+
_emit(opts.quiet, f"ℹ️ Manifest is empty: {manifest}")
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
missing, surfaced = _group_by_scope(entries, project_root)
|
|
128
|
+
total_missing = sum(len(v) for v in missing.values())
|
|
129
|
+
total_present = len(entries) - total_missing
|
|
130
|
+
|
|
131
|
+
_emit(opts.quiet, f"Manifest: {manifest}")
|
|
132
|
+
_emit(opts.quiet, f"Tools: {len(entries)} listed, {total_present} present, {total_missing} missing")
|
|
133
|
+
if total_missing == 0:
|
|
134
|
+
_emit(opts.quiet, "✅ All bridges already installed. Nothing to do.")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
for name, marker in surfaced:
|
|
138
|
+
_emit(opts.quiet, f" • {name:<15} → {marker} (missing)")
|
|
139
|
+
|
|
140
|
+
if opts.dry_run:
|
|
141
|
+
_emit(opts.quiet, "")
|
|
142
|
+
_emit(opts.quiet, "Dry-run: no bridges written.")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
_emit(opts.quiet, "")
|
|
146
|
+
for scope in ("project", "global"):
|
|
147
|
+
tools = missing[scope]
|
|
148
|
+
if not tools:
|
|
149
|
+
continue
|
|
150
|
+
_emit(opts.quiet, f"Replaying scope={scope}: {', '.join(sorted(tools))}")
|
|
151
|
+
rc = _run_install(scope, tools, project_root, force=opts.force, dry_run=False)
|
|
152
|
+
if rc != 0:
|
|
153
|
+
_emit(opts.quiet, f"❌ Installer failed for scope={scope} (rc={rc}); aborting.")
|
|
154
|
+
return rc
|
|
155
|
+
|
|
156
|
+
_emit(opts.quiet, "")
|
|
157
|
+
_emit(opts.quiet, "✅ Sync complete.")
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
sys.exit(main(sys.argv[1:]))
|