@event4u/agent-config 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent-src/commands/install-via-agent.md +129 -0
- package/.agent-src/commands/video/from-script.md +1 -1
- package/.agent-src/commands/video.md +1 -1
- package/.agent-src/contexts/execution/cheap-question-mechanics.md +81 -0
- package/.agent-src/rules/caveman-speak.md +2 -2
- package/.agent-src/rules/context-hygiene.md +36 -0
- package/.agent-src/rules/engineering-safety-floor.md +102 -0
- package/.agent-src/rules/finance-safety-floor.md +114 -0
- package/.agent-src/rules/git-history-discipline.md +1 -1
- package/.agent-src/rules/no-cheap-questions.md +34 -32
- package/.agent-src/rules/provider-lifecycle-discipline.md +4 -4
- package/.agent-src/rules/strategy-safety-floor.md +114 -0
- package/.agent-src/skills/agents-md-thin-root/SKILL.md +15 -9
- package/.agent-src/skills/async-python-patterns/SKILL.md +1 -1
- package/.agent-src/skills/project-analysis-node-express/SKILL.md +1 -1
- package/.agent-src/skills/readme-reviewer/SKILL.md +52 -3
- package/.agent-src/skills/readme-writing/SKILL.md +52 -4
- package/.agent-src/skills/readme-writing-package/SKILL.md +48 -5
- package/.agent-src/skills/systematic-debugging/SKILL.md +41 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/hooks/pre-commit-frontmatter +66 -0
- package/.agent-src/templates/hooks/pre-commit-roadmap-progress +78 -39
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +4 -1
- package/.agent-src/templates/scripts/work_engine/orchestration.py +25 -11
- package/.claude-plugin/marketplace.json +2 -1
- package/AGENTS.md +10 -8
- package/CHANGELOG.md +233 -123
- package/README.md +165 -553
- package/config/agent-settings.template.yml +0 -7
- package/config/discovery/packs.yml +20 -0
- package/config/discovery/unassigned-artefacts.yml +2 -0
- package/config/gitignore-block.txt +19 -3
- package/dist/cli/commands/uiServe.js +13 -4
- package/dist/cli/commands/uiServe.js.map +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +7 -0
- package/dist/discovery/discovery-manifest.json +2107 -1409
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +9 -9
- package/dist/discovery/orphan-report.md +10 -0
- package/dist/discovery/packs.json +1002 -0
- package/dist/discovery/trust-report.md +26 -0
- package/dist/discovery/workspaces.json +705 -0
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1623 -0
- package/dist/server/app.js +11 -3
- package/dist/server/app.js.map +1 -1
- package/dist/server/io/atomicMultiWrite.js +3 -1
- package/dist/server/io/atomicMultiWrite.js.map +1 -1
- package/dist/server/io/yamlIO.js +22 -0
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/routes/ping.js +8 -0
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/routes/schema.js +2 -2
- package/dist/server/routes/schema.js.map +1 -1
- package/dist/server/routes/settings.js +104 -23
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/userMd.js +37 -27
- package/dist/server/routes/userMd.js.map +1 -1
- package/dist/server/routes/wizard.js +256 -20
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/server/schemas/settings.js +0 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/server/token.js +10 -3
- package/dist/server/token.js.map +1 -1
- package/dist/server/writeRoot.js +28 -11
- package/dist/server/writeRoot.js.map +1 -1
- package/dist/server/writeRoot.test.js +22 -4
- package/dist/server/writeRoot.test.js.map +1 -1
- package/dist/shared/userMd/formAdapter.js +29 -51
- package/dist/shared/userMd/formAdapter.js.map +1 -1
- package/dist/shared/userMd/schema.js +32 -104
- package/dist/shared/userMd/schema.js.map +1 -1
- package/dist/shared/userMd/utils.js +64 -50
- package/dist/shared/userMd/utils.js.map +1 -1
- package/dist/ui/assets/index-D-DY1ywI.js +35 -0
- package/dist/ui/assets/index-D-DY1ywI.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/adrs/router/0001-three-tier-routing.md +5 -5
- package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +1 -1
- package/docs/architecture.md +3 -3
- package/docs/archive/CHANGELOG-pre-3.1.0.md +167 -0
- package/docs/catalog.md +30 -26
- package/docs/contracts/CHANGELOG-conventions.md +1 -1
- package/docs/contracts/agent-user-schema.md +6 -9
- package/docs/contracts/consumer-bridge.md +79 -0
- package/docs/contracts/discovery-manifest.md +209 -0
- package/docs/contracts/discovery-manifest.schema.json +77 -4
- package/docs/contracts/explain-trace.schema.json +1 -1
- package/docs/contracts/file-ownership-matrix.json +197 -13
- package/docs/contracts/frontmatter-contract.md +140 -0
- package/docs/contracts/gui-wizard.md +223 -0
- package/docs/contracts/installer-agent-mode.md +137 -0
- package/docs/contracts/kernel-membership.md +1 -1
- package/docs/contracts/mcp-tool-inventory.md +9 -9
- package/docs/contracts/namespace.md +6 -6
- package/docs/contracts/provider-lifecycle.md +5 -5
- package/docs/contracts/rule-router.md +4 -4
- package/docs/contracts/settings-api.md +53 -6
- package/docs/contracts/smoke-contracts.md +3 -3
- package/docs/contracts/trust-and-safety.md +144 -0
- package/docs/customization.md +2 -2
- package/docs/decisions/ADR-007-agent-discovery-scopes.md +12 -0
- package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +24 -0
- package/docs/decisions/ADR-015-discovery-manifest-contract.md +146 -0
- package/docs/decisions/ADR-016-installer-architecture.md +189 -0
- package/docs/decisions/ADR-017-monorepo-physical-layout.md +261 -0
- package/docs/decisions/ADR-018-trust-and-safety-layer.md +159 -0
- package/docs/decisions/ADR-019-router-json-dist-location.md +124 -0
- package/docs/decisions/ADR-020-global-only-consumer-scope.md +123 -0
- package/docs/decisions/ADR-021-deployment-shape.md +153 -0
- package/docs/decisions/INDEX.md +7 -0
- package/docs/deploy/connector-setup.md +129 -0
- package/docs/deploy/env-vars.md +70 -0
- package/docs/deploy/policy-cookbook.md +130 -0
- package/docs/deploy/quickstart.md +112 -0
- package/docs/distribution/public-install-smoke.md +68 -0
- package/docs/distribution/registries.md +55 -0
- package/docs/distribution/telemetry-privacy.md +128 -0
- package/docs/distribution/telemetry-schema.md +174 -0
- package/docs/featured-skills.md +95 -0
- package/docs/getting-started-by-role.md +19 -1
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +11 -8
- package/docs/guidelines/docs/readme-size-and-splitting.md +53 -1
- package/docs/installation.md +27 -14
- package/docs/maintainers/dev-mode.md +105 -0
- package/docs/setup/per-ide/claude-desktop.md +3 -2
- package/docs/wizard.md +39 -4
- package/package.json +18 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_doctor.py +150 -2
- package/scripts/_cli/cmd_explain.py +2 -1
- package/scripts/_cli/cmd_migrate_to_global.py +415 -0
- package/scripts/_cli/cmd_settings_migrate.py +146 -0
- package/scripts/_cli/explain_last/route.py +2 -1
- package/scripts/_dispatch.bash +36 -3
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/agent_settings.py +4 -1
- package/scripts/_lib/agent_src.py +157 -0
- package/scripts/agent-config +17 -6
- package/scripts/audit_skill_descriptions.py +18 -6
- package/scripts/build_discovery_manifest.py +373 -17
- package/scripts/check_artefact_checksums.py +104 -0
- package/scripts/check_cluster_patterns.py +20 -4
- package/scripts/check_command_count_messaging.py +33 -14
- package/scripts/check_council_references.py +43 -4
- package/scripts/check_overlay_cascade_subdirs.py +7 -3
- package/scripts/check_references.py +5 -2
- package/scripts/check_reply_consistency.py +32 -9
- package/scripts/check_template_pin_drift.py +24 -7
- package/scripts/check_token_optimizer_freshness.py +18 -3
- package/scripts/compile_router.py +34 -2
- package/scripts/compress.py +162 -44
- package/scripts/config/presets.py +19 -1
- package/scripts/config/profiles.py +16 -1
- package/scripts/discovery_stats.py +70 -0
- package/scripts/expected_perms.json +47 -0
- package/scripts/generate_index.py +78 -46
- package/scripts/generate_ownership_matrix.py +98 -43
- package/scripts/generate_pack_manifests.py +183 -0
- package/scripts/install +18 -1
- package/scripts/install.py +934 -59
- package/scripts/install.sh +27 -9
- package/scripts/lint_agents_layout.py +93 -13
- package/scripts/lint_agents_md.py +1 -1
- package/scripts/lint_archived_skills.py +32 -16
- package/scripts/lint_bench_corpus.py +14 -2
- package/scripts/lint_command_tiers.py +15 -2
- package/scripts/lint_featured_skills.py +139 -0
- package/scripts/lint_framework_leakage.py +33 -6
- package/scripts/lint_global_paths.py +147 -0
- package/scripts/lint_orchestration_dsl.py +6 -3
- package/scripts/lint_pack_boundaries.py +147 -0
- package/scripts/lint_pack_first_win.py +103 -0
- package/scripts/lint_readme_jargon.py +131 -0
- package/scripts/lint_readme_size.py +33 -0
- package/scripts/lint_rule_interactions.py +23 -5
- package/scripts/lint_rule_tiers.py +12 -3
- package/scripts/lint_trust_coherence.py +212 -0
- package/scripts/measure_rule_budget.py +22 -4
- package/scripts/move_artefact.py +143 -0
- package/scripts/new_skill.py +148 -0
- package/scripts/plan_physical_move.py +353 -0
- package/scripts/refine_ticket_detect.py +30 -7
- package/scripts/release.py +22 -2
- package/scripts/schemas/command.schema.json +4 -0
- package/scripts/skill_linter.py +248 -118
- package/scripts/skill_trigger_eval.py +28 -8
- package/scripts/smoke/kernel.sh +1 -1
- package/scripts/smoke/router.sh +24 -5
- package/scripts/smoke/skills.sh +15 -7
- package/scripts/smoke_quickstart.py +11 -2
- package/scripts/snapshot_agent_outputs.py +144 -0
- package/scripts/update_counts.py +45 -17
- package/scripts/validate_decision_engine.py +9 -1
- package/scripts/validate_discovery_manifest.py +94 -0
- package/scripts/validate_frontmatter.py +39 -20
- package/scripts/verify_physical_move.py +185 -0
- package/templates/agent-user.md +0 -1
- package/templates/agent-user.yml +21 -0
- package/templates/minimal/agents-overrides-readme.md +46 -0
- package/templates/minimal/overrides-gitkeep +2 -0
- package/dist/ui/assets/index-BTRcKDlB.js +0 -39
- package/dist/ui/assets/index-BTRcKDlB.js.map +0 -1
- package/templates/minimal/agents-gitkeep +0 -2
|
@@ -0,0 +1,415 @@
|
|
|
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())
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""``agent-config settings:migrate`` — lift project-local settings into the global store.
|
|
2
|
+
|
|
3
|
+
Phase 2.4 of ``agents/roadmaps/road-to-global-only-install.md``. Copies
|
|
4
|
+
an existing project-local ``.agent-settings.yml`` / ``.agent-user.yml``
|
|
5
|
+
into ``~/.event4u/agent-config/`` so the global-only consumer surface
|
|
6
|
+
(ADR-020) can take over. Read-only on the source — the destructive
|
|
7
|
+
``move`` step is owned by the Phase 5 ``migrate-to-global`` subcommand.
|
|
8
|
+
|
|
9
|
+
Idempotent — refuses to overwrite a non-empty global file without
|
|
10
|
+
``--force``. ``--dry-run`` lists intended copies; zero writes; exit 0.
|
|
11
|
+
|
|
12
|
+
Exit codes:
|
|
13
|
+
|
|
14
|
+
* ``0`` — success or no-op (nothing to migrate / already migrated).
|
|
15
|
+
* ``1`` — at least one global file is non-empty and ``--force`` was
|
|
16
|
+
not passed, or a source file failed YAML parse.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _import_install():
|
|
29
|
+
"""Lazy import so ``--help`` works without the package on sys.path."""
|
|
30
|
+
here = Path(__file__).resolve().parents[2]
|
|
31
|
+
if str(here) not in sys.path:
|
|
32
|
+
sys.path.insert(0, str(here))
|
|
33
|
+
from scripts import install as install_mod # noqa: PLC0415
|
|
34
|
+
return install_mod
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_non_empty_yaml(path: Path) -> bool:
|
|
38
|
+
"""Return True when the file exists and has non-whitespace content."""
|
|
39
|
+
if not path.is_file():
|
|
40
|
+
return False
|
|
41
|
+
try:
|
|
42
|
+
text = path.read_text(encoding="utf-8")
|
|
43
|
+
except OSError:
|
|
44
|
+
return False
|
|
45
|
+
return text.strip() != ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_yaml_or_fail(path: Path, out) -> bool:
|
|
49
|
+
"""Soft-parse a YAML file; print the error and return False on failure."""
|
|
50
|
+
try:
|
|
51
|
+
import yaml # type: ignore[import-not-found]
|
|
52
|
+
except ImportError:
|
|
53
|
+
return True # No PyYAML — defer the validation to the consumer.
|
|
54
|
+
try:
|
|
55
|
+
text = path.read_text(encoding="utf-8")
|
|
56
|
+
yaml.safe_load(text)
|
|
57
|
+
return True
|
|
58
|
+
except (OSError, yaml.YAMLError) as exc:
|
|
59
|
+
print(f"❌ {path}: cannot parse as YAML: {exc}", file=out)
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _copy(src: Path, dst: Path, *, dry_run: bool, out) -> str:
|
|
64
|
+
"""Copy `src` to `dst` with mode 0600. Returns a one-line summary."""
|
|
65
|
+
if dry_run:
|
|
66
|
+
return f"would copy {src} → {dst}"
|
|
67
|
+
dst.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
68
|
+
shutil.copy2(src, dst)
|
|
69
|
+
try:
|
|
70
|
+
os.chmod(dst, 0o600)
|
|
71
|
+
except OSError:
|
|
72
|
+
pass
|
|
73
|
+
return f"copied {src} → {dst}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
77
|
+
parser = argparse.ArgumentParser(
|
|
78
|
+
prog="agent-config settings:migrate",
|
|
79
|
+
description=(
|
|
80
|
+
"Lift project-local .agent-settings.yml / .agent-user.yml into "
|
|
81
|
+
"~/.event4u/agent-config/ (the global-only consumer surface)."
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument("--from", dest="from_dir", default=None,
|
|
85
|
+
help="project root to read from (default: cwd)")
|
|
86
|
+
parser.add_argument("--force", action="store_true",
|
|
87
|
+
help="overwrite a non-empty global file")
|
|
88
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
89
|
+
help="list intended copies; zero writes; exit 0")
|
|
90
|
+
opts = parser.parse_args(argv)
|
|
91
|
+
|
|
92
|
+
install_mod = _import_install()
|
|
93
|
+
project = Path(opts.from_dir).resolve() if opts.from_dir else Path.cwd()
|
|
94
|
+
|
|
95
|
+
# Source candidates — typed subdir wins over the legacy flat path.
|
|
96
|
+
src_settings = project / "settings" / install_mod.SETTINGS_FILE
|
|
97
|
+
if not src_settings.is_file():
|
|
98
|
+
src_settings = project / install_mod.SETTINGS_FILE
|
|
99
|
+
src_user = project / "settings" / ".agent-user.yml"
|
|
100
|
+
if not src_user.is_file():
|
|
101
|
+
src_user = project / ".agent-user.yml"
|
|
102
|
+
|
|
103
|
+
dst_settings = install_mod.GLOBAL_AGENT_SETTINGS_PATH
|
|
104
|
+
dst_user = install_mod.GLOBAL_USER_SETTINGS_PATH
|
|
105
|
+
|
|
106
|
+
plan: list[tuple[Path, Path]] = []
|
|
107
|
+
skipped: list[str] = []
|
|
108
|
+
|
|
109
|
+
for src, dst, label in (
|
|
110
|
+
(src_settings, dst_settings, "settings"),
|
|
111
|
+
(src_user, dst_user, "user"),
|
|
112
|
+
):
|
|
113
|
+
if not src.is_file():
|
|
114
|
+
skipped.append(f"{label}: source absent ({src})")
|
|
115
|
+
continue
|
|
116
|
+
if _is_non_empty_yaml(dst) and not opts.force:
|
|
117
|
+
print(f"❌ {dst} is non-empty — pass --force to overwrite.",
|
|
118
|
+
file=sys.stderr)
|
|
119
|
+
return 1
|
|
120
|
+
if not _parse_yaml_or_fail(src, sys.stderr):
|
|
121
|
+
return 1
|
|
122
|
+
plan.append((src, dst))
|
|
123
|
+
|
|
124
|
+
if not plan:
|
|
125
|
+
print("✅ nothing to migrate — no project-local settings detected.")
|
|
126
|
+
for line in skipped:
|
|
127
|
+
print(f" - {line}")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
summary: list[str] = []
|
|
131
|
+
for src, dst in plan:
|
|
132
|
+
summary.append(_copy(src, dst, dry_run=opts.dry_run, out=sys.stdout))
|
|
133
|
+
|
|
134
|
+
verb = "would migrate" if opts.dry_run else "migrated"
|
|
135
|
+
print(f"✅ {verb} {len(plan)} file(s):")
|
|
136
|
+
for line in summary:
|
|
137
|
+
print(f" - {line}")
|
|
138
|
+
for line in skipped:
|
|
139
|
+
print(f" - {line}")
|
|
140
|
+
if opts.dry_run:
|
|
141
|
+
print("\n Re-run without --dry-run to apply.")
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__": # pragma: no cover
|
|
146
|
+
sys.exit(main())
|
|
@@ -14,10 +14,11 @@ from typing import Any
|
|
|
14
14
|
from scripts._cli.explain_last.scrubber import scrub_string
|
|
15
15
|
|
|
16
16
|
ROUTER_FILENAME = "router.json"
|
|
17
|
+
ROUTER_RELATIVE = Path("dist") / ROUTER_FILENAME
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _load_router(project_root: Path) -> dict[str, Any] | None:
|
|
20
|
-
path = project_root /
|
|
21
|
+
path = project_root / ROUTER_RELATIVE
|
|
21
22
|
if not path.exists():
|
|
22
23
|
return None
|
|
23
24
|
try:
|
package/scripts/_dispatch.bash
CHANGED
|
@@ -134,7 +134,16 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
134
134
|
settings:check Validate .agent-settings.yml against the YAML-subset contract
|
|
135
135
|
(docs/contracts/settings-sync-yaml-subset.md). Read-only.
|
|
136
136
|
Exit 0 clean, 1 finding(s), 2 file absent / unreadable.
|
|
137
|
-
|
|
137
|
+
settings:migrate Lift project-local .agent-settings.yml / .agent-user.yml into
|
|
138
|
+
~/.event4u/agent-config/ (the global-only consumer surface,
|
|
139
|
+
ADR-020). Idempotent; --force overwrites a non-empty global
|
|
140
|
+
file, --dry-run lists intended copies with zero writes.
|
|
141
|
+
migrate-to-global One-shot legacy → global-only migration (Phase 5,
|
|
142
|
+
road-to-global-only-install.md). Copy → verify → move →
|
|
143
|
+
bridge. Runs lint_global_paths.py first. Flags:
|
|
144
|
+
--dry-run, --force, --rollback, --skip-perms-gate.
|
|
145
|
+
hooks:install Install the combined pre-commit hook (roadmap-progress
|
|
146
|
+
+ ADR-013 artefact frontmatter lint).
|
|
138
147
|
(use --print to dump it, --force to overwrite an existing hook)
|
|
139
148
|
hooks:status Print the runtime hook matrix (per-platform install + bindings)
|
|
140
149
|
Flags: --format json|table, --strict (CI), --project-root <path>
|
|
@@ -557,14 +566,16 @@ cmd_hooks_install() {
|
|
|
557
566
|
--print) print_only=true ;;
|
|
558
567
|
-h|--help)
|
|
559
568
|
cat <<'HELP'
|
|
560
|
-
agent-config hooks:install — install the pre-commit
|
|
569
|
+
agent-config hooks:install — install the combined pre-commit hook
|
|
570
|
+
(roadmap-progress + ADR-013 artefact frontmatter lint).
|
|
561
571
|
|
|
562
572
|
Usage:
|
|
563
573
|
./agent-config hooks:install [--force] [--print]
|
|
564
574
|
|
|
565
575
|
Without flags: copies the hook to .git/hooks/pre-commit. Refuses to
|
|
566
576
|
overwrite an existing pre-commit hook unless --force is given (the
|
|
567
|
-
existing hook may already chain other tooling).
|
|
577
|
+
existing hook may already chain other tooling). Each concern only
|
|
578
|
+
runs when relevant files are staged — zero overhead otherwise.
|
|
568
579
|
|
|
569
580
|
--print dump the hook script to stdout (for manual chaining into an
|
|
570
581
|
existing pre-commit script, husky, lefthook, etc.)
|
|
@@ -717,6 +728,26 @@ cmd_settings_check() {
|
|
|
717
728
|
exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_settings_check "$@"
|
|
718
729
|
}
|
|
719
730
|
|
|
731
|
+
# `agent-config settings:migrate` — lift project-local
|
|
732
|
+
# .agent-settings.yml / .agent-user.yml into ~/.event4u/agent-config/.
|
|
733
|
+
# Phase 2.4 of road-to-global-only-install.md. Read-only on the source —
|
|
734
|
+
# the destructive move step is owned by `migrate-to-global` (Phase 5).
|
|
735
|
+
# Exit 0 success / no-op, 1 non-empty global without --force or parse error.
|
|
736
|
+
cmd_settings_migrate() {
|
|
737
|
+
require_python3
|
|
738
|
+
exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_settings_migrate "$@"
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
# `agent-config migrate-to-global` — Phase 5.1 + 5.3 + 5.5 of
|
|
742
|
+
# road-to-global-only-install.md. Order: copy → verify → move → bridge.
|
|
743
|
+
# Runs the lint_global_paths.py permissions gate first (Phase 5.0 / A7).
|
|
744
|
+
# Flags: --dry-run (zero writes), --force (overwrite non-empty global),
|
|
745
|
+
# --rollback (reverse the latest .legacy-pre-global-only/<stamp>/ snapshot).
|
|
746
|
+
cmd_migrate_to_global() {
|
|
747
|
+
require_python3
|
|
748
|
+
exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_migrate_to_global "$@"
|
|
749
|
+
}
|
|
750
|
+
|
|
720
751
|
# `agent-config uninstall` — remove bridge markers (project) or lockfile
|
|
721
752
|
# entries (global). Idempotent. Pass `--purge` to also delete deployed
|
|
722
753
|
# content directories under user-scope anchors (destructive). See
|
|
@@ -809,6 +840,8 @@ main() {
|
|
|
809
840
|
sync) cmd_sync "$@" ;;
|
|
810
841
|
validate) cmd_validate "$@" ;;
|
|
811
842
|
settings:check) cmd_settings_check "$@" ;;
|
|
843
|
+
settings:migrate) cmd_settings_migrate "$@" ;;
|
|
844
|
+
migrate-to-global) cmd_migrate_to_global "$@" ;;
|
|
812
845
|
uninstall) cmd_uninstall "$@" ;;
|
|
813
846
|
prune) cmd_prune "$@" ;;
|
|
814
847
|
doctor) cmd_doctor "$@" ;;
|
|
Binary file
|
|
Binary file
|
|
@@ -99,11 +99,14 @@ ANCHOR_GIT = "git"
|
|
|
99
99
|
|
|
100
100
|
#: Marker subpaths that qualify a bare ``agents/`` directory as a project
|
|
101
101
|
#: anchor (D1). Any one is sufficient. Bare ``agents/`` without a marker
|
|
102
|
-
#: is **not** an anchor.
|
|
102
|
+
#: is **not** an anchor. ``.event4u-bridge.yml`` is the global-only
|
|
103
|
+
#: consumer anchor (ADR-020 § Phase 4.2) — a clean consumer repo only
|
|
104
|
+
#: ever ships ``agents/overrides/`` plus this marker.
|
|
103
105
|
_AGENTS_DIR_MARKERS: tuple[str, ...] = (
|
|
104
106
|
"roadmaps",
|
|
105
107
|
"settings/.ai-council.yml",
|
|
106
108
|
"roadmaps-progress.md",
|
|
109
|
+
".event4u-bridge.yml",
|
|
107
110
|
)
|
|
108
111
|
|
|
109
112
|
#: Kill-switch (D5). When set to ``"1"``, :func:`find_project_root` and
|