@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,415 +0,0 @@
|
|
|
1
|
-
"""``agent-config migrate-to-global`` — one-shot legacy → global migration.
|
|
2
|
-
|
|
3
|
-
Phase 5.1 + 5.3 + 5.5 of ``agents/roadmaps/road-to-global-only-install.md``.
|
|
4
|
-
Lifts a v2.x global-default consumer onto the v2.x global-only surface
|
|
5
|
-
(ADR-020).
|
|
6
|
-
|
|
7
|
-
**Order (per A2)**: ``copy → verify → move → bridge`` — never the inverse.
|
|
8
|
-
|
|
9
|
-
1. **Gate** — run ``scripts/lint_global_paths.py`` first; any finding aborts
|
|
10
|
-
before a single byte is written. Once ``.legacy-pre-global-only/`` is on
|
|
11
|
-
disk, a perms leak cannot be un-written.
|
|
12
|
-
2. **Detect** — project-local YAML settings (``.agent-settings.yml``,
|
|
13
|
-
``.agent-user.yml``, optionally under ``settings/``) and tool-scope
|
|
14
|
-
leftover directories (``.augment/``, ``.claude/``, ``.cursor/``).
|
|
15
|
-
3. **Copy** YAML values into ``~/.event4u/agent-config/``. Refuses to
|
|
16
|
-
overwrite a non-empty global file without ``--force``.
|
|
17
|
-
4. **Verify** every global copy round-trip parses and has mode ``0600``.
|
|
18
|
-
5. **Move** legacy originals into ``.legacy-pre-global-only/<stamp>/``
|
|
19
|
-
alongside a ``manifest.json`` recording every file moved and every
|
|
20
|
-
global file this migration created (used by ``--rollback``).
|
|
21
|
-
6. **Bridge** — write ``agents/.event4u-bridge.yml`` last.
|
|
22
|
-
7. **Summary** — single block: copied / verified / moved / skipped per file.
|
|
23
|
-
|
|
24
|
-
``--dry-run`` lists the plan without touching disk; exit 0.
|
|
25
|
-
``--rollback`` reads the latest snapshot manifest and reverses the
|
|
26
|
-
migration byte-identically (Phase 5.5 / A3).
|
|
27
|
-
"""
|
|
28
|
-
from __future__ import annotations
|
|
29
|
-
|
|
30
|
-
import argparse
|
|
31
|
-
import json
|
|
32
|
-
import os
|
|
33
|
-
import shutil
|
|
34
|
-
import sys
|
|
35
|
-
from datetime import datetime, timezone
|
|
36
|
-
from pathlib import Path
|
|
37
|
-
from typing import Optional
|
|
38
|
-
|
|
39
|
-
# Filenames detected at the project root (Phase 5.1 step 1).
|
|
40
|
-
LEGACY_YAML_FILES: tuple[str, ...] = (".agent-settings.yml", ".agent-user.yml")
|
|
41
|
-
LEGACY_TOOL_DIRS: tuple[str, ...] = (".augment", ".claude", ".cursor")
|
|
42
|
-
SNAPSHOT_DIRNAME = ".legacy-pre-global-only"
|
|
43
|
-
MANIFEST_NAME = "manifest.json"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _import_install():
|
|
47
|
-
here = Path(__file__).resolve().parents[2]
|
|
48
|
-
if str(here) not in sys.path:
|
|
49
|
-
sys.path.insert(0, str(here))
|
|
50
|
-
from scripts import install as install_mod # noqa: PLC0415
|
|
51
|
-
return install_mod
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _import_lint_global_paths():
|
|
55
|
-
here = Path(__file__).resolve().parents[2]
|
|
56
|
-
if str(here) not in sys.path:
|
|
57
|
-
sys.path.insert(0, str(here))
|
|
58
|
-
from scripts import lint_global_paths as lgp # noqa: PLC0415
|
|
59
|
-
return lgp
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _resolve_installed_version(install_mod) -> str:
|
|
63
|
-
"""Return the current package version via the install module's lock helper."""
|
|
64
|
-
try:
|
|
65
|
-
lock_mod = install_mod._load_installed_lock_module()
|
|
66
|
-
return lock_mod.current_package_version()
|
|
67
|
-
except Exception: # noqa: BLE001 — best-effort version resolution.
|
|
68
|
-
return "unknown"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _consumer_bridge_marker_abs(project: Path, install_mod) -> Path:
|
|
72
|
-
"""Resolve ``agents/.event4u-bridge.yml`` for ``project``."""
|
|
73
|
-
relpath = install_mod.CONSUMER_BRIDGE_MARKER_RELPATH
|
|
74
|
-
return project / relpath
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _is_non_empty(path: Path) -> bool:
|
|
78
|
-
try:
|
|
79
|
-
return path.is_file() and path.read_text(encoding="utf-8").strip() != ""
|
|
80
|
-
except OSError:
|
|
81
|
-
return False
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _parse_yaml(path: Path) -> tuple[bool, str]:
|
|
85
|
-
try:
|
|
86
|
-
import yaml # type: ignore[import-not-found]
|
|
87
|
-
except ImportError:
|
|
88
|
-
return True, "" # No PyYAML available — defer validation.
|
|
89
|
-
try:
|
|
90
|
-
text = path.read_text(encoding="utf-8")
|
|
91
|
-
yaml.safe_load(text)
|
|
92
|
-
return True, ""
|
|
93
|
-
except (OSError, Exception) as exc: # noqa: BLE001 — argparse-style error.
|
|
94
|
-
return False, str(exc)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _stamp() -> str:
|
|
98
|
-
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _resolve_yaml_sources(project: Path, install_mod) -> dict[str, Path]:
|
|
102
|
-
"""Map ``.agent-settings.yml`` / ``.agent-user.yml`` to their on-disk
|
|
103
|
-
source — typed ``settings/`` subdir wins over the legacy flat path."""
|
|
104
|
-
out: dict[str, Path] = {}
|
|
105
|
-
for name in LEGACY_YAML_FILES:
|
|
106
|
-
candidate_typed = project / "settings" / name
|
|
107
|
-
candidate_flat = project / name
|
|
108
|
-
if candidate_typed.is_file():
|
|
109
|
-
out[name] = candidate_typed
|
|
110
|
-
elif candidate_flat.is_file():
|
|
111
|
-
out[name] = candidate_flat
|
|
112
|
-
return out
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _yaml_destination(install_mod, name: str) -> Path:
|
|
116
|
-
if name == ".agent-settings.yml":
|
|
117
|
-
return install_mod.GLOBAL_AGENT_SETTINGS_PATH
|
|
118
|
-
if name == ".agent-user.yml":
|
|
119
|
-
return install_mod.GLOBAL_USER_SETTINGS_PATH
|
|
120
|
-
raise ValueError(f"unknown YAML target: {name}")
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _copy_yaml(src: Path, dst: Path) -> None:
|
|
124
|
-
dst.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
125
|
-
shutil.copy2(src, dst)
|
|
126
|
-
try:
|
|
127
|
-
os.chmod(dst, 0o600)
|
|
128
|
-
except OSError:
|
|
129
|
-
pass
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _verify_yaml(path: Path) -> tuple[bool, str]:
|
|
133
|
-
"""Verify the global YAML copy: exists, parses, mode ``0600``."""
|
|
134
|
-
if not path.is_file():
|
|
135
|
-
return False, f"missing after copy: {path}"
|
|
136
|
-
ok, err = _parse_yaml(path)
|
|
137
|
-
if not ok:
|
|
138
|
-
return False, f"reparse failed: {err}"
|
|
139
|
-
try:
|
|
140
|
-
mode = path.stat().st_mode & 0o777
|
|
141
|
-
except OSError as exc:
|
|
142
|
-
return False, f"stat failed: {exc}"
|
|
143
|
-
if mode != 0o600:
|
|
144
|
-
return False, f"mode {oct(mode)} (expected 0o600)"
|
|
145
|
-
return True, ""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _move_into_snapshot(src: Path, snapshot_root: Path, project: Path) -> Path:
|
|
149
|
-
"""Move ``src`` under ``snapshot_root`` preserving project-relative layout.
|
|
150
|
-
Returns the new path inside the snapshot."""
|
|
151
|
-
rel = src.relative_to(project)
|
|
152
|
-
dst = snapshot_root / rel
|
|
153
|
-
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
-
shutil.move(str(src), str(dst))
|
|
155
|
-
return dst
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _run_perms_gate(out) -> int:
|
|
160
|
-
"""Run the Phase 5.0 entry-gate; return ``lint`` exit code."""
|
|
161
|
-
lgp = _import_lint_global_paths()
|
|
162
|
-
return lgp.lint(lgp.DEFAULT_POLICY, quiet=True)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _build_plan(project: Path, install_mod) -> dict:
|
|
166
|
-
"""Return a plan describing every detected legacy artefact."""
|
|
167
|
-
yaml_sources = _resolve_yaml_sources(project, install_mod)
|
|
168
|
-
yaml_plan: list[dict] = []
|
|
169
|
-
for name, src in yaml_sources.items():
|
|
170
|
-
dst = _yaml_destination(install_mod, name)
|
|
171
|
-
yaml_plan.append({
|
|
172
|
-
"name": name,
|
|
173
|
-
"src": str(src),
|
|
174
|
-
"dst": str(dst),
|
|
175
|
-
"global_existed_non_empty": _is_non_empty(dst),
|
|
176
|
-
})
|
|
177
|
-
dir_plan: list[dict] = []
|
|
178
|
-
for name in LEGACY_TOOL_DIRS:
|
|
179
|
-
p = project / name
|
|
180
|
-
if p.is_dir() and not p.is_symlink():
|
|
181
|
-
dir_plan.append({"name": name, "src": str(p)})
|
|
182
|
-
return {"yaml": yaml_plan, "dirs": dir_plan}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _format_plan(plan: dict, dry_run: bool, out) -> None:
|
|
186
|
-
n_yaml = len(plan["yaml"])
|
|
187
|
-
n_dirs = len(plan["dirs"])
|
|
188
|
-
if n_yaml + n_dirs == 0:
|
|
189
|
-
print("✅ nothing to migrate — no legacy artefacts detected.", file=out)
|
|
190
|
-
return
|
|
191
|
-
verb = "would migrate" if dry_run else "migrating"
|
|
192
|
-
print(f"📦 {verb} {n_yaml} YAML file(s) + {n_dirs} directory(ies):", file=out)
|
|
193
|
-
for entry in plan["yaml"]:
|
|
194
|
-
flag = " (would overwrite)" if entry["global_existed_non_empty"] else ""
|
|
195
|
-
print(f" - copy {entry['src']} → {entry['dst']}{flag}", file=out)
|
|
196
|
-
for entry in plan["dirs"]:
|
|
197
|
-
print(f" - move {entry['src']} → snapshot", file=out)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def _do_migrate(project: Path, force: bool, install_mod, out) -> int:
|
|
201
|
-
plan = _build_plan(project, install_mod)
|
|
202
|
-
if not plan["yaml"] and not plan["dirs"]:
|
|
203
|
-
print("✅ nothing to migrate — no legacy artefacts detected.", file=out)
|
|
204
|
-
return 0
|
|
205
|
-
|
|
206
|
-
for entry in plan["yaml"]:
|
|
207
|
-
if entry["global_existed_non_empty"] and not force:
|
|
208
|
-
print(f"❌ {entry['dst']} is non-empty — pass --force to overwrite.",
|
|
209
|
-
file=sys.stderr)
|
|
210
|
-
return 1
|
|
211
|
-
|
|
212
|
-
for entry in plan["yaml"]:
|
|
213
|
-
ok, err = _parse_yaml(Path(entry["src"]))
|
|
214
|
-
if not ok:
|
|
215
|
-
print(f"❌ {entry['src']}: cannot parse as YAML: {err}",
|
|
216
|
-
file=sys.stderr)
|
|
217
|
-
return 1
|
|
218
|
-
|
|
219
|
-
# COPY
|
|
220
|
-
copied: list[tuple[Path, Path]] = []
|
|
221
|
-
for entry in plan["yaml"]:
|
|
222
|
-
src, dst = Path(entry["src"]), Path(entry["dst"])
|
|
223
|
-
_copy_yaml(src, dst)
|
|
224
|
-
copied.append((src, dst))
|
|
225
|
-
|
|
226
|
-
# VERIFY
|
|
227
|
-
for _src, dst in copied:
|
|
228
|
-
ok, err = _verify_yaml(dst)
|
|
229
|
-
if not ok:
|
|
230
|
-
print(f"❌ verify failed for {dst}: {err}", file=sys.stderr)
|
|
231
|
-
print(" aborting migration — local originals untouched.",
|
|
232
|
-
file=sys.stderr)
|
|
233
|
-
return 1
|
|
234
|
-
|
|
235
|
-
# MOVE — only after verify passes.
|
|
236
|
-
snapshot_root = project / SNAPSHOT_DIRNAME / _stamp()
|
|
237
|
-
snapshot_root.mkdir(parents=True, exist_ok=True)
|
|
238
|
-
moved_yaml: list[tuple[str, str]] = []
|
|
239
|
-
moved_dirs: list[tuple[str, str]] = []
|
|
240
|
-
for entry in plan["yaml"]:
|
|
241
|
-
src = Path(entry["src"])
|
|
242
|
-
if src.is_file():
|
|
243
|
-
dst_snap = _move_into_snapshot(src, snapshot_root, project)
|
|
244
|
-
moved_yaml.append((str(src), str(dst_snap)))
|
|
245
|
-
for entry in plan["dirs"]:
|
|
246
|
-
src = Path(entry["src"])
|
|
247
|
-
if src.is_dir():
|
|
248
|
-
dst_snap = _move_into_snapshot(src, snapshot_root, project)
|
|
249
|
-
moved_dirs.append((str(src), str(dst_snap)))
|
|
250
|
-
|
|
251
|
-
manifest = {
|
|
252
|
-
"schema": "event4u-migrate-snapshot/v1",
|
|
253
|
-
"stamp": _stamp(),
|
|
254
|
-
"project_root": str(project),
|
|
255
|
-
"global_root": str(install_mod.GLOBAL_ROOT),
|
|
256
|
-
"moved_yaml": moved_yaml,
|
|
257
|
-
"moved_dirs": moved_dirs,
|
|
258
|
-
"global_copies": [str(dst) for _src, dst in copied],
|
|
259
|
-
}
|
|
260
|
-
(snapshot_root / MANIFEST_NAME).write_text(
|
|
261
|
-
json.dumps(manifest, indent=2) + "\n", encoding="utf-8",
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
# BRIDGE (last)
|
|
265
|
-
version = _resolve_installed_version(install_mod)
|
|
266
|
-
marker = install_mod._write_consumer_bridge_marker(project, version)
|
|
267
|
-
|
|
268
|
-
print(f"✅ migrated — snapshot at {snapshot_root}", file=out)
|
|
269
|
-
for src, dst in copied:
|
|
270
|
-
print(f" - copied {src} → {dst}", file=out)
|
|
271
|
-
for src, dst in moved_yaml:
|
|
272
|
-
print(f" - moved {src} → {dst}", file=out)
|
|
273
|
-
for src, dst in moved_dirs:
|
|
274
|
-
print(f" - moved {src} → {dst}", file=out)
|
|
275
|
-
if marker is not None:
|
|
276
|
-
print(f" - bridge {marker}", file=out)
|
|
277
|
-
return 0
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
def _find_latest_snapshot(project: Path) -> Optional[Path]:
|
|
282
|
-
root = project / SNAPSHOT_DIRNAME
|
|
283
|
-
if not root.is_dir():
|
|
284
|
-
return None
|
|
285
|
-
stamps = sorted(
|
|
286
|
-
(p for p in root.iterdir() if p.is_dir() and (p / MANIFEST_NAME).is_file()),
|
|
287
|
-
key=lambda p: p.name,
|
|
288
|
-
)
|
|
289
|
-
return stamps[-1] if stamps else None
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def _do_rollback(project: Path, dry_run: bool, install_mod, out) -> int:
|
|
293
|
-
snapshot = _find_latest_snapshot(project)
|
|
294
|
-
if snapshot is None:
|
|
295
|
-
print(f"❌ no snapshot under {project / SNAPSHOT_DIRNAME} — nothing to roll back.",
|
|
296
|
-
file=sys.stderr)
|
|
297
|
-
return 1
|
|
298
|
-
try:
|
|
299
|
-
manifest = json.loads((snapshot / MANIFEST_NAME).read_text(encoding="utf-8"))
|
|
300
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
301
|
-
print(f"❌ cannot read manifest {snapshot / MANIFEST_NAME}: {exc}",
|
|
302
|
-
file=sys.stderr)
|
|
303
|
-
return 1
|
|
304
|
-
|
|
305
|
-
moved_yaml = manifest.get("moved_yaml", [])
|
|
306
|
-
moved_dirs = manifest.get("moved_dirs", [])
|
|
307
|
-
global_copies = manifest.get("global_copies", [])
|
|
308
|
-
|
|
309
|
-
if dry_run:
|
|
310
|
-
print(f"🔁 would roll back from {snapshot}", file=out)
|
|
311
|
-
for original, snap in moved_yaml + moved_dirs:
|
|
312
|
-
print(f" - restore {snap} → {original}", file=out)
|
|
313
|
-
for path in global_copies:
|
|
314
|
-
print(f" - delete {path}", file=out)
|
|
315
|
-
print(f" - remove {_consumer_bridge_marker_abs(project, install_mod)}", file=out)
|
|
316
|
-
return 0
|
|
317
|
-
|
|
318
|
-
# Pre-flight: every restore target must be vacant.
|
|
319
|
-
for original, _snap in moved_yaml + moved_dirs:
|
|
320
|
-
if Path(original).exists():
|
|
321
|
-
print(f"❌ restore target already exists: {original}", file=sys.stderr)
|
|
322
|
-
print(" aborting — manual cleanup required.", file=sys.stderr)
|
|
323
|
-
return 1
|
|
324
|
-
|
|
325
|
-
# Restore moved originals.
|
|
326
|
-
for original, snap in moved_yaml + moved_dirs:
|
|
327
|
-
src, dst = Path(snap), Path(original)
|
|
328
|
-
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
329
|
-
shutil.move(str(src), str(dst))
|
|
330
|
-
|
|
331
|
-
# Delete global copies this migration created.
|
|
332
|
-
for path in global_copies:
|
|
333
|
-
p = Path(path)
|
|
334
|
-
try:
|
|
335
|
-
if p.is_file():
|
|
336
|
-
p.unlink()
|
|
337
|
-
except OSError as exc:
|
|
338
|
-
print(f"⚠️ could not delete {p}: {exc}", file=sys.stderr)
|
|
339
|
-
|
|
340
|
-
# Drop the bridge marker.
|
|
341
|
-
marker = _consumer_bridge_marker_abs(project, install_mod)
|
|
342
|
-
try:
|
|
343
|
-
if marker.is_file():
|
|
344
|
-
marker.unlink()
|
|
345
|
-
except OSError as exc:
|
|
346
|
-
print(f"⚠️ could not remove bridge marker {marker}: {exc}", file=sys.stderr)
|
|
347
|
-
|
|
348
|
-
# Archive the consumed snapshot directory so a second rollback cleanly
|
|
349
|
-
# surfaces "no snapshot found" rather than re-restoring stale data.
|
|
350
|
-
consumed = snapshot.with_name(snapshot.name + ".consumed")
|
|
351
|
-
try:
|
|
352
|
-
snapshot.rename(consumed)
|
|
353
|
-
except OSError:
|
|
354
|
-
pass
|
|
355
|
-
|
|
356
|
-
print(f"✅ rolled back — originals restored, global copies removed.", file=out)
|
|
357
|
-
return 0
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
def main(argv: Optional[list[str]] = None) -> int:
|
|
361
|
-
parser = argparse.ArgumentParser(
|
|
362
|
-
prog="agent-config migrate-to-global",
|
|
363
|
-
description="Lift legacy project-local config into ~/.event4u/agent-config/.",
|
|
364
|
-
)
|
|
365
|
-
parser.add_argument(
|
|
366
|
-
"--from", dest="project", type=Path, default=Path.cwd(),
|
|
367
|
-
help="Project root to migrate (default: cwd).",
|
|
368
|
-
)
|
|
369
|
-
parser.add_argument(
|
|
370
|
-
"--dry-run", action="store_true",
|
|
371
|
-
help="Print the plan without touching disk.",
|
|
372
|
-
)
|
|
373
|
-
parser.add_argument(
|
|
374
|
-
"--force", action="store_true",
|
|
375
|
-
help="Overwrite non-empty global files.",
|
|
376
|
-
)
|
|
377
|
-
parser.add_argument(
|
|
378
|
-
"--rollback", action="store_true",
|
|
379
|
-
help="Reverse the latest snapshot under .legacy-pre-global-only/.",
|
|
380
|
-
)
|
|
381
|
-
parser.add_argument(
|
|
382
|
-
"--skip-perms-gate", action="store_true",
|
|
383
|
-
help="Skip the Phase 5.0 permissions audit (NOT recommended).",
|
|
384
|
-
)
|
|
385
|
-
args = parser.parse_args(argv)
|
|
386
|
-
|
|
387
|
-
project = args.project.resolve()
|
|
388
|
-
if not project.is_dir():
|
|
389
|
-
print(f"❌ not a directory: {project}", file=sys.stderr)
|
|
390
|
-
return 2
|
|
391
|
-
|
|
392
|
-
install_mod = _import_install()
|
|
393
|
-
out = sys.stdout
|
|
394
|
-
|
|
395
|
-
if args.rollback:
|
|
396
|
-
return _do_rollback(project, args.dry_run, install_mod, out)
|
|
397
|
-
|
|
398
|
-
if not args.skip_perms_gate:
|
|
399
|
-
rc = _run_perms_gate(out)
|
|
400
|
-
if rc != 0:
|
|
401
|
-
print("❌ permissions audit failed — refusing to migrate.", file=sys.stderr)
|
|
402
|
-
print(" run `agent-config doctor` (or `python3 scripts/lint_global_paths.py`) for details.",
|
|
403
|
-
file=sys.stderr)
|
|
404
|
-
return rc
|
|
405
|
-
|
|
406
|
-
if args.dry_run:
|
|
407
|
-
plan = _build_plan(project, install_mod)
|
|
408
|
-
_format_plan(plan, dry_run=True, out=out)
|
|
409
|
-
return 0
|
|
410
|
-
|
|
411
|
-
return _do_migrate(project, args.force, install_mod, out)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if __name__ == "__main__": # pragma: no cover
|
|
415
|
-
sys.exit(main())
|