@event4u/agent-config 4.8.0 → 5.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.
- package/.agent-src/commands/implement-ticket.md +5 -4
- package/.agent-src/rules/language-and-tone.md +4 -10
- package/.agent-src/skills/command-routing/SKILL.md +5 -4
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +86 -0
- package/CONTRIBUTING.md +19 -0
- package/README.md +11 -0
- package/dist/cli/registry.js +0 -2
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +5 -5
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/router.json +1 -1671
- package/docs/benchmark.md +20 -8
- package/docs/benchmarks.md +11 -0
- package/docs/contracts/benchmark-corpus-spec.md +31 -3
- package/docs/contracts/command-surface-tiers.md +1 -1
- package/docs/contracts/hook-architecture-v1.md +33 -0
- package/docs/contracts/migrate-command.md +197 -0
- package/docs/contracts/settings-api.md +2 -1
- package/docs/contracts/value-dashboard-spec.md +374 -0
- package/docs/contracts/value-report-schema.md +150 -0
- package/docs/decisions/ADR-031-validation-severity-tiers-and-projection-roundtrip.md +97 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +6 -3
- package/docs/guidelines/agent-infra/language-and-tone-examples.md +35 -0
- package/docs/migration/v1-to-v2.md +40 -27
- package/docs/value.md +84 -0
- package/package.json +8 -8
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_migrate.py +264 -102
- package/scripts/_cli/cmd_settings_migrate.py +2 -1
- package/scripts/_dispatch.bash +147 -49
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/install_regenerator.py +129 -0
- package/scripts/_lib/value_ladder.py +599 -0
- package/scripts/_lib/value_report.py +441 -0
- package/scripts/bench_rtk_savings.py +320 -0
- package/scripts/compile_router.py +19 -5
- package/scripts/expected_perms.json +1 -1
- package/scripts/first_run_gate_hook.py +178 -0
- package/scripts/hook_manifest.yaml +16 -7
- package/scripts/hooks/dispatch_hook.py +27 -0
- package/scripts/hooks/dispatch_issues.py +136 -0
- package/scripts/hooks_doctor.py +40 -1
- package/scripts/install.py +25 -21
- package/scripts/inventory_abstraction_budget.py +616 -0
- package/scripts/lint_agents_layout.py +5 -4
- package/scripts/lint_bench_corpus.py +86 -4
- package/scripts/lint_global_paths.py +4 -3
- package/scripts/lint_marketplace_install_completeness.py +188 -0
- package/scripts/lint_value_dashboard.py +218 -0
- package/scripts/render_benchmark_md.py +6 -2
- package/scripts/render_value_md.py +355 -0
- package/scripts/repro/repro_marketplace_install_gap.sh +161 -0
- package/scripts/roadmap_progress_hook.py +23 -0
- package/scripts/router_telemetry.py +470 -0
- package/scripts/validate_frontmatter.py +23 -9
- package/scripts/_cli/cmd_migrate_to_global.py +0 -415
|
@@ -1,27 +1,37 @@
|
|
|
1
|
-
"""``agent-config migrate`` — one-shot migration off
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
``
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
"""``agent-config migrate`` — one-shot, opinionated migration off every
|
|
2
|
+
legacy install / state shape.
|
|
3
|
+
|
|
4
|
+
Contract: ``docs/contracts/migrate-command.md``.
|
|
5
|
+
|
|
6
|
+
Source roadmap: ``agents/roadmaps/road-to-one-migrate-command.md``. The
|
|
7
|
+
unified command collapses the legacy ``migrate``, ``migrate-state``,
|
|
8
|
+
and ``migrate-to-global`` triplet into a single, opinionated entry
|
|
9
|
+
point. The only flag is ``--dry-run`` (preview vs. apply).
|
|
10
|
+
|
|
11
|
+
Apply order (fixed; foundation-first):
|
|
12
|
+
|
|
13
|
+
1. Strip ``@event4u/agent-config`` from ``package.json``
|
|
14
|
+
(``dependencies`` / ``devDependencies``).
|
|
15
|
+
2. Strip ``event4u/agent-config`` from ``composer.json``
|
|
16
|
+
(``require`` / ``require-dev``).
|
|
17
|
+
3. Delete managed symlinks (``.augment``, ``.claude``, ``.cursor``,
|
|
18
|
+
``.clinerules``, ``.windsurfrules``) whose target points into a
|
|
19
|
+
legacy install dir (``vendor/`` or ``node_modules/``). Preserve
|
|
20
|
+
user-managed symlinks pointing elsewhere with a warning.
|
|
21
|
+
4. Migrate ``.implement-ticket-state.json`` → ``.work-state.json`` if
|
|
22
|
+
a v0 payload is present (the v0 source is renamed ``.bak``).
|
|
23
|
+
5. Hard-delete legacy project-local config:
|
|
24
|
+
``.agent-settings.yml``, ``.agent-user.yml``,
|
|
25
|
+
``settings/.agent-settings.yml``, ``settings/.agent-user.yml``.
|
|
26
|
+
Remove the ``settings/`` directory if it becomes empty.
|
|
27
|
+
6. Remove the empty ``agent-config/`` shell directory at the project
|
|
28
|
+
root, if present and empty.
|
|
29
|
+
7. Refresh the ``.gitignore`` agent-config managed block to the
|
|
30
|
+
canonical shape.
|
|
31
|
+
|
|
32
|
+
Re-runs on a fully-migrated repo emit ``already migrated`` and exit 0
|
|
33
|
+
without touching the filesystem. ``--dry-run`` runs the same
|
|
34
|
+
detection and prints what would change without mutating disk.
|
|
25
35
|
"""
|
|
26
36
|
from __future__ import annotations
|
|
27
37
|
|
|
@@ -30,7 +40,7 @@ import json
|
|
|
30
40
|
import re
|
|
31
41
|
import sys
|
|
32
42
|
from pathlib import Path
|
|
33
|
-
from typing import
|
|
43
|
+
from typing import Optional
|
|
34
44
|
|
|
35
45
|
from scripts._lib.agent_settings import resolve_project_root
|
|
36
46
|
|
|
@@ -52,8 +62,14 @@ GITIGNORE_NEW_BODY = (
|
|
|
52
62
|
"agents/runtime/council/responses/\n"
|
|
53
63
|
"agents/runtime/council/sessions/\n"
|
|
54
64
|
)
|
|
65
|
+
LEGACY_SETTINGS_FILES = (".agent-settings.yml", ".agent-user.yml")
|
|
66
|
+
LEGACY_STATE_FILENAME = ".implement-ticket-state.json"
|
|
67
|
+
LEGACY_STATE_V1_FILENAME = ".work-state.json"
|
|
68
|
+
LEGACY_AGENT_CONFIG_SHELL = "agent-config"
|
|
55
69
|
|
|
56
70
|
|
|
71
|
+
# ---------- detection ----------
|
|
72
|
+
|
|
57
73
|
def _detect_npm(pkg_json: Path) -> bool:
|
|
58
74
|
if not pkg_json.is_file():
|
|
59
75
|
return False
|
|
@@ -82,6 +98,69 @@ def _detect_composer(composer_json: Path) -> bool:
|
|
|
82
98
|
return False
|
|
83
99
|
|
|
84
100
|
|
|
101
|
+
def _classify_symlink(link: Path) -> Optional[str]:
|
|
102
|
+
"""Return 'legacy' if the link points into vendor/ or node_modules/, 'user' otherwise."""
|
|
103
|
+
if not link.is_symlink():
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
target = Path(link.readlink()) if hasattr(link, "readlink") else Path(link.resolve())
|
|
107
|
+
except OSError:
|
|
108
|
+
return None
|
|
109
|
+
target_str = str(target)
|
|
110
|
+
if any(seg in target_str.split("/") for seg in LEGACY_DIRS):
|
|
111
|
+
return "legacy"
|
|
112
|
+
return "user"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _detect_legacy_state(project: Path) -> bool:
|
|
116
|
+
"""A v0 state file is present at the project root."""
|
|
117
|
+
return (project / LEGACY_STATE_FILENAME).is_file()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _detect_legacy_settings(project: Path) -> list[Path]:
|
|
121
|
+
"""Return the list of legacy settings files present, in deletion order."""
|
|
122
|
+
found: list[Path] = []
|
|
123
|
+
for name in LEGACY_SETTINGS_FILES:
|
|
124
|
+
flat = project / name
|
|
125
|
+
if flat.is_file():
|
|
126
|
+
found.append(flat)
|
|
127
|
+
typed = project / "settings" / name
|
|
128
|
+
if typed.is_file():
|
|
129
|
+
found.append(typed)
|
|
130
|
+
return found
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _detect_empty_shell(project: Path) -> bool:
|
|
134
|
+
"""An empty ``agent-config/`` directory at the project root."""
|
|
135
|
+
shell = project / LEGACY_AGENT_CONFIG_SHELL
|
|
136
|
+
if not shell.is_dir() or shell.is_symlink():
|
|
137
|
+
return False
|
|
138
|
+
try:
|
|
139
|
+
return not any(shell.iterdir())
|
|
140
|
+
except OSError:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _detect_already_migrated(project: Path) -> bool:
|
|
145
|
+
"""A repo counts as migrated when no legacy signal remains."""
|
|
146
|
+
if _detect_npm(project / "package.json"):
|
|
147
|
+
return False
|
|
148
|
+
if _detect_composer(project / "composer.json"):
|
|
149
|
+
return False
|
|
150
|
+
for name in MANAGED_SYMLINKS:
|
|
151
|
+
if _classify_symlink(project / name) == "legacy":
|
|
152
|
+
return False
|
|
153
|
+
if _detect_legacy_state(project):
|
|
154
|
+
return False
|
|
155
|
+
if _detect_legacy_settings(project):
|
|
156
|
+
return False
|
|
157
|
+
if _detect_empty_shell(project):
|
|
158
|
+
return False
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------- apply primitives ----------
|
|
163
|
+
|
|
85
164
|
def _strip_npm_entry(pkg_json: Path) -> bool:
|
|
86
165
|
try:
|
|
87
166
|
data = json.loads(pkg_json.read_text(encoding="utf-8"))
|
|
@@ -118,20 +197,6 @@ def _strip_composer_entry(composer_json: Path) -> bool:
|
|
|
118
197
|
return changed
|
|
119
198
|
|
|
120
199
|
|
|
121
|
-
def _classify_symlink(link: Path) -> Optional[str]:
|
|
122
|
-
"""Return 'legacy' if the link points into vendor/ or node_modules/, 'user' otherwise."""
|
|
123
|
-
if not link.is_symlink():
|
|
124
|
-
return None
|
|
125
|
-
try:
|
|
126
|
-
target = Path(link.readlink()) if hasattr(link, "readlink") else Path(link.resolve())
|
|
127
|
-
except OSError:
|
|
128
|
-
return None
|
|
129
|
-
target_str = str(target)
|
|
130
|
-
if any(seg in target_str.split("/") for seg in LEGACY_DIRS):
|
|
131
|
-
return "legacy"
|
|
132
|
-
return "user"
|
|
133
|
-
|
|
134
|
-
|
|
135
200
|
def _purge_legacy_symlinks(project: Path) -> tuple[list[str], list[str]]:
|
|
136
201
|
removed: list[str] = []
|
|
137
202
|
preserved: list[str] = []
|
|
@@ -149,20 +214,77 @@ def _purge_legacy_symlinks(project: Path) -> tuple[list[str], list[str]]:
|
|
|
149
214
|
return removed, preserved
|
|
150
215
|
|
|
151
216
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
217
|
+
def _migrate_state_file(project: Path) -> Optional[str]:
|
|
218
|
+
"""Migrate ``.implement-ticket-state.json`` if v0; return a summary line or None.
|
|
219
|
+
|
|
220
|
+
Raises on conversion error so the caller can surface a non-zero exit.
|
|
221
|
+
"""
|
|
222
|
+
source = project / LEGACY_STATE_FILENAME
|
|
223
|
+
if not source.is_file():
|
|
224
|
+
return None
|
|
225
|
+
target = project / LEGACY_STATE_V1_FILENAME
|
|
226
|
+
if target.exists():
|
|
227
|
+
# Migration already happened; just clean up the v0 source.
|
|
228
|
+
try:
|
|
229
|
+
source.unlink()
|
|
230
|
+
return f"removed stale {LEGACY_STATE_FILENAME} (v1 already present)"
|
|
231
|
+
except OSError:
|
|
232
|
+
return None
|
|
233
|
+
migrator = _load_state_migrator()
|
|
234
|
+
if migrator is None:
|
|
235
|
+
return None
|
|
236
|
+
migrator.migrate_file(source, destination=target, backup=True)
|
|
237
|
+
return f"migrated {LEGACY_STATE_FILENAME} → {LEGACY_STATE_V1_FILENAME}"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _load_state_migrator():
|
|
241
|
+
"""Import the v0→v1 state migrator from the shipped engine."""
|
|
242
|
+
pkg_root = Path(__file__).resolve().parents[2]
|
|
243
|
+
engine_root = pkg_root / ".agent-src" / "templates" / "scripts"
|
|
244
|
+
if not (engine_root / "work_engine" / "migration").is_dir():
|
|
245
|
+
return None
|
|
246
|
+
if str(engine_root) not in sys.path:
|
|
247
|
+
sys.path.insert(0, str(engine_root))
|
|
248
|
+
try:
|
|
249
|
+
from work_engine.migration import v0_to_v1 # noqa: PLC0415
|
|
250
|
+
except ImportError:
|
|
251
|
+
return None
|
|
252
|
+
return v0_to_v1
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _delete_legacy_settings(project: Path) -> list[str]:
|
|
256
|
+
"""Hard-delete every legacy settings file under ``project``.
|
|
257
|
+
|
|
258
|
+
Returns the list of relative paths actually removed. Removes the
|
|
259
|
+
``settings/`` directory itself if it becomes empty after the YAML
|
|
260
|
+
sweep.
|
|
261
|
+
"""
|
|
262
|
+
removed: list[str] = []
|
|
263
|
+
for path in _detect_legacy_settings(project):
|
|
264
|
+
try:
|
|
265
|
+
path.unlink()
|
|
266
|
+
removed.append(str(path.relative_to(project)))
|
|
267
|
+
except OSError:
|
|
268
|
+
continue
|
|
269
|
+
settings_dir = project / "settings"
|
|
270
|
+
if settings_dir.is_dir() and not settings_dir.is_symlink():
|
|
271
|
+
try:
|
|
272
|
+
if not any(settings_dir.iterdir()):
|
|
273
|
+
settings_dir.rmdir()
|
|
274
|
+
removed.append("settings/")
|
|
275
|
+
except OSError:
|
|
276
|
+
pass
|
|
277
|
+
return removed
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _remove_empty_shell(project: Path) -> bool:
|
|
281
|
+
shell = project / LEGACY_AGENT_CONFIG_SHELL
|
|
282
|
+
if not _detect_empty_shell(project):
|
|
155
283
|
return False
|
|
156
|
-
body = (
|
|
157
|
-
"# .agent-settings.yml — generated by `agent-config migrate`.\n"
|
|
158
|
-
"# See docs/customization.md for the full key reference.\n"
|
|
159
|
-
f'agent_config_version: "{version}"\n'
|
|
160
|
-
)
|
|
161
|
-
settings.write_text(body, encoding="utf-8")
|
|
162
284
|
try:
|
|
163
|
-
|
|
285
|
+
shell.rmdir()
|
|
164
286
|
except OSError:
|
|
165
|
-
|
|
287
|
+
return False
|
|
166
288
|
return True
|
|
167
289
|
|
|
168
290
|
|
|
@@ -195,48 +317,54 @@ def _update_gitignore(project: Path) -> bool:
|
|
|
195
317
|
return True
|
|
196
318
|
|
|
197
319
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
320
|
+
# ---------- plan + apply ----------
|
|
321
|
+
|
|
322
|
+
def _build_plan(project: Path) -> dict:
|
|
323
|
+
"""Return a dict describing every detected legacy signal."""
|
|
324
|
+
return {
|
|
325
|
+
"npm": _detect_npm(project / "package.json"),
|
|
326
|
+
"composer": _detect_composer(project / "composer.json"),
|
|
327
|
+
"symlinks_legacy": [
|
|
328
|
+
name for name in MANAGED_SYMLINKS
|
|
329
|
+
if _classify_symlink(project / name) == "legacy"
|
|
330
|
+
],
|
|
331
|
+
"symlinks_user": [
|
|
332
|
+
name for name in MANAGED_SYMLINKS
|
|
333
|
+
if _classify_symlink(project / name) == "user"
|
|
334
|
+
],
|
|
335
|
+
"state_file": (project / LEGACY_STATE_FILENAME).is_file(),
|
|
336
|
+
"settings_files": [
|
|
337
|
+
str(p.relative_to(project)) for p in _detect_legacy_settings(project)
|
|
338
|
+
],
|
|
339
|
+
"empty_shell": _detect_empty_shell(project),
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _format_dry_run(plan: dict, out) -> None:
|
|
344
|
+
lines: list[str] = []
|
|
345
|
+
if plan["npm"]:
|
|
346
|
+
lines.append(f"would remove {PACKAGE_NAME_NPM} from package.json")
|
|
347
|
+
if plan["composer"]:
|
|
348
|
+
lines.append(f"would remove {PACKAGE_NAME_COMPOSER} from composer.json")
|
|
349
|
+
for name in plan["symlinks_legacy"]:
|
|
350
|
+
lines.append(f"would remove legacy symlink {name}")
|
|
351
|
+
for name in plan["symlinks_user"]:
|
|
352
|
+
lines.append(f"would preserve user-managed {name} (review manually)")
|
|
353
|
+
if plan["state_file"]:
|
|
354
|
+
lines.append(
|
|
355
|
+
f"would migrate {LEGACY_STATE_FILENAME} → {LEGACY_STATE_V1_FILENAME}"
|
|
356
|
+
)
|
|
357
|
+
for rel in plan["settings_files"]:
|
|
358
|
+
lines.append(f"would delete legacy config {rel}")
|
|
359
|
+
if plan["empty_shell"]:
|
|
360
|
+
lines.append(f"would remove empty {LEGACY_AGENT_CONFIG_SHELL}/ shell")
|
|
361
|
+
lines.append("would refresh .gitignore agent-config block")
|
|
362
|
+
print("ℹ️ legacy install detected — re-run without --dry-run to migrate:", file=out)
|
|
363
|
+
for line in lines:
|
|
364
|
+
print(f" - {line}", file=out)
|
|
235
365
|
|
|
236
|
-
if args.dry_run:
|
|
237
|
-
print("ℹ️ legacy install detected — re-run without --dry-run to migrate.", file=out)
|
|
238
|
-
return 0
|
|
239
366
|
|
|
367
|
+
def _apply(project: Path, out) -> int:
|
|
240
368
|
summary: list[str] = []
|
|
241
369
|
if _strip_npm_entry(project / "package.json"):
|
|
242
370
|
summary.append(f"removed {PACKAGE_NAME_NPM} from package.json")
|
|
@@ -247,8 +375,17 @@ def main(
|
|
|
247
375
|
summary.append(f"removed legacy symlink {name}")
|
|
248
376
|
for name in preserved_links:
|
|
249
377
|
summary.append(f"preserved user-managed {name} (review manually)")
|
|
250
|
-
|
|
251
|
-
|
|
378
|
+
try:
|
|
379
|
+
state_summary = _migrate_state_file(project)
|
|
380
|
+
except Exception as exc: # noqa: BLE001 — surface as exit-1.
|
|
381
|
+
print(f"❌ state migration failed: {exc}", file=sys.stderr)
|
|
382
|
+
return 1
|
|
383
|
+
if state_summary:
|
|
384
|
+
summary.append(state_summary)
|
|
385
|
+
for rel in _delete_legacy_settings(project):
|
|
386
|
+
summary.append(f"deleted legacy config {rel}")
|
|
387
|
+
if _remove_empty_shell(project):
|
|
388
|
+
summary.append(f"removed empty {LEGACY_AGENT_CONFIG_SHELL}/ shell")
|
|
252
389
|
if _update_gitignore(project):
|
|
253
390
|
summary.append(".gitignore agent-config block refreshed")
|
|
254
391
|
|
|
@@ -259,16 +396,41 @@ def main(
|
|
|
259
396
|
return 0
|
|
260
397
|
|
|
261
398
|
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
399
|
+
def main(
|
|
400
|
+
argv: Optional[list[str]] = None,
|
|
401
|
+
*,
|
|
402
|
+
cwd: Optional[Path] = None,
|
|
403
|
+
version: Optional[str] = None, # noqa: ARG001 — accepted for test compat; unused.
|
|
404
|
+
out=sys.stdout,
|
|
405
|
+
err=sys.stderr, # noqa: ARG001 — reserved for future error paths.
|
|
406
|
+
) -> int:
|
|
407
|
+
parser = argparse.ArgumentParser(
|
|
408
|
+
prog="agent-config migrate",
|
|
409
|
+
description=(
|
|
410
|
+
"One-shot, opinionated migration off legacy install / state shapes. "
|
|
411
|
+
"Removes composer / npm package entries, deletes legacy symlinks + "
|
|
412
|
+
"project-local config, migrates the v0 work-engine state file, and "
|
|
413
|
+
"refreshes the .gitignore block. The wizard recreates fresh config."
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
parser.add_argument(
|
|
417
|
+
"--dry-run", action="store_true",
|
|
418
|
+
help="Detect only; print the plan without writing any files.",
|
|
419
|
+
)
|
|
420
|
+
args = parser.parse_args(argv)
|
|
421
|
+
|
|
422
|
+
project, _ = resolve_project_root(None, cwd=cwd)
|
|
423
|
+
|
|
424
|
+
if _detect_already_migrated(project):
|
|
425
|
+
print("✅ already migrated — nothing to do.", file=out)
|
|
426
|
+
return 0
|
|
427
|
+
|
|
428
|
+
if args.dry_run:
|
|
429
|
+
plan = _build_plan(project)
|
|
430
|
+
_format_dry_run(plan, out=out)
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
return _apply(project, out=out)
|
|
272
434
|
|
|
273
435
|
|
|
274
436
|
if __name__ == "__main__": # pragma: no cover
|
|
@@ -4,7 +4,8 @@ Phase 2.4 of ``agents/roadmaps/road-to-global-only-install.md``. Copies
|
|
|
4
4
|
an existing project-local ``.agent-settings.yml`` / ``.agent-user.yml``
|
|
5
5
|
into ``~/.event4u/agent-config/`` so the global-only consumer surface
|
|
6
6
|
(ADR-020) can take over. Read-only on the source — the destructive
|
|
7
|
-
``move`` step is owned by the
|
|
7
|
+
``move`` step is owned by the unified ``agent-config migrate`` command
|
|
8
|
+
(see ``docs/contracts/migrate-command.md``).
|
|
8
9
|
|
|
9
10
|
Idempotent — refuses to overwrite a non-empty global file without
|
|
10
11
|
``--force``. ``--dry-run`` lists intended copies; zero writes; exit 0.
|