@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.
Files changed (38) hide show
  1. package/.agent-src/rules/no-cheap-questions.md +11 -2
  2. package/.agent-src/skills/readme-writing-package/SKILL.md +24 -0
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +69 -0
  5. package/README.md +71 -6
  6. package/docs/DISTRIBUTION_CHECKLIST.md +7 -6
  7. package/docs/architecture.md +1 -1
  8. package/docs/contracts/tier-3-contrib-plugin.md +129 -0
  9. package/docs/decisions/ADR-007-agent-discovery-scopes.md +286 -0
  10. package/docs/decisions/ADR-008-installed-tools-manifest.md +160 -0
  11. package/docs/decisions/INDEX.md +2 -0
  12. package/docs/getting-started.md +1 -1
  13. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +32 -0
  14. package/docs/guidelines/agent-infra/installed-tools-manifest.md +135 -0
  15. package/docs/installation.md +83 -27
  16. package/docs/setup/per-ide/aider.md +1 -1
  17. package/docs/setup/per-ide/claude-code.md +1 -1
  18. package/docs/setup/per-ide/claude-desktop.md +8 -4
  19. package/docs/setup/per-ide/cline.md +2 -2
  20. package/docs/setup/per-ide/codex.md +1 -1
  21. package/docs/setup/per-ide/copilot.md +2 -2
  22. package/docs/setup/per-ide/cursor.md +2 -2
  23. package/docs/setup/per-ide/gemini-cli.md +1 -1
  24. package/docs/setup/per-ide/windsurf.md +2 -2
  25. package/docs/troubleshooting.md +4 -4
  26. package/package.json +1 -1
  27. package/scripts/_cli/cmd_export.py +157 -0
  28. package/scripts/_cli/cmd_sync.py +162 -0
  29. package/scripts/_cli/cmd_update.py +23 -1
  30. package/scripts/_cli/cmd_validate.py +164 -0
  31. package/scripts/_lib/installed_lock.py +160 -0
  32. package/scripts/_lib/installed_tools.py +237 -0
  33. package/scripts/agent-config +78 -1
  34. package/scripts/install +43 -10
  35. package/scripts/install.py +975 -14
  36. package/templates/agent-config-wrapper.sh +1 -1
  37. package/templates/consumer-settings/README.md +1 -1
  38. package/templates/marketing-copy.yml +6 -5
@@ -1,7 +1,32 @@
1
1
  # Installation
2
2
 
3
- **Principle:** Project-installed by default, plugin-enhanced when available.
4
- No Task, no Make, no build tools required for installation.
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/create-agent-config init --tools=claude-code` | [`per-ide/claude-code.md`](setup/per-ide/claude-code.md) |
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/create-agent-config init --tools=cursor` | [`per-ide/cursor.md`](setup/per-ide/cursor.md) |
18
- | **Windsurf** | `npx @event4u/create-agent-config init --tools=windsurf` | [`per-ide/windsurf.md`](setup/per-ide/windsurf.md) |
19
- | **Cline** | `npx @event4u/create-agent-config init --tools=cline` | [`per-ide/cline.md`](setup/per-ide/cline.md) |
20
- | **Aider** | `npx @event4u/create-agent-config init --tools=aider` | [`per-ide/aider.md`](setup/per-ide/aider.md) |
21
- | **Codex CLI** | `npx @event4u/create-agent-config init --tools=codex` | [`per-ide/codex.md`](setup/per-ide/codex.md) |
22
- | **Gemini CLI** | `npx @event4u/create-agent-config init --tools=gemini` | [`per-ide/gemini-cli.md`](setup/per-ide/gemini-cli.md) |
23
- | **GitHub Copilot** | `npx @event4u/create-agent-config init --tools=copilot` | [`per-ide/copilot.md`](setup/per-ide/copilot.md) |
24
- | **All surfaces** | `npx @event4u/create-agent-config init` (default) | (each page above applies) |
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/create-agent-config init` and `setup.sh` (curl-based)
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/create-agent-config init
151
+ npx @event4u/agent-config init
127
152
 
128
153
  # Pick tools explicitly, non-interactive
129
- npx @event4u/create-agent-config init --tools=claude-code,cursor --yes
154
+ npx @event4u/agent-config init --tools=claude-code,cursor --yes
130
155
 
131
156
  # Install everything (the default — backward-compatible)
132
- npx @event4u/create-agent-config init --tools=all --yes
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/create-agent-config init --ref=main --yes
160
+ npx @event4u/agent-config init --ref=main --yes
136
161
  ```
137
162
 
138
- The `@event4u/create-agent-config` package is a thin wrapper: it
139
- downloads the latest `@event4u/agent-config` tarball into a temp
140
- directory, runs `bash scripts/install --target <cwd> ...`, and cleans
141
- up after itself. The project-local payload package
142
- (`@event4u/agent-config`) is unchanged.
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/create-agent-config init --tools=claude-code,cursor
205
+ npx @event4u/agent-config init --tools=claude-code,cursor
181
206
  ```
182
207
 
183
- The wrapper downloads the latest `@event4u/agent-config` tarball into a
184
- temp dir, runs `scripts/install` with the selected tools, and cleans up
185
- afterwards. Nothing is added to `package.json`.
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
- New team members: run `composer install` (or `npm install`) open editor → done.
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/create-agent-config init --tools=claude-code,cursor
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
@@ -11,7 +11,7 @@ from the repo root for project conventions.
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npx @event4u/create-agent-config init --tools=aider
14
+ npx @event4u/agent-config init --tools=aider
15
15
  ```
16
16
 
17
17
  Populates:
@@ -15,7 +15,7 @@ projects to those paths during install.
15
15
 
16
16
  ```bash
17
17
  # Inside an existing repo:
18
- npx @event4u/create-agent-config init --tools=claude-code
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** — run `npx @event4u/agent-config init --tools=claude-code`
7
- > inside each project that should expose the skills/rules to Claude
8
- > Desktop. The package now ships as an npx-resolved runtime; the
9
- > retired `--global` symlink scheme has been removed.
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/create-agent-config init --tools=cline
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/create-agent-config init --tools=cline`. |
38
+ | `.clinerules` missing | Re-run `npx @event4u/agent-config init --tools=cline`. |
39
39
 
40
40
  ## Cross-references
41
41
 
@@ -11,7 +11,7 @@ repo root for project context.
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npx @event4u/create-agent-config init --tools=codex
14
+ npx @event4u/agent-config init --tools=codex
15
15
  ```
16
16
 
17
17
  Populates:
@@ -13,7 +13,7 @@ falls back to `AGENTS.md` where supported.
13
13
  ## Install
14
14
 
15
15
  ```bash
16
- npx @event4u/create-agent-config init --tools=copilot
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/create-agent-config init --tools=copilot`. |
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/create-agent-config init --tools=cursor
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/create-agent-config init --tools=cursor,claude-code
35
+ npx @event4u/agent-config init --tools=cursor,claude-code
36
36
  ```
37
37
 
38
38
  ## Global install
@@ -11,7 +11,7 @@ in the package's projection) for project context.
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npx @event4u/create-agent-config init --tools=gemini
14
+ npx @event4u/agent-config init --tools=gemini
15
15
  ```
16
16
 
17
17
  Populates:
@@ -19,7 +19,7 @@ The package ships **both**.
19
19
  ## Project install
20
20
 
21
21
  ```bash
22
- npx @event4u/create-agent-config init --tools=windsurf
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/create-agent-config init --tools=windsurf,claude-code,cursor
35
+ npx @event4u/agent-config init --tools=windsurf,claude-code,cursor
36
36
  ```
37
37
 
38
38
  ## Wave-8 frontmatter
@@ -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/create-agent-config init --tools=claude-code,cursor
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/create-agent-config init --tools=claude-code,cursor
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/create-agent-config init --tools=claude-code,cursor
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/create-agent-config init` from WSL.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -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:]))