@event4u/agent-config 1.41.1 → 2.0.0

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.
@@ -0,0 +1,98 @@
1
+ # Migration — v1 → v2 (npx-only runtime)
2
+
3
+ > **Status:** skeleton. The one-shot `npx @event4u/agent-config migrate`
4
+ > is implemented in P3.5 of
5
+ > [`road-to-portable-runtime-and-update-check`](../../agents/roadmaps/road-to-portable-runtime-and-update-check.md);
6
+ > this document tracks the user-facing cutover contract so consumers can
7
+ > rehearse the change before the command lands.
8
+
9
+ ## Why this change
10
+
11
+ v2 retires the local-install scheme (Composer dependency, npm
12
+ `postinstall`, `--global` symlink namespace under `~/.claude/`,
13
+ `~/.cursor/`, `~/.codeium/windsurf/`, `~/.config/agent-config/`) in
14
+ favour of an **npx-only runtime**. The trade-off and the council's Q1
15
+ rejection + the user's override are recorded in
16
+ [`docs/architecture.md` § Distribution model](../architecture.md#distribution-model--npx-only--version-pin-governance).
17
+
18
+ ## TL;DR
19
+
20
+ ```bash
21
+ npx @event4u/agent-config migrate
22
+ ```
23
+
24
+ One command, idempotent. Re-runs on an already-migrated repo do nothing.
25
+
26
+ ## What disappears from the consumer
27
+
28
+ | Path / entry | Reason |
29
+ |-------------------------------------------------------|-----------------------------------------|
30
+ | `composer.json` → `require-dev.event4u/agent-config` | No Composer dependency under v2. |
31
+ | `composer.lock` line for `event4u/agent-config` | Same; lockfile updated by Composer. |
32
+ | `package.json` → `devDependencies.@event4u/agent-config` | No npm dependency under v2. |
33
+ | `package-lock.json` / `pnpm-lock.yaml` entries | Same. |
34
+ | `node_modules/@event4u/agent-config/` | Removed by the package manager once the `package.json` entry is gone. |
35
+ | `vendor/event4u/agent-config/` | Removed by Composer once the require entry is gone. |
36
+ | `~/.claude/{rules,skills}/event4u/` | Retired `--global` namespace dir. |
37
+ | `~/.cursor/rules/imported/event4u/` | Same. |
38
+ | `~/.codeium/windsurf/global_workflows/event4u/` | Same. |
39
+ | `~/.config/agent-config/{rules,skills}/event4u/` | Same (fallback path). |
40
+ | Legacy `.gitignore` block lines marked `event4u/agent-config (legacy local install)` | Replaced by the v2 block written by `sync-gitignore`. |
41
+
42
+ The retired `templates/global-install-manifest.yml` shipped inside the
43
+ package and is gone in v2; consumers never carried it directly.
44
+
45
+ ## What appears in the consumer
46
+
47
+ | Path / entry | Owner / shape |
48
+ |-------------------------------------------------------|-----------------------------------------|
49
+ | `.agent-settings.yml` → `agent_config_version: "<pin>"` | Project version pin, reviewed in PRs. |
50
+ | `.agent-settings.yml` → `update_check:` block | Defaults shipped by `init`; opt-out per knob. |
51
+ | Updated `.gitignore` block | v2 entries written by `sync-gitignore`. |
52
+
53
+ The per-tool glue (`.claude/`, `.cursor/`, `.clinerules/`,
54
+ `.windsurfrules`, `GEMINI.md`, `.github/copilot-instructions.md`,
55
+ `.augment/`, `.vscode/settings.json`) keeps the same shape as v1 — only
56
+ the source that writes them changed (from `vendor/` /
57
+ `node_modules/` scripts to the npx-resolved runtime).
58
+
59
+ ## The `migrate` command — contract sketch
60
+
61
+ ```bash
62
+ npx @event4u/agent-config migrate # interactive, default
63
+ npx @event4u/agent-config migrate --dry-run # plan only, no writes
64
+ npx @event4u/agent-config migrate --yes # non-interactive
65
+ ```
66
+
67
+ Order of operations (locked once P3.5 lands):
68
+
69
+ 1. Detect pre-v2 markers: `composer.json` require entry,
70
+ `package.json` devDependency, `vendor/event4u/agent-config/`,
71
+ `node_modules/@event4u/agent-config/`, legacy `.gitignore` lines,
72
+ `~/.claude/{rules,skills}/event4u/` and siblings.
73
+ 2. Print the planned change set (file removals, file writes, pin
74
+ value). Stop here under `--dry-run`.
75
+ 3. Remove dependency entries via the appropriate package manager
76
+ (`composer remove`, `npm uninstall` / `pnpm remove` / `yarn remove`).
77
+ 4. Wipe the retired user-scope `event4u/` namespace dirs.
78
+ 5. Write / update `.agent-settings.yml` with the new shape +
79
+ `agent_config_version` pin.
80
+ 6. Re-run `sync-gitignore` to refresh the project `.gitignore` block.
81
+ 7. Print a one-screen post-migration verification list.
82
+
83
+ Idempotency: each step is a no-op when the v1 marker is absent. Re-runs
84
+ print *"already on v2 — nothing to do"* and exit 0.
85
+
86
+ ## Verification after migration
87
+
88
+ ```bash
89
+ npx @event4u/agent-config doctor # P3 — runtime sanity check
90
+ ```
91
+
92
+ Expected: pin resolved, no v1 markers detected, `update_check` reachable.
93
+
94
+ ## See also
95
+
96
+ - [`docs/architecture.md` § Distribution model](../architecture.md#distribution-model--npx-only--version-pin-governance) — Q1 council rejection + override + pin substitution.
97
+ - [`agents/roadmaps/road-to-portable-runtime-and-update-check.md`](../../agents/roadmaps/road-to-portable-runtime-and-update-check.md) — full delivery plan and acceptance criteria.
98
+ - [`docs/installation.md`](../installation.md) — v2 install reference.
@@ -29,23 +29,6 @@ Either form populates:
29
29
  - `CLAUDE.md` — agent root pointer (auto-loaded by Claude Code)
30
30
  - `.agent-settings.yml` — your per-project knobs (kept out of git)
31
31
 
32
- ## Global install (cross-project skills)
33
-
34
- ```bash
35
- npx @event4u/agent-config global --tools=claude-code
36
- ```
37
-
38
- Seeds `~/.claude/skills/` with the curated top-N skills from
39
- [`templates/global-install-manifest.yml`](../../../templates/global-install-manifest.yml).
40
- Available across every project on the machine; project-level files
41
- always take precedence.
42
-
43
- Uninstall:
44
-
45
- ```bash
46
- npx @event4u/agent-config global --uninstall
47
- ```
48
-
49
32
  ## Plugin marketplace (Claude Code 2026+)
50
33
 
51
34
  Claude Code 2026 supports plugin marketplaces via
@@ -3,56 +3,46 @@
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** — install the package globally with `--global` so the
7
- > kernel rules and the curated top-N skills land in `~/.claude/`,
8
- > then restart Claude Desktop. The slash-command menu picks them up
9
- > automatically.
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.
10
10
 
11
11
  ## Prerequisites
12
12
 
13
13
  - Claude Desktop installed (free or paid plan — same install path).
14
- - Node ≥ 18 *or* a clone of the `event4u/agent-config` repo
15
- (either route can run `--global`).
14
+ - Node ≥ 18 (`npx` resolves the package per-project).
16
15
  - 5 minutes.
17
16
 
18
- ## Step 1 — global install
17
+ ## Step 1 — project-local install
19
18
 
20
- Pick whichever entrypoint matches your environment. Both seed the same
21
- files under `~/.claude/`.
19
+ Run inside each project that should be visible to Claude Desktop:
22
20
 
23
21
  ```bash
24
- # Node no clone needed.
25
- npx @event4u/create-agent-config --global --tools=claude-code
26
-
27
- # Or via curl (no Node).
28
- curl -fsSL https://raw.githubusercontent.com/event4u/agent-config/main/setup.sh \
29
- | bash -s -- --global --tools=claude-code
30
-
31
- # Or from a local clone.
32
- bash scripts/install --global --tools=claude-code
22
+ npx @event4u/agent-config init --tools=claude-code
33
23
  ```
34
24
 
35
25
  > `--tools=claude-code` covers both Claude Code **and** Claude
36
- > Desktop — the two surfaces share `~/.claude/`. Pass
37
- > `--tools=claude-code,cursor,windsurf` if you want Cursor / Windsurf
38
- > globally seeded in the same run.
26
+ > Desktop — the two surfaces share the project's `.claude/`
27
+ > directory. Pass `--tools=claude-code,cursor,windsurf` to seed
28
+ > additional surfaces in the same run.
39
29
 
40
- After the install you'll have:
30
+ The init writes:
41
31
 
42
32
  ```
43
- ~/.claude/
44
- ├── rules/event4u/ # 9 kernel rules (Iron-Law set)
45
- └── skills/event4u/ # 15 curated top-N skills
33
+ .claude/
34
+ ├── rules/ # active rules for the project
35
+ ├── skills/ # active skills for the project
36
+ └── commands/ # slash commands
46
37
  ```
47
38
 
48
- Curation lives in
49
- [`templates/global-install-manifest.yml`](../../../templates/global-install-manifest.yml).
50
- Edit and re-run `--global` to grow or shrink the set.
39
+ `.agent-settings.yml` carries the `agent_config_version` pin so every
40
+ `npx` invocation resolves the same runtime.
51
41
 
52
42
  ## Step 2 — verify
53
43
 
54
44
  1. Restart Claude Desktop (full quit, not just window close).
55
- 2. Open a new conversation.
45
+ 2. Open the project folder in a new conversation.
56
46
  3. Type `/` — the curated skills (`/work`, `/commit`, `/create-pr`,
57
47
  `/quality-fix`, `/review-changes`, `/agent-handoff`,
58
48
  `/project-analyze`, …) appear in the slash-command menu.
@@ -61,7 +51,8 @@ Edit and re-run `--global` to grow or shrink the set.
61
51
 
62
52
  If the menu is empty:
63
53
 
64
- - Check `ls ~/.claude/skills/event4u/` — should list 15 directories.
54
+ - Check `ls .claude/skills/` inside the project — should list the
55
+ curated skills.
65
56
  - Quit Claude Desktop (`Cmd+Q` on macOS, **not** just close the
66
57
  window — the menubar process keeps the old skills cached).
67
58
  - Re-open and try `/` again.
@@ -123,22 +114,23 @@ native HTTP) and per-client Bearer-auth snippets live in
123
114
 
124
115
  ## Claude Desktop ↔ Claude Code config sharing
125
116
 
126
- Both surfaces read **the same `~/.claude/` directory**. Anything you
127
- install for one is automatically available in the other:
117
+ Both surfaces read **the same project `.claude/` directory**. Anything
118
+ the `npx … init` writes for one is automatically picked up by the
119
+ other when the project folder is opened:
128
120
 
129
121
  | File / dir | Shared by Desktop & Code? |
130
122
  | -------------------------------- | ------------------------- |
131
- | `~/.claude/CLAUDE.md` | yes — global system prompt |
132
- | `~/.claude/rules/event4u/` | yes — installed by `--global` |
133
- | `~/.claude/skills/event4u/` | yes — installed by `--global` |
134
- | `~/.claude/commands/` | yes — slash commands |
135
- | `~/.claude/hooks/` | yes — lifecycle hooks |
123
+ | `<project>/.claude/CLAUDE.md` | yes — project system prompt |
124
+ | `<project>/.claude/rules/` | yes — written by `npx … init` |
125
+ | `<project>/.claude/skills/` | yes — written by `npx … init` |
126
+ | `<project>/.claude/commands/` | yes — slash commands |
127
+ | `<project>/.claude/hooks/` | yes — lifecycle hooks |
136
128
  | `claude_desktop_config.json` | Desktop only (MCP) |
137
129
  | `~/.claude.json` (CLI config) | Code only |
138
130
 
139
- Translation: run `--global` once, both clients pick the files up.
140
- Cross-link to [`claude-code.md`](claude-code.md) for the CLI-side
141
- view.
131
+ Translation: run `npx @event4u/agent-config init` once per project,
132
+ both clients pick the files up. Cross-link to
133
+ [`claude-code.md`](claude-code.md) for the CLI-side view.
142
134
 
143
135
  ## Claude Cowork
144
136
 
@@ -158,16 +150,11 @@ client-side feature set.
158
150
 
159
151
  ## Uninstall
160
152
 
161
- ```bash
162
- bash scripts/install --global --uninstall --tools=claude-code
163
- ```
164
-
165
- Removes only `~/.claude/{rules,skills}/event4u/`. Anything you added
166
- under sibling paths (custom rules, your own slash commands) stays.
153
+ Remove the project's `.claude/`, `.agent-settings.yml`, and any bridge
154
+ files written by `npx … init`. Nothing lives under `~/.claude/` from
155
+ this package any more.
167
156
 
168
157
  ## See also
169
158
 
170
159
  - Project-local install — [`../../installation.md`](../../installation.md)
171
- - Global install reference — [`../../installation.md#global-user-level-install---global`](../../installation.md#global-user-level-install---global)
172
160
  - MCP client transports — [`../mcp-client-config.md`](../mcp-client-config.md)
173
- - Curation manifest — [`../../../templates/global-install-manifest.yml`](../../../templates/global-install-manifest.yml)
@@ -35,17 +35,6 @@ Combine with other surfaces:
35
35
  npx @event4u/create-agent-config init --tools=windsurf,claude-code,cursor
36
36
  ```
37
37
 
38
- ## Global install
39
-
40
- ```bash
41
- npx @event4u/agent-config global --tools=windsurf
42
- ```
43
-
44
- Seeds `~/.codeium/windsurf/global_workflows/` with the curated
45
- workflow set (see [`templates/global-install-manifest.yml`](../../../templates/global-install-manifest.yml)).
46
- Available across every project; per-workspace `.windsurf/workflows/`
47
- takes precedence on slug collisions.
48
-
49
38
  ## Wave-8 frontmatter
50
39
 
51
40
  Each rule under `.windsurf/rules/` has the Windsurf-shaped header:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "1.41.1",
3
+ "version": "2.0.0",
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,
@@ -16,7 +16,6 @@
16
16
  ".agent-src/",
17
17
  ".augment-plugin/",
18
18
  ".claude-plugin/",
19
- "bin/",
20
19
  "config/",
21
20
  "docs/",
22
21
  "scripts/",
@@ -26,15 +25,11 @@
26
25
  "CONTRIBUTING.md",
27
26
  "LICENSE",
28
27
  "README.md",
29
- "composer.json",
30
28
  "llms.txt"
31
29
  ],
32
30
  "bin": {
33
31
  "agent-config": "scripts/agent-config"
34
32
  },
35
- "scripts": {
36
- "postinstall": "bash scripts/postinstall.sh"
37
- },
38
33
  "publishConfig": {
39
34
  "access": "public",
40
35
  "provenance": true
File without changes
@@ -0,0 +1,270 @@
1
+ """``agent-config migrate`` — one-shot migration off legacy install paths.
2
+
3
+ P3.5/P3.6 of road-to-portable-runtime-and-update-check.md. Migrates a
4
+ consumer project from the legacy composer / npm install paths onto
5
+ the ``npx``-only runtime described in ``docs/architecture.md``.
6
+
7
+ Steps performed (idempotent):
8
+
9
+ 1. Detect legacy install signals (composer.json entry, package.json
10
+ devDependency, in-project symlinks pointing at vendor/ or
11
+ node_modules/).
12
+ 2. Remove the package entry from composer.json / package.json
13
+ in-place, preserving sibling keys + formatting.
14
+ 3. Delete agent-config managed symlinks that point inside the legacy
15
+ install dirs. User-added links elsewhere are preserved with a
16
+ warning.
17
+ 4. Write a fresh ``.agent-settings.yml`` (only if missing) with
18
+ ``agent_config_version`` pinned to the running version.
19
+ 5. Update the consumer's ``.gitignore`` block (legacy paths out, new
20
+ project-scope entries in).
21
+ 6. Print a summary so the developer can review + commit.
22
+
23
+ Re-runs on an already-migrated repo emit ``already migrated`` and
24
+ exit 0.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import re
31
+ import sys
32
+ from pathlib import Path
33
+ from typing import Iterable, Optional
34
+
35
+ PACKAGE_NAME_NPM = "@event4u/agent-config"
36
+ PACKAGE_NAME_COMPOSER = "event4u/agent-config"
37
+ LEGACY_DIRS = ("vendor", "node_modules")
38
+ MANAGED_SYMLINKS = (
39
+ ".augment",
40
+ ".claude",
41
+ ".cursor",
42
+ ".clinerules",
43
+ ".windsurfrules",
44
+ )
45
+ GITIGNORE_BLOCK_START = "# >>> event4u/agent-config (managed) >>>"
46
+ GITIGNORE_BLOCK_END = "# <<< event4u/agent-config (managed) <<<"
47
+ GITIGNORE_NEW_BODY = (
48
+ ".agent-settings.yml\n"
49
+ "agents/sessions/\n"
50
+ "agents/council-responses/\n"
51
+ "agents/council-sessions/\n"
52
+ )
53
+
54
+
55
+ def _detect_npm(pkg_json: Path) -> bool:
56
+ if not pkg_json.is_file():
57
+ return False
58
+ try:
59
+ data = json.loads(pkg_json.read_text(encoding="utf-8"))
60
+ except (OSError, ValueError, json.JSONDecodeError):
61
+ return False
62
+ for key in ("dependencies", "devDependencies"):
63
+ section = data.get(key) or {}
64
+ if isinstance(section, dict) and PACKAGE_NAME_NPM in section:
65
+ return True
66
+ return False
67
+
68
+
69
+ def _detect_composer(composer_json: Path) -> bool:
70
+ if not composer_json.is_file():
71
+ return False
72
+ try:
73
+ data = json.loads(composer_json.read_text(encoding="utf-8"))
74
+ except (OSError, ValueError, json.JSONDecodeError):
75
+ return False
76
+ for key in ("require", "require-dev"):
77
+ section = data.get(key) or {}
78
+ if isinstance(section, dict) and PACKAGE_NAME_COMPOSER in section:
79
+ return True
80
+ return False
81
+
82
+
83
+ def _strip_npm_entry(pkg_json: Path) -> bool:
84
+ try:
85
+ data = json.loads(pkg_json.read_text(encoding="utf-8"))
86
+ except (OSError, ValueError, json.JSONDecodeError):
87
+ return False
88
+ changed = False
89
+ for key in ("dependencies", "devDependencies"):
90
+ section = data.get(key)
91
+ if isinstance(section, dict) and PACKAGE_NAME_NPM in section:
92
+ del section[PACKAGE_NAME_NPM]
93
+ changed = True
94
+ if not section:
95
+ del data[key]
96
+ if changed:
97
+ pkg_json.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
98
+ return changed
99
+
100
+
101
+ def _strip_composer_entry(composer_json: Path) -> bool:
102
+ try:
103
+ data = json.loads(composer_json.read_text(encoding="utf-8"))
104
+ except (OSError, ValueError, json.JSONDecodeError):
105
+ return False
106
+ changed = False
107
+ for key in ("require", "require-dev"):
108
+ section = data.get(key)
109
+ if isinstance(section, dict) and PACKAGE_NAME_COMPOSER in section:
110
+ del section[PACKAGE_NAME_COMPOSER]
111
+ changed = True
112
+ if not section:
113
+ del data[key]
114
+ if changed:
115
+ composer_json.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
116
+ return changed
117
+
118
+
119
+ def _classify_symlink(link: Path) -> Optional[str]:
120
+ """Return 'legacy' if the link points into vendor/ or node_modules/, 'user' otherwise."""
121
+ if not link.is_symlink():
122
+ return None
123
+ try:
124
+ target = Path(link.readlink()) if hasattr(link, "readlink") else Path(link.resolve())
125
+ except OSError:
126
+ return None
127
+ target_str = str(target)
128
+ if any(seg in target_str.split("/") for seg in LEGACY_DIRS):
129
+ return "legacy"
130
+ return "user"
131
+
132
+
133
+ def _purge_legacy_symlinks(project: Path) -> tuple[list[str], list[str]]:
134
+ removed: list[str] = []
135
+ preserved: list[str] = []
136
+ for name in MANAGED_SYMLINKS:
137
+ link = project / name
138
+ kind = _classify_symlink(link)
139
+ if kind == "legacy":
140
+ try:
141
+ link.unlink()
142
+ removed.append(name)
143
+ except OSError:
144
+ preserved.append(name)
145
+ elif kind == "user":
146
+ preserved.append(name)
147
+ return removed, preserved
148
+
149
+
150
+ def _write_settings(project: Path, version: str) -> bool:
151
+ settings = project / ".agent-settings.yml"
152
+ if settings.exists():
153
+ return False
154
+ body = (
155
+ "# .agent-settings.yml — generated by `agent-config migrate`.\n"
156
+ "# See docs/customization.md for the full key reference.\n"
157
+ f'agent_config_version: "{version}"\n'
158
+ )
159
+ settings.write_text(body, encoding="utf-8")
160
+ try:
161
+ settings.chmod(0o644)
162
+ except OSError:
163
+ pass
164
+ return True
165
+
166
+
167
+ def _update_gitignore(project: Path) -> bool:
168
+ gitignore = project / ".gitignore"
169
+ block = (
170
+ f"{GITIGNORE_BLOCK_START}\n"
171
+ f"{GITIGNORE_NEW_BODY}"
172
+ f"{GITIGNORE_BLOCK_END}\n"
173
+ )
174
+ if not gitignore.exists():
175
+ gitignore.write_text(block, encoding="utf-8")
176
+ return True
177
+
178
+ text = gitignore.read_text(encoding="utf-8")
179
+ pattern = re.compile(
180
+ re.escape(GITIGNORE_BLOCK_START) + r".*?" + re.escape(GITIGNORE_BLOCK_END) + r"\n?",
181
+ re.DOTALL,
182
+ )
183
+ if pattern.search(text):
184
+ new_text = pattern.sub(block, text)
185
+ else:
186
+ new_text = text
187
+ if new_text and not new_text.endswith("\n"):
188
+ new_text += "\n"
189
+ new_text += block
190
+ if new_text == text:
191
+ return False
192
+ gitignore.write_text(new_text, encoding="utf-8")
193
+ return True
194
+
195
+
196
+ def _detect_already_migrated(project: Path) -> bool:
197
+ """A repo counts as migrated when no legacy signal remains."""
198
+ if _detect_npm(project / "package.json"):
199
+ return False
200
+ if _detect_composer(project / "composer.json"):
201
+ return False
202
+ for name in MANAGED_SYMLINKS:
203
+ if _classify_symlink(project / name) == "legacy":
204
+ return False
205
+ return True
206
+
207
+
208
+ def main(
209
+ argv: Optional[list[str]] = None,
210
+ *,
211
+ cwd: Optional[Path] = None,
212
+ version: Optional[str] = None,
213
+ out=sys.stdout,
214
+ err=sys.stderr, # noqa: ARG001 — reserved for future error paths
215
+ ) -> int:
216
+ parser = argparse.ArgumentParser(
217
+ prog="agent-config migrate",
218
+ description="One-shot migration off legacy composer / npm install paths.",
219
+ )
220
+ parser.add_argument("--dry-run", action="store_true",
221
+ help="Detect only; do not write any files.")
222
+ args = parser.parse_args(argv)
223
+
224
+ project = (cwd or Path.cwd()).resolve()
225
+ version = version or _detect_installed_version()
226
+
227
+ if _detect_already_migrated(project):
228
+ print("✅ already migrated — nothing to do.", file=out)
229
+ return 0
230
+
231
+ if args.dry_run:
232
+ print("ℹ️ legacy install detected — re-run without --dry-run to migrate.", file=out)
233
+ return 0
234
+
235
+ summary: list[str] = []
236
+ if _strip_npm_entry(project / "package.json"):
237
+ summary.append(f"removed {PACKAGE_NAME_NPM} from package.json")
238
+ if _strip_composer_entry(project / "composer.json"):
239
+ summary.append(f"removed {PACKAGE_NAME_COMPOSER} from composer.json")
240
+ removed_links, preserved_links = _purge_legacy_symlinks(project)
241
+ for name in removed_links:
242
+ summary.append(f"removed legacy symlink {name}")
243
+ for name in preserved_links:
244
+ summary.append(f"preserved user-managed {name} (review manually)")
245
+ if _write_settings(project, version):
246
+ summary.append(f".agent-settings.yml written (pinned to {version})")
247
+ if _update_gitignore(project):
248
+ summary.append(".gitignore agent-config block refreshed")
249
+
250
+ print("✅ migration complete:", file=out)
251
+ for line in summary:
252
+ print(f" - {line}", file=out)
253
+ print("\n Next: review the diff and commit.", file=out)
254
+ return 0
255
+
256
+
257
+ def _detect_installed_version() -> str:
258
+ pkg_json = Path(__file__).resolve().parents[2] / "package.json"
259
+ try:
260
+ data = json.loads(pkg_json.read_text(encoding="utf-8"))
261
+ version = data.get("version")
262
+ if isinstance(version, str) and version.strip():
263
+ return version.strip()
264
+ except (OSError, ValueError, json.JSONDecodeError):
265
+ pass
266
+ return "0.0.0"
267
+
268
+
269
+ if __name__ == "__main__": # pragma: no cover
270
+ sys.exit(main())