@event4u/agent-config 2.15.0 → 2.17.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.
Files changed (106) hide show
  1. package/.agent-src/commands/ghostwriter/delete.md +118 -0
  2. package/.agent-src/commands/ghostwriter/fetch.md +185 -0
  3. package/.agent-src/commands/ghostwriter/list.md +102 -0
  4. package/.agent-src/commands/ghostwriter/show.md +113 -0
  5. package/.agent-src/commands/ghostwriter/write.md +160 -0
  6. package/.agent-src/commands/ghostwriter.md +96 -0
  7. package/.agent-src/commands/post-as/ghostwriter.md +66 -0
  8. package/.agent-src/commands/post-as/me.md +124 -0
  9. package/.agent-src/commands/post-as.md +58 -0
  10. package/.agent-src/ghostwriter/README.md +61 -0
  11. package/.agent-src/ghostwriter/fictional-fixture-v1.md +94 -0
  12. package/.agent-src/personas/README.md +8 -0
  13. package/.agent-src/rules/domain-safety-disclaimer-consulting.md +52 -0
  14. package/.agent-src/rules/domain-safety-disclaimer-financial.md +54 -0
  15. package/.agent-src/rules/domain-safety-disclaimer-legal.md +49 -0
  16. package/.agent-src/rules/domain-safety-disclaimer-medical.md +56 -0
  17. package/.agent-src/rules/domain-safety-export-redact.md +65 -0
  18. package/.agent-src/rules/domain-safety-logging-pii-floor.md +55 -0
  19. package/.agent-src/rules/domain-safety-pii-finance.md +57 -0
  20. package/.agent-src/rules/domain-safety-pii-marketing.md +60 -0
  21. package/.agent-src/rules/domain-safety-pii-recruiting.md +56 -0
  22. package/.agent-src/rules/domain-safety-pii-support.md +57 -0
  23. package/.agent-src/rules/domain-safety-retention-finance.md +48 -0
  24. package/.agent-src/rules/domain-safety-retention-support.md +55 -0
  25. package/.agent-src/skills/api-design/SKILL.md +3 -0
  26. package/.agent-src/skills/authz-review/SKILL.md +3 -0
  27. package/.agent-src/skills/competitive-moat-analysis/SKILL.md +3 -0
  28. package/.agent-src/skills/competitive-positioning/SKILL.md +3 -0
  29. package/.agent-src/skills/content-funnel-design/SKILL.md +3 -0
  30. package/.agent-src/skills/contracts-cognition/SKILL.md +3 -0
  31. package/.agent-src/skills/dashboard-design/SKILL.md +3 -0
  32. package/.agent-src/skills/data-handling-judgment/SKILL.md +3 -0
  33. package/.agent-src/skills/dcf-modeling/SKILL.md +3 -0
  34. package/.agent-src/skills/deal-qualification-meddic/SKILL.md +3 -0
  35. package/.agent-src/skills/discovery-interview/SKILL.md +3 -0
  36. package/.agent-src/skills/editorial-calendar/SKILL.md +3 -0
  37. package/.agent-src/skills/forecast-accuracy/SKILL.md +3 -0
  38. package/.agent-src/skills/forecasting/SKILL.md +3 -0
  39. package/.agent-src/skills/fundraising-narrative/SKILL.md +3 -0
  40. package/.agent-src/skills/gtm-launch/SKILL.md +3 -0
  41. package/.agent-src/skills/incident-commander/SKILL.md +3 -0
  42. package/.agent-src/skills/launch-readiness/SKILL.md +3 -0
  43. package/.agent-src/skills/messaging-architecture/SKILL.md +3 -0
  44. package/.agent-src/skills/okr-tree-modeling/SKILL.md +3 -0
  45. package/.agent-src/skills/pipeline-strategy/SKILL.md +3 -0
  46. package/.agent-src/skills/playwright-architect/SKILL.md +3 -0
  47. package/.agent-src/skills/privacy-review/SKILL.md +4 -1
  48. package/.agent-src/skills/quality-tools/SKILL.md +3 -0
  49. package/.agent-src/skills/release-comms/SKILL.md +3 -0
  50. package/.agent-src/skills/runway-cognition/SKILL.md +3 -0
  51. package/.agent-src/skills/scenario-modeling/SKILL.md +3 -0
  52. package/.agent-src/skills/secrets-management/SKILL.md +3 -0
  53. package/.agent-src/skills/tech-debt-tracker/SKILL.md +3 -0
  54. package/.agent-src/skills/unit-economics-modeling/SKILL.md +3 -0
  55. package/.agent-src/skills/voc-extract/SKILL.md +3 -0
  56. package/.agent-src/skills/voice-and-tone-design/SKILL.md +3 -0
  57. package/.agent-src/templates/agents/agent-project-settings.example.yml +16 -1
  58. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +299 -20
  59. package/.claude-plugin/marketplace.json +10 -1
  60. package/CHANGELOG.md +200 -211
  61. package/README.md +55 -23
  62. package/config/gitignore-block.txt +8 -0
  63. package/docs/announcements/2026-05-non-dev-launch.md +79 -0
  64. package/docs/architecture.md +2 -2
  65. package/docs/archive/CHANGELOG-pre-2.15.0.md +244 -0
  66. package/docs/case-studies/_template.md +60 -0
  67. package/docs/catalog.md +24 -3
  68. package/docs/contracts/agent-user-schema.md +1 -0
  69. package/docs/contracts/command-clusters.md +2 -0
  70. package/docs/contracts/file-ownership-matrix.json +490 -0
  71. package/docs/contracts/ghostwriter-schema.md +337 -0
  72. package/docs/contracts/init-telemetry.md +133 -0
  73. package/docs/contracts/router-blending.md +71 -0
  74. package/docs/contracts/universal-skills.md +92 -0
  75. package/docs/contracts/write-engine.md +142 -0
  76. package/docs/getting-started-by-role.md +89 -0
  77. package/docs/getting-started-laravel.md +72 -0
  78. package/docs/getting-started.md +2 -2
  79. package/docs/installation.md +221 -2
  80. package/docs/safety.md +30 -0
  81. package/package.json +1 -1
  82. package/scripts/_cli/cmd_doctor.py +238 -8
  83. package/scripts/_cli/cmd_migrate.py +6 -1
  84. package/scripts/_cli/cmd_prune.py +8 -3
  85. package/scripts/_cli/cmd_sync.py +7 -3
  86. package/scripts/_cli/cmd_uninstall.py +4 -3
  87. package/scripts/_cli/cmd_update.py +5 -1
  88. package/scripts/_cli/cmd_validate.py +6 -3
  89. package/scripts/_cli/cmd_versions.py +15 -2
  90. package/scripts/_lib/agent_settings.py +299 -20
  91. package/scripts/agent-config +64 -0
  92. package/scripts/bench_runner.py +158 -0
  93. package/scripts/check_role_doc_links.py +110 -0
  94. package/scripts/compress.py +11 -0
  95. package/scripts/ghostwriter_fixture_allowlist.txt +16 -0
  96. package/scripts/install +39 -2
  97. package/scripts/install.py +304 -1
  98. package/scripts/install.sh +20 -0
  99. package/scripts/lint_ghostwriter_source.py +240 -0
  100. package/scripts/measure_skill_reduction.py +102 -0
  101. package/scripts/schemas/rule.schema.json +5 -0
  102. package/scripts/schemas/skill.schema.json +6 -0
  103. package/scripts/update-github-metadata.sh +84 -0
  104. package/templates/agent-config-wrapper.sh +7 -0
  105. package/templates/minimal/.agent-settings.yml +23 -0
  106. 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
- if arg:
61
- return Path(arg).expanduser().resolve()
62
- return Path.cwd().resolve()
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
- project_root = _resolve_project_root(opts.project)
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(f"❌ doctor: no project lockfile at {manifest_pth}",
961
- file=sys.stderr)
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
- project = (cwd or Path.cwd()).resolve()
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
- if arg:
51
- return Path(arg).expanduser().resolve()
52
- return Path.cwd().resolve()
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
@@ -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
- project_root = Path(
112
- opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()
113
- ).resolve()
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
- if arg:
47
- return Path(arg).expanduser().resolve()
48
- return Path.cwd().resolve()
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
- cwd = (cwd or Path.cwd()).resolve()
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
- project_root = Path(
125
- opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()
126
- ).resolve()
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
- Path.cwd() / "package.json",
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 = Path.cwd() / ".agent-settings.yml"
54
+ settings = _project_root() / ".agent-settings.yml"
42
55
  if not settings.exists():
43
56
  return ""
44
57
  try: