@event4u/agent-config 2.15.0 → 2.16.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/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +299 -20
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +145 -225
- package/README.md +11 -0
- package/docs/archive/CHANGELOG-pre-2.15.0.md +244 -0
- package/docs/installation.md +221 -2
- package/package.json +1 -1
- package/scripts/_cli/cmd_doctor.py +238 -8
- package/scripts/_cli/cmd_migrate.py +6 -1
- package/scripts/_cli/cmd_prune.py +8 -3
- package/scripts/_cli/cmd_sync.py +7 -3
- package/scripts/_cli/cmd_uninstall.py +4 -3
- package/scripts/_cli/cmd_update.py +5 -1
- package/scripts/_cli/cmd_validate.py +6 -3
- package/scripts/_cli/cmd_versions.py +15 -2
- package/scripts/_lib/agent_settings.py +299 -20
- package/scripts/agent-config +64 -0
- package/scripts/install +39 -2
- package/scripts/install.py +171 -0
- package/scripts/install.sh +20 -0
- package/templates/agent-config-wrapper.sh +7 -0
- package/templates/minimal/.agent-settings.yml +23 -0
- package/templates/minimal/agents-gitkeep +2 -0
|
@@ -35,6 +35,7 @@ from __future__ import annotations
|
|
|
35
35
|
import argparse
|
|
36
36
|
import hashlib
|
|
37
37
|
import json
|
|
38
|
+
import os
|
|
38
39
|
import re
|
|
39
40
|
import shutil
|
|
40
41
|
import sys
|
|
@@ -42,6 +43,13 @@ from pathlib import Path
|
|
|
42
43
|
from typing import Any
|
|
43
44
|
|
|
44
45
|
from scripts._lib import installed_tools
|
|
46
|
+
from scripts._lib.agent_settings import (
|
|
47
|
+
PROJECT_ROOT_ENV,
|
|
48
|
+
ROOT_OVERRIDE_ENV,
|
|
49
|
+
ProjectRootError,
|
|
50
|
+
find_project_root_with_trace,
|
|
51
|
+
resolve_project_root,
|
|
52
|
+
)
|
|
45
53
|
|
|
46
54
|
|
|
47
55
|
class _Sentinel:
|
|
@@ -56,10 +64,155 @@ class _Sentinel:
|
|
|
56
64
|
NO_FRONTMATTER = _Sentinel()
|
|
57
65
|
|
|
58
66
|
|
|
59
|
-
def _resolve_project_root(arg: str | None) -> Path:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
def _resolve_project_root(arg: str | None) -> tuple[Path, str]:
|
|
68
|
+
"""Resolve the doctor's project root via the shared Phase-3 helper.
|
|
69
|
+
|
|
70
|
+
Returns ``(root, origin)`` where ``origin`` is one of the anchor
|
|
71
|
+
names from :mod:`scripts._lib.agent_settings` (``agents-dir``,
|
|
72
|
+
``git``, ``agent-settings``) or one of the origin tags
|
|
73
|
+
``root-flag`` / ``explicit`` / ``env`` / ``cwd-fallback``. The tag
|
|
74
|
+
is surfaced in both human and JSON output so subdir invocations
|
|
75
|
+
are auditable.
|
|
76
|
+
"""
|
|
77
|
+
return resolve_project_root(arg)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
#: Path of the install-mode marker file (Step 8 A5). One-line file:
|
|
81
|
+
#: ``minimal\n`` or ``full\n``. Written by ``install.py``; consumed by
|
|
82
|
+
#: ``doctor --context`` to surface install state without re-deriving it
|
|
83
|
+
#: from filesystem heuristics.
|
|
84
|
+
_INSTALL_MODE_MARKER_REL = "agents/.agent-state/install-mode.txt"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _detect_install_mode(project_root: Path) -> tuple[str, str]:
|
|
88
|
+
"""Return ``(mode, source)`` for the project install state.
|
|
89
|
+
|
|
90
|
+
Hybrid detection (Step 8 council decision Q5):
|
|
91
|
+
|
|
92
|
+
1. ``agents/.agent-state/install-mode.txt`` if present (authoritative
|
|
93
|
+
for installs since Step 8) → ``source="marker-file"``.
|
|
94
|
+
2. Filesystem heuristic for back-compat: ``AGENTS.md`` + copilot
|
|
95
|
+
bridges → ``full``; otherwise ``minimal`` → ``source="heuristic"``.
|
|
96
|
+
|
|
97
|
+
Heuristic is fragile by design — users who delete ``AGENTS.md`` but
|
|
98
|
+
keep copilot bridges get misreported. The marker file is the
|
|
99
|
+
intended source of truth for installs from Step 8 onward.
|
|
100
|
+
"""
|
|
101
|
+
marker = project_root / _INSTALL_MODE_MARKER_REL
|
|
102
|
+
if marker.is_file():
|
|
103
|
+
try:
|
|
104
|
+
value = marker.read_text(encoding="utf-8").strip()
|
|
105
|
+
except OSError:
|
|
106
|
+
value = ""
|
|
107
|
+
if value in ("minimal", "full"):
|
|
108
|
+
return value, "marker-file"
|
|
109
|
+
has_agents_md = (project_root / "AGENTS.md").exists()
|
|
110
|
+
has_copilot = (project_root / ".github" / "copilot-instructions.md").exists()
|
|
111
|
+
if has_agents_md and has_copilot:
|
|
112
|
+
return "full", "heuristic"
|
|
113
|
+
return "minimal", "heuristic"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _settings_layer_chain(project_root: Path) -> list[str]:
|
|
117
|
+
"""Return the ordered ``.agent-settings.yml`` layer files that exist.
|
|
118
|
+
|
|
119
|
+
Shallowest first (user-global → project root). Used by ``--context``
|
|
120
|
+
to surface which layers will participate in the cascade merge.
|
|
121
|
+
"""
|
|
122
|
+
layers: list[str] = []
|
|
123
|
+
from scripts._lib import user_global_paths
|
|
124
|
+
user_global = user_global_paths.resolve_with_fallback("agent-settings.yml")
|
|
125
|
+
if user_global is not None and user_global.is_file():
|
|
126
|
+
layers.append(str(user_global))
|
|
127
|
+
project_settings = project_root / ".agent-settings.yml"
|
|
128
|
+
if project_settings.is_file():
|
|
129
|
+
layers.append(str(project_settings))
|
|
130
|
+
return layers
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _detect_wrapper(project_root: Path) -> dict[str, Any]:
|
|
134
|
+
"""Return ``{path, exists, embedded_root}`` for the project wrapper.
|
|
135
|
+
|
|
136
|
+
The ``./agent-config`` wrapper at the project root pins
|
|
137
|
+
``AGENT_CONFIG_PROJECT_ROOT`` to its own ``SELF_DIR``, so the
|
|
138
|
+
"embedded root" is just the wrapper's directory. Surfaced for
|
|
139
|
+
debugging wrapper-coupling issues (Step 8 A3-coupling).
|
|
140
|
+
"""
|
|
141
|
+
wrapper = project_root / "agent-config"
|
|
142
|
+
if not wrapper.exists():
|
|
143
|
+
return {"path": str(wrapper), "exists": False, "embedded_root": None}
|
|
144
|
+
return {
|
|
145
|
+
"path": str(wrapper),
|
|
146
|
+
"exists": True,
|
|
147
|
+
"embedded_root": str(project_root),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _emit_trace_text(
|
|
152
|
+
root: Path | None,
|
|
153
|
+
anchor: str | None,
|
|
154
|
+
trace: list[dict[str, Any]],
|
|
155
|
+
origin: str,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Render the discovery trace as human-readable text."""
|
|
158
|
+
print(f" 📍 start: {Path.cwd()}")
|
|
159
|
+
print(f" 📍 origin: {origin}")
|
|
160
|
+
if trace:
|
|
161
|
+
print(" trace:")
|
|
162
|
+
for record in trace:
|
|
163
|
+
hit = record["hit"]
|
|
164
|
+
symbol = "✅" if hit else "·"
|
|
165
|
+
tag = f"[{record['pass']}]"
|
|
166
|
+
anchor_str = f" → {hit}" if hit else ""
|
|
167
|
+
print(
|
|
168
|
+
f" {symbol} {tag} {record['ancestor']}"
|
|
169
|
+
f"{anchor_str} ({record['reason']})"
|
|
170
|
+
)
|
|
171
|
+
if root is not None:
|
|
172
|
+
print(f" 📍 resolved root: {root} (anchor: {anchor or 'n/a'})")
|
|
173
|
+
else:
|
|
174
|
+
print(" ⚠️ no anchor found in chain")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _emit_trace_json(
|
|
178
|
+
root: Path | None,
|
|
179
|
+
anchor: str | None,
|
|
180
|
+
trace: list[dict[str, Any]],
|
|
181
|
+
origin: str,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Render the discovery trace as a JSON payload."""
|
|
184
|
+
payload: dict[str, Any] = {
|
|
185
|
+
"start": str(Path.cwd()),
|
|
186
|
+
"origin": origin,
|
|
187
|
+
"resolved_root": str(root) if root is not None else None,
|
|
188
|
+
"anchor": anchor,
|
|
189
|
+
"trace": trace,
|
|
190
|
+
}
|
|
191
|
+
print(json.dumps(payload, indent=2))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _emit_context_text(ctx: dict[str, Any]) -> None:
|
|
195
|
+
"""Render the install / discovery context as human-readable text."""
|
|
196
|
+
print(f" 📍 project_root: {ctx['project_root']} (origin: {ctx['origin']})")
|
|
197
|
+
print(f" 📦 install_mode: {ctx['install_mode']} (source: {ctx['install_mode_source']})")
|
|
198
|
+
env_pin = ctx.get("env_pin")
|
|
199
|
+
if env_pin:
|
|
200
|
+
marker = " (--root override)" if ctx.get("root_override") else ""
|
|
201
|
+
print(f" 🔒 env_pin: {env_pin}{marker}")
|
|
202
|
+
else:
|
|
203
|
+
print(" 🔒 env_pin: (unset)")
|
|
204
|
+
layers = ctx.get("settings_layers") or []
|
|
205
|
+
if layers:
|
|
206
|
+
print(f" 📑 settings layers ({len(layers)}):")
|
|
207
|
+
for layer in layers:
|
|
208
|
+
print(f" - {layer}")
|
|
209
|
+
else:
|
|
210
|
+
print(" 📑 settings layers: (none)")
|
|
211
|
+
wrapper = ctx.get("wrapper") or {}
|
|
212
|
+
if wrapper.get("exists"):
|
|
213
|
+
print(f" 🧩 wrapper: {wrapper['path']} (embedded root: {wrapper.get('embedded_root')})")
|
|
214
|
+
else:
|
|
215
|
+
print(f" 🧩 wrapper: (not present at {wrapper.get('path')})")
|
|
63
216
|
|
|
64
217
|
|
|
65
218
|
def _resolve_path(project_root: Path, raw: str) -> Path:
|
|
@@ -872,6 +1025,7 @@ def _emit_json(
|
|
|
872
1025
|
foreign: list[dict[str, Any]],
|
|
873
1026
|
tag_drift: list[dict[str, Any]],
|
|
874
1027
|
checks: list[dict[str, Any]] | None = None,
|
|
1028
|
+
origin: str | None = None,
|
|
875
1029
|
) -> None:
|
|
876
1030
|
payload: dict[str, Any] = {
|
|
877
1031
|
"project_root": str(project_root),
|
|
@@ -880,6 +1034,8 @@ def _emit_json(
|
|
|
880
1034
|
"foreign": foreign,
|
|
881
1035
|
"tag_drift": tag_drift,
|
|
882
1036
|
}
|
|
1037
|
+
if origin is not None:
|
|
1038
|
+
payload["project_root_origin"] = origin
|
|
883
1039
|
if checks is not None:
|
|
884
1040
|
payload["checks"] = checks
|
|
885
1041
|
print(json.dumps(payload, indent=2))
|
|
@@ -948,17 +1104,89 @@ def _parse(argv: list[str]) -> argparse.Namespace:
|
|
|
948
1104
|
help=("run a single health check by id "
|
|
949
1105
|
f"({' · '.join(CHECK_IDS)})"),
|
|
950
1106
|
)
|
|
1107
|
+
parser.add_argument(
|
|
1108
|
+
"--trace-root", action="store_true",
|
|
1109
|
+
help=("print every ancestor checked during project-root discovery "
|
|
1110
|
+
"plus the winning anchor; short-circuits the drift report"),
|
|
1111
|
+
)
|
|
1112
|
+
parser.add_argument(
|
|
1113
|
+
"--context", action="store_true",
|
|
1114
|
+
help=("print effective project root, anchor, env-pin, settings "
|
|
1115
|
+
"layer chain, wrapper, and install-mode; short-circuits "
|
|
1116
|
+
"the drift report"),
|
|
1117
|
+
)
|
|
951
1118
|
return parser.parse_args(argv)
|
|
952
1119
|
|
|
953
1120
|
|
|
1121
|
+
def _run_trace_root(opts: argparse.Namespace) -> int:
|
|
1122
|
+
"""Handle ``--trace-root``: discovery walk + records, no drift report."""
|
|
1123
|
+
start = Path.cwd()
|
|
1124
|
+
root, anchor, trace = find_project_root_with_trace(start)
|
|
1125
|
+
# Determine origin label even when no anchor is found.
|
|
1126
|
+
if os.environ.get(ROOT_OVERRIDE_ENV) == "1" and os.environ.get(PROJECT_ROOT_ENV):
|
|
1127
|
+
origin = "root-flag"
|
|
1128
|
+
elif opts.project:
|
|
1129
|
+
origin = "explicit"
|
|
1130
|
+
elif os.environ.get(PROJECT_ROOT_ENV):
|
|
1131
|
+
origin = "env"
|
|
1132
|
+
elif root is not None:
|
|
1133
|
+
origin = anchor or "unknown"
|
|
1134
|
+
else:
|
|
1135
|
+
origin = "cwd-fallback"
|
|
1136
|
+
if opts.json:
|
|
1137
|
+
_emit_trace_json(root, anchor, trace, origin)
|
|
1138
|
+
else:
|
|
1139
|
+
_emit_trace_text(root, anchor, trace, origin)
|
|
1140
|
+
return 0
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _run_context(opts: argparse.Namespace) -> int:
|
|
1144
|
+
"""Handle ``--context``: install + discovery snapshot, no drift report."""
|
|
1145
|
+
try:
|
|
1146
|
+
project_root, origin = _resolve_project_root(opts.project)
|
|
1147
|
+
except ProjectRootError as exc:
|
|
1148
|
+
print(f"❌ doctor: {exc}", file=sys.stderr)
|
|
1149
|
+
return 2
|
|
1150
|
+
mode, mode_source = _detect_install_mode(project_root)
|
|
1151
|
+
ctx: dict[str, Any] = {
|
|
1152
|
+
"project_root": str(project_root),
|
|
1153
|
+
"origin": origin,
|
|
1154
|
+
"install_mode": mode,
|
|
1155
|
+
"install_mode_source": mode_source,
|
|
1156
|
+
"env_pin": os.environ.get(PROJECT_ROOT_ENV) or None,
|
|
1157
|
+
"root_override": os.environ.get(ROOT_OVERRIDE_ENV) == "1",
|
|
1158
|
+
"settings_layers": _settings_layer_chain(project_root),
|
|
1159
|
+
"wrapper": _detect_wrapper(project_root),
|
|
1160
|
+
}
|
|
1161
|
+
if opts.json:
|
|
1162
|
+
print(json.dumps(ctx, indent=2))
|
|
1163
|
+
else:
|
|
1164
|
+
_emit_context_text(ctx)
|
|
1165
|
+
return 0
|
|
1166
|
+
|
|
1167
|
+
|
|
954
1168
|
def main(argv: list[str] | None = None) -> int:
|
|
955
1169
|
opts = _parse(list(argv) if argv is not None else sys.argv[1:])
|
|
956
|
-
|
|
1170
|
+
if opts.trace_root:
|
|
1171
|
+
return _run_trace_root(opts)
|
|
1172
|
+
if opts.context:
|
|
1173
|
+
return _run_context(opts)
|
|
1174
|
+
try:
|
|
1175
|
+
project_root, origin = _resolve_project_root(opts.project)
|
|
1176
|
+
except ProjectRootError as exc:
|
|
1177
|
+
print(f"❌ doctor: {exc}", file=sys.stderr)
|
|
1178
|
+
return 2
|
|
957
1179
|
manifest_pth = installed_tools.manifest_path(project_root)
|
|
958
1180
|
manifest = installed_tools.read_manifest(manifest_pth)
|
|
959
1181
|
if manifest is None:
|
|
960
|
-
print(
|
|
961
|
-
|
|
1182
|
+
print(
|
|
1183
|
+
f"❌ doctor: no project lockfile at {manifest_pth}",
|
|
1184
|
+
file=sys.stderr,
|
|
1185
|
+
)
|
|
1186
|
+
print(
|
|
1187
|
+
f" project_root: {project_root} (origin: {origin})",
|
|
1188
|
+
file=sys.stderr,
|
|
1189
|
+
)
|
|
962
1190
|
print(" run `./agent-config init` to create one",
|
|
963
1191
|
file=sys.stderr)
|
|
964
1192
|
return 2
|
|
@@ -978,9 +1206,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
978
1206
|
if opts.json:
|
|
979
1207
|
_emit_json(
|
|
980
1208
|
project_root, missing, modified, foreign, tag_drift,
|
|
981
|
-
checks=checks,
|
|
1209
|
+
checks=checks, origin=origin,
|
|
982
1210
|
)
|
|
983
1211
|
else:
|
|
1212
|
+
if opts.check is None:
|
|
1213
|
+
print(f" 📍 project_root: {project_root} (origin: {origin})")
|
|
984
1214
|
_emit_checks_text(checks)
|
|
985
1215
|
if opts.check is None:
|
|
986
1216
|
_emit_text(project_root, missing, modified, foreign, tag_drift)
|
|
@@ -32,6 +32,8 @@ import sys
|
|
|
32
32
|
from pathlib import Path
|
|
33
33
|
from typing import Iterable, Optional
|
|
34
34
|
|
|
35
|
+
from scripts._lib.agent_settings import resolve_project_root
|
|
36
|
+
|
|
35
37
|
PACKAGE_NAME_NPM = "@event4u/agent-config"
|
|
36
38
|
PACKAGE_NAME_COMPOSER = "event4u/agent-config"
|
|
37
39
|
LEGACY_DIRS = ("vendor", "node_modules")
|
|
@@ -221,7 +223,10 @@ def main(
|
|
|
221
223
|
help="Detect only; do not write any files.")
|
|
222
224
|
args = parser.parse_args(argv)
|
|
223
225
|
|
|
224
|
-
|
|
226
|
+
# Phase 3 — honor AGENT_CONFIG_PROJECT_ROOT + anchor walk so
|
|
227
|
+
# ``agent-config migrate`` invoked from a subdir still targets the
|
|
228
|
+
# real project root. ``cwd`` kwarg is preserved for test injection.
|
|
229
|
+
project, _ = resolve_project_root(None, cwd=cwd)
|
|
225
230
|
version = version or _detect_installed_version()
|
|
226
231
|
|
|
227
232
|
if _detect_already_migrated(project):
|
|
@@ -43,13 +43,18 @@ import sys
|
|
|
43
43
|
from pathlib import Path
|
|
44
44
|
|
|
45
45
|
from scripts._lib import installed_tools
|
|
46
|
+
from scripts._lib.agent_settings import resolve_project_root
|
|
46
47
|
from scripts.install import PROJECT_BRIDGE_MARKERS
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
def _resolve_project_root(arg: str | None) -> Path:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
"""Resolve the project root using the shared Phase-3 helper.
|
|
52
|
+
|
|
53
|
+
Drops the origin tag — ``prune`` does not surface it in output but
|
|
54
|
+
still honors ``AGENT_CONFIG_PROJECT_ROOT`` and the anchor walk.
|
|
55
|
+
"""
|
|
56
|
+
root, _ = resolve_project_root(arg)
|
|
57
|
+
return root
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
def _load_manifest(project_root: Path, *, force_empty: bool
|
package/scripts/_cli/cmd_sync.py
CHANGED
|
@@ -19,6 +19,7 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Iterable
|
|
20
20
|
|
|
21
21
|
from scripts._lib import installed_tools
|
|
22
|
+
from scripts._lib.agent_settings import resolve_project_root
|
|
22
23
|
from scripts.install import main as install_main
|
|
23
24
|
|
|
24
25
|
|
|
@@ -108,9 +109,12 @@ def _emit(quiet: bool, msg: str) -> None:
|
|
|
108
109
|
|
|
109
110
|
def main(argv: list[str]) -> int:
|
|
110
111
|
opts = _parse(argv)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
# Phase 3 — shared helper honors AGENT_CONFIG_PROJECT_ROOT (set by
|
|
113
|
+
# the ./agent-config wrapper) and the Step-7 anchor walk. Legacy
|
|
114
|
+
# ``PROJECT_ROOT`` env var is preserved as an explicit fallback so
|
|
115
|
+
# existing CI scripts that set it keep working.
|
|
116
|
+
arg = opts.project or os.environ.get("PROJECT_ROOT")
|
|
117
|
+
project_root, _ = resolve_project_root(arg)
|
|
114
118
|
manifest = installed_tools.manifest_path(project_root)
|
|
115
119
|
data = installed_tools.read_manifest(manifest)
|
|
116
120
|
|
|
@@ -38,14 +38,15 @@ from pathlib import Path
|
|
|
38
38
|
from typing import Any, Iterable
|
|
39
39
|
|
|
40
40
|
from scripts._lib import fs_atomic, installed_lock, installed_tools
|
|
41
|
+
from scripts._lib.agent_settings import resolve_project_root
|
|
41
42
|
from scripts._lib.json_pointers import subtract_pointers
|
|
42
43
|
from scripts.install import PROJECT_BRIDGE_MARKERS, USER_SCOPE_PATHS
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def _resolve_project_root(arg: str | None) -> Path:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return
|
|
47
|
+
"""Resolve the project root using the shared Phase-3 helper."""
|
|
48
|
+
root, _ = resolve_project_root(arg)
|
|
49
|
+
return root
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def _filter_tools(all_tools: Iterable[str], requested: str | None) -> list[str]:
|
|
@@ -42,6 +42,7 @@ from scripts._lib.agent_settings import (
|
|
|
42
42
|
DEFAULT_PROJECT_FILE,
|
|
43
43
|
_resolve_cascade_paths,
|
|
44
44
|
find_project_root,
|
|
45
|
+
resolve_project_root,
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
PACKAGE_NAME = "@event4u/agent-config"
|
|
@@ -164,7 +165,10 @@ def main(
|
|
|
164
165
|
"(without --to there is no source for 'latest').")
|
|
165
166
|
args = parser.parse_args(argv)
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
# Phase 3 — anchor walk + AGENT_CONFIG_PROJECT_ROOT honored so
|
|
169
|
+
# ``agent-config update`` from a subdir writes to the right file.
|
|
170
|
+
# ``cwd`` is kept as a kwarg for test injection.
|
|
171
|
+
cwd, _ = resolve_project_root(None, cwd=cwd)
|
|
168
172
|
installed_version = installed_version or _detect_installed_version()
|
|
169
173
|
state_path = state_path or update_check.DEFAULT_STATE_PATH
|
|
170
174
|
|
|
@@ -22,6 +22,7 @@ from pathlib import Path
|
|
|
22
22
|
from typing import Iterable
|
|
23
23
|
|
|
24
24
|
from scripts._lib import installed_lock, installed_tools
|
|
25
|
+
from scripts._lib.agent_settings import resolve_project_root
|
|
25
26
|
from scripts.install import PROJECT_BRIDGE_MARKERS, USER_SCOPE_PATHS
|
|
26
27
|
|
|
27
28
|
|
|
@@ -121,9 +122,11 @@ def _format(issue: dict) -> str:
|
|
|
121
122
|
|
|
122
123
|
def main(argv: list[str]) -> int:
|
|
123
124
|
opts = _parse(argv)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
# Phase 3 — honor AGENT_CONFIG_PROJECT_ROOT + anchor walk via the
|
|
126
|
+
# shared helper. Legacy ``PROJECT_ROOT`` env var stays as a fallback
|
|
127
|
+
# so existing CI scripts keep working.
|
|
128
|
+
arg = opts.project or os.environ.get("PROJECT_ROOT")
|
|
129
|
+
project_root, _ = resolve_project_root(arg)
|
|
127
130
|
manifest = installed_tools.manifest_path(project_root)
|
|
128
131
|
data = installed_tools.read_manifest(manifest)
|
|
129
132
|
|
|
@@ -18,14 +18,27 @@ import subprocess
|
|
|
18
18
|
import sys
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
from scripts._lib.agent_settings import resolve_project_root
|
|
22
|
+
|
|
21
23
|
PACKAGE_NAME = "@event4u/agent-config"
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def _project_root() -> Path:
|
|
27
|
+
"""Resolve the consumer project root via the Phase-3 helper.
|
|
28
|
+
|
|
29
|
+
Honors ``AGENT_CONFIG_PROJECT_ROOT`` and the Step-7 anchor walk so
|
|
30
|
+
``agent-config versions`` invoked from a subdir reads the correct
|
|
31
|
+
``.agent-settings.yml`` / ``package.json``.
|
|
32
|
+
"""
|
|
33
|
+
root, _ = resolve_project_root(None)
|
|
34
|
+
return root
|
|
35
|
+
|
|
36
|
+
|
|
24
37
|
def _local_package_version() -> str:
|
|
25
38
|
"""Return ``version`` from the local ``package.json``, or ``""`` if absent."""
|
|
26
39
|
candidates = [
|
|
27
40
|
Path(__file__).resolve().parents[2] / "package.json",
|
|
28
|
-
|
|
41
|
+
_project_root() / "package.json",
|
|
29
42
|
]
|
|
30
43
|
for p in candidates:
|
|
31
44
|
if p.exists():
|
|
@@ -38,7 +51,7 @@ def _local_package_version() -> str:
|
|
|
38
51
|
|
|
39
52
|
def _pinned_version() -> str:
|
|
40
53
|
"""Return the ``agent_config_version`` pin from ``.agent-settings.yml``."""
|
|
41
|
-
settings =
|
|
54
|
+
settings = _project_root() / ".agent-settings.yml"
|
|
42
55
|
if not settings.exists():
|
|
43
56
|
return ""
|
|
44
57
|
try:
|