@event4u/agent-config 2.14.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/commands/agents/user/accept.md +117 -0
- package/.agent-src/commands/agents/user/init.md +163 -0
- package/.agent-src/commands/agents/user/review.md +107 -0
- package/.agent-src/commands/agents/user/show.md +109 -0
- package/.agent-src/commands/agents/user/update.md +98 -0
- package/.agent-src/commands/agents/user.md +66 -0
- package/.agent-src/commands/agents.md +2 -0
- 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 +7 -1
- package/CHANGELOG.md +145 -196
- package/README.md +29 -3
- package/config/agent-settings.template.yml +4 -0
- package/config/gitignore-block.txt +11 -0
- package/docs/architecture.md +1 -1
- package/docs/archive/CHANGELOG-pre-2.15.0.md +244 -0
- package/docs/catalog.md +8 -2
- package/docs/contracts/agent-user-schema.md +165 -0
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/file-ownership-matrix.json +190 -0
- package/docs/examples/agent-user.example.md +21 -0
- package/docs/getting-started.md +1 -1
- 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
|
@@ -17,11 +17,21 @@ when shipped into consumer projects) with a read-fallback to the legacy
|
|
|
17
17
|
``~/.config/agent-config/agent-settings.yml`` so pre-2.4 installs keep
|
|
18
18
|
working during the namespace migration.
|
|
19
19
|
|
|
20
|
-
``<repo-root>`` is the nearest ancestor that
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
``<repo-root>`` is the nearest ancestor that anchors the project. As of
|
|
21
|
+
Step 7 the anchor set is (closest-leaf wins; tiebreaker
|
|
22
|
+
``.agent-settings.yml`` > ``agents/`` > ``.git``):
|
|
23
|
+
|
|
24
|
+
* ``.agent-settings.yml`` file,
|
|
25
|
+
* ``agents/`` directory containing ``roadmaps/``, ``.ai-council.yml``,
|
|
26
|
+
or ``roadmaps-progress.md`` (bare ``agents/`` does **not** anchor),
|
|
27
|
+
* ``.git`` file or directory (submodule support).
|
|
28
|
+
|
|
29
|
+
Set ``AGENT_CONFIG_LEGACY_ANCHOR=1`` to revert to the pre-Step-7
|
|
30
|
+
``.git``-only walk for one minor-version soak. The walk stops at the
|
|
31
|
+
first anchor — it never drifts into a parent repo or ``$HOME``. When
|
|
32
|
+
``cwd`` is ``None`` (default), the loader behaves identically to the
|
|
33
|
+
pre-cascade contract: project file + user-global only, no ancestor
|
|
34
|
+
walk. Back-compat is hard.
|
|
25
35
|
|
|
26
36
|
Whitelisted keys (``MERGEABLE_KEYS``) are exact dotted paths. A
|
|
27
37
|
non-whitelisted key in the user-global file is silently ignored — the
|
|
@@ -41,6 +51,7 @@ Contract — pure, read-only, tolerant:
|
|
|
41
51
|
from __future__ import annotations
|
|
42
52
|
|
|
43
53
|
import logging
|
|
54
|
+
import os
|
|
44
55
|
from pathlib import Path
|
|
45
56
|
from typing import Any, Iterator
|
|
46
57
|
|
|
@@ -81,27 +92,295 @@ MERGEABLE_KEYS: tuple[str, ...] = (
|
|
|
81
92
|
_DEFAULTS: dict[str, Any] = {}
|
|
82
93
|
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
#: Anchor identifier returned by :func:`find_project_root_with_anchor`.
|
|
96
|
+
ANCHOR_AGENT_SETTINGS = "agent-settings"
|
|
97
|
+
ANCHOR_AGENTS_DIR = "agents-dir"
|
|
98
|
+
ANCHOR_GIT = "git"
|
|
99
|
+
|
|
100
|
+
#: Marker subpaths that qualify a bare ``agents/`` directory as a project
|
|
101
|
+
#: anchor (D1). Any one is sufficient. Bare ``agents/`` without a marker
|
|
102
|
+
#: is **not** an anchor.
|
|
103
|
+
_AGENTS_DIR_MARKERS: tuple[str, ...] = (
|
|
104
|
+
"roadmaps",
|
|
105
|
+
".ai-council.yml",
|
|
106
|
+
"roadmaps-progress.md",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
#: Kill-switch (D5). When set to ``"1"``, :func:`find_project_root` and
|
|
110
|
+
#: :func:`find_project_root_with_anchor` revert to the pre-Step-7
|
|
111
|
+
#: ``.git``-only walk for one minor-version soak.
|
|
112
|
+
_LEGACY_ANCHOR_ENV = "AGENT_CONFIG_LEGACY_ANCHOR"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _boundary_anchor_at(path: Path) -> str | None:
|
|
116
|
+
"""Return the boundary-anchor name at ``path`` or ``None``.
|
|
86
117
|
|
|
87
|
-
|
|
88
|
-
pointer) or directory (regular checkout), or ``None`` if the walk
|
|
89
|
-
reaches the filesystem root without finding one. The walk stops at
|
|
90
|
-
the project boundary — it never drifts into a parent repo or
|
|
91
|
-
``$HOME``.
|
|
118
|
+
Boundary anchors stop the walk and define the project root:
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
120
|
+
* ``agents/`` containing a D1 marker → ``"agents-dir"``
|
|
121
|
+
* ``.git`` (file or directory) → ``"git"``
|
|
122
|
+
|
|
123
|
+
``.agent-settings.yml`` is a **layer marker**, not a boundary
|
|
124
|
+
anchor (decision: ``step-7-d3-cascade-conflict-decision``). It
|
|
125
|
+
only anchors when no boundary is found in any ancestor — handled
|
|
126
|
+
by :func:`find_project_root_with_anchor` as a second pass.
|
|
127
|
+
|
|
128
|
+
Pure read-only — at most ``1 + len(_AGENTS_DIR_MARKERS)``
|
|
129
|
+
``exists()`` probes per call (D6 perf budget).
|
|
130
|
+
"""
|
|
131
|
+
agents_dir = path / "agents"
|
|
132
|
+
if agents_dir.is_dir():
|
|
133
|
+
for marker in _AGENTS_DIR_MARKERS:
|
|
134
|
+
if (agents_dir / marker).exists():
|
|
135
|
+
return ANCHOR_AGENTS_DIR
|
|
136
|
+
if (path / ".git").exists():
|
|
137
|
+
return ANCHOR_GIT
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def find_project_root_with_anchor(start: Path) -> tuple[Path, str] | None:
|
|
142
|
+
"""Walk up from ``start`` and return ``(root, anchor_name)`` or ``None``.
|
|
143
|
+
|
|
144
|
+
Two-tier lookup (boundary vs layer split — see council decision
|
|
145
|
+
``step-7-d3-cascade-conflict-decision``):
|
|
146
|
+
|
|
147
|
+
1. **Boundary pass.** Walk up from ``start``. First ancestor with
|
|
148
|
+
a boundary anchor wins:
|
|
149
|
+
|
|
150
|
+
* ``agents/`` containing **any** of ``roadmaps/``,
|
|
151
|
+
``.ai-council.yml``, or ``roadmaps-progress.md`` (D1) →
|
|
152
|
+
``"agents-dir"``
|
|
153
|
+
* ``.git`` (file or directory; submodule support) → ``"git"``
|
|
154
|
+
|
|
155
|
+
When both coexist at the same ancestor, ``agents/`` wins
|
|
156
|
+
(D3 ordering minus the layer marker).
|
|
157
|
+
|
|
158
|
+
2. **Layer fallback.** No boundary found in the chain. Walk again
|
|
159
|
+
and return the **outermost** ancestor containing
|
|
160
|
+
``.agent-settings.yml`` → ``"agent-settings"``. This delivers
|
|
161
|
+
Step-7's minimal-init goal without breaking the cascade.
|
|
162
|
+
|
|
163
|
+
When ``AGENT_CONFIG_LEGACY_ANCHOR=1`` is set (D5 kill-switch), only
|
|
164
|
+
the ``.git`` anchor is considered.
|
|
165
|
+
|
|
166
|
+
Pure read-only; never writes, never raises on missing paths.
|
|
95
167
|
"""
|
|
96
168
|
current = start.resolve() if start.exists() else start
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
169
|
+
legacy = os.environ.get(_LEGACY_ANCHOR_ENV) == "1"
|
|
170
|
+
chain = [current, *current.parents]
|
|
171
|
+
if legacy:
|
|
172
|
+
for candidate in chain:
|
|
173
|
+
if (candidate / ".git").exists():
|
|
174
|
+
return candidate, ANCHOR_GIT
|
|
175
|
+
return None
|
|
176
|
+
# Boundary pass.
|
|
177
|
+
for candidate in chain:
|
|
178
|
+
anchor = _boundary_anchor_at(candidate)
|
|
179
|
+
if anchor is not None:
|
|
180
|
+
return candidate, anchor
|
|
181
|
+
# Layer fallback — outermost .agent-settings.yml wins so the
|
|
182
|
+
# cascade can layer deeper files below it.
|
|
183
|
+
outermost: Path | None = None
|
|
184
|
+
for candidate in chain:
|
|
185
|
+
if (candidate / DEFAULT_PROJECT_FILE).exists():
|
|
186
|
+
outermost = candidate
|
|
187
|
+
if outermost is not None:
|
|
188
|
+
return outermost, ANCHOR_AGENT_SETTINGS
|
|
102
189
|
return None
|
|
103
190
|
|
|
104
191
|
|
|
192
|
+
def find_project_root(start: Path) -> Path | None:
|
|
193
|
+
"""Walk up from ``start`` and return the project root or ``None``.
|
|
194
|
+
|
|
195
|
+
Thin wrapper over :func:`find_project_root_with_anchor` that drops
|
|
196
|
+
the anchor-name component. Kept for back-compat — every pre-Step-7
|
|
197
|
+
caller already takes a ``Path | None`` here.
|
|
198
|
+
"""
|
|
199
|
+
result = find_project_root_with_anchor(start)
|
|
200
|
+
return result[0] if result is not None else None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def find_project_root_with_trace(
|
|
204
|
+
start: Path,
|
|
205
|
+
) -> tuple[Path | None, str | None, list[dict[str, Any]]]:
|
|
206
|
+
"""Walk up from ``start`` and return ``(root, anchor, trace)``.
|
|
207
|
+
|
|
208
|
+
Step 8 A1 — diagnostic variant of :func:`find_project_root_with_anchor`.
|
|
209
|
+
Returns the same ``(root, anchor)`` pair (or ``(None, None)`` when no
|
|
210
|
+
anchor is found) plus an ordered list of trace records describing
|
|
211
|
+
every ancestor probed.
|
|
212
|
+
|
|
213
|
+
Each trace record is a dict:
|
|
214
|
+
|
|
215
|
+
* ``ancestor`` — absolute path probed (string).
|
|
216
|
+
* ``pass`` — ``"boundary"`` or ``"layer"``.
|
|
217
|
+
* ``hit`` — anchor name on hit, ``None`` on miss.
|
|
218
|
+
* ``reason`` — one-line explanation (``agents/ has roadmaps/``,
|
|
219
|
+
``no .git``, ``layer marker``, ``legacy: only .git considered``,
|
|
220
|
+
etc.).
|
|
221
|
+
|
|
222
|
+
Pure read-only. No additional ``exists()`` cost beyond
|
|
223
|
+
:func:`find_project_root_with_anchor` — the trace records reuse the
|
|
224
|
+
same probes.
|
|
225
|
+
"""
|
|
226
|
+
trace: list[dict[str, Any]] = []
|
|
227
|
+
current = start.resolve() if start.exists() else start
|
|
228
|
+
legacy = os.environ.get(_LEGACY_ANCHOR_ENV) == "1"
|
|
229
|
+
chain = [current, *current.parents]
|
|
230
|
+
|
|
231
|
+
if legacy:
|
|
232
|
+
for candidate in chain:
|
|
233
|
+
hit = (candidate / ".git").exists()
|
|
234
|
+
trace.append({
|
|
235
|
+
"ancestor": str(candidate),
|
|
236
|
+
"pass": "boundary",
|
|
237
|
+
"hit": ANCHOR_GIT if hit else None,
|
|
238
|
+
"reason": (
|
|
239
|
+
"legacy: .git found" if hit
|
|
240
|
+
else "legacy: no .git"
|
|
241
|
+
),
|
|
242
|
+
})
|
|
243
|
+
if hit:
|
|
244
|
+
return candidate, ANCHOR_GIT, trace
|
|
245
|
+
return None, None, trace
|
|
246
|
+
|
|
247
|
+
# Boundary pass — same probes as find_project_root_with_anchor.
|
|
248
|
+
for candidate in chain:
|
|
249
|
+
agents_dir = candidate / "agents"
|
|
250
|
+
if agents_dir.is_dir():
|
|
251
|
+
for marker in _AGENTS_DIR_MARKERS:
|
|
252
|
+
if (agents_dir / marker).exists():
|
|
253
|
+
trace.append({
|
|
254
|
+
"ancestor": str(candidate),
|
|
255
|
+
"pass": "boundary",
|
|
256
|
+
"hit": ANCHOR_AGENTS_DIR,
|
|
257
|
+
"reason": f"agents/ has {marker}",
|
|
258
|
+
})
|
|
259
|
+
return candidate, ANCHOR_AGENTS_DIR, trace
|
|
260
|
+
if (candidate / ".git").exists():
|
|
261
|
+
trace.append({
|
|
262
|
+
"ancestor": str(candidate),
|
|
263
|
+
"pass": "boundary",
|
|
264
|
+
"hit": ANCHOR_GIT,
|
|
265
|
+
"reason": ".git present",
|
|
266
|
+
})
|
|
267
|
+
return candidate, ANCHOR_GIT, trace
|
|
268
|
+
trace.append({
|
|
269
|
+
"ancestor": str(candidate),
|
|
270
|
+
"pass": "boundary",
|
|
271
|
+
"hit": None,
|
|
272
|
+
"reason": "no agents/ marker, no .git",
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
# Layer fallback — outermost .agent-settings.yml wins.
|
|
276
|
+
outermost: Path | None = None
|
|
277
|
+
for candidate in chain:
|
|
278
|
+
present = (candidate / DEFAULT_PROJECT_FILE).exists()
|
|
279
|
+
trace.append({
|
|
280
|
+
"ancestor": str(candidate),
|
|
281
|
+
"pass": "layer",
|
|
282
|
+
"hit": ANCHOR_AGENT_SETTINGS if present else None,
|
|
283
|
+
"reason": (
|
|
284
|
+
f"{DEFAULT_PROJECT_FILE} present" if present
|
|
285
|
+
else f"no {DEFAULT_PROJECT_FILE}"
|
|
286
|
+
),
|
|
287
|
+
})
|
|
288
|
+
if present:
|
|
289
|
+
outermost = candidate
|
|
290
|
+
if outermost is not None:
|
|
291
|
+
return outermost, ANCHOR_AGENT_SETTINGS, trace
|
|
292
|
+
return None, None, trace
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
#: Origin tag returned by :func:`resolve_project_root` alongside the
|
|
296
|
+
#: anchor names defined above. Distinct values let callers (doctor,
|
|
297
|
+
#: tests, future telemetry) surface *how* the root was chosen.
|
|
298
|
+
ORIGIN_ROOT_FLAG = "root-flag" # --root global flag (Step 8 A3)
|
|
299
|
+
ORIGIN_EXPLICIT = "explicit" # --project arg on a subcommand
|
|
300
|
+
ORIGIN_ENV = "env" # AGENT_CONFIG_PROJECT_ROOT (wrapper-pinned)
|
|
301
|
+
ORIGIN_CWD_FALLBACK = "cwd-fallback" # no anchor found
|
|
302
|
+
|
|
303
|
+
PROJECT_ROOT_ENV = "AGENT_CONFIG_PROJECT_ROOT"
|
|
304
|
+
ROOT_OVERRIDE_ENV = "AGENT_CONFIG_ROOT_OVERRIDE"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ProjectRootError(Exception):
|
|
308
|
+
"""Raised when an explicit project-root override points to an invalid path.
|
|
309
|
+
|
|
310
|
+
Step 8 A3: ``--root <path>`` and ``AGENT_CONFIG_PROJECT_ROOT`` must
|
|
311
|
+
fail loudly when the target does not exist or is not a directory.
|
|
312
|
+
Callers translate this into exit code 2 (no silent CWD fallback).
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _validate_root_path(path: Path, origin_label: str) -> Path:
|
|
317
|
+
"""Resolve ``path``; raise :class:`ProjectRootError` when invalid.
|
|
318
|
+
|
|
319
|
+
``origin_label`` is one of ``--root``, ``AGENT_CONFIG_PROJECT_ROOT``,
|
|
320
|
+
or ``--project``; surfaced verbatim in the error message so the
|
|
321
|
+
operator can see which channel injected the bad value.
|
|
322
|
+
"""
|
|
323
|
+
resolved = Path(path).expanduser()
|
|
324
|
+
if not resolved.exists():
|
|
325
|
+
raise ProjectRootError(
|
|
326
|
+
f"{origin_label} points to a path that does not exist: {resolved}",
|
|
327
|
+
)
|
|
328
|
+
if not resolved.is_dir():
|
|
329
|
+
raise ProjectRootError(
|
|
330
|
+
f"{origin_label} points to a non-directory: {resolved}",
|
|
331
|
+
)
|
|
332
|
+
return resolved.resolve()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def resolve_project_root(
|
|
336
|
+
arg: str | Path | None,
|
|
337
|
+
*,
|
|
338
|
+
cwd: Path | None = None,
|
|
339
|
+
) -> tuple[Path, str]:
|
|
340
|
+
"""Return ``(root, origin)`` for any ``cmd_*`` entry point.
|
|
341
|
+
|
|
342
|
+
Resolution order (Step 8 A3 — explicit override hardening):
|
|
343
|
+
|
|
344
|
+
1. ``AGENT_CONFIG_PROJECT_ROOT`` env var with
|
|
345
|
+
``AGENT_CONFIG_ROOT_OVERRIDE=1`` set by the master CLI's ``--root``
|
|
346
|
+
flag → ``ORIGIN_ROOT_FLAG``. Fail-loud on invalid path.
|
|
347
|
+
2. Explicit ``--project`` / ``--target`` argument → ``ORIGIN_EXPLICIT``.
|
|
348
|
+
Fail-loud on invalid path.
|
|
349
|
+
3. ``AGENT_CONFIG_PROJECT_ROOT`` environment variable, set by the
|
|
350
|
+
project-local ``./agent-config`` wrapper → ``ORIGIN_ENV``.
|
|
351
|
+
Fail-loud on invalid path.
|
|
352
|
+
4. Anchor walk from ``cwd`` via
|
|
353
|
+
:func:`find_project_root_with_anchor` → anchor name
|
|
354
|
+
(``agents-dir`` / ``git`` / ``agent-settings``).
|
|
355
|
+
5. Fall back to ``cwd`` itself → ``ORIGIN_CWD_FALLBACK``.
|
|
356
|
+
|
|
357
|
+
The ``--root`` channel wins over a subcommand-level ``--project``
|
|
358
|
+
because it is a deliberate global override (Step 8 council decision).
|
|
359
|
+
Wrapper-set env (3) still wins over the anchor walk so subdir
|
|
360
|
+
invocations stay pinned.
|
|
361
|
+
|
|
362
|
+
Raises :class:`ProjectRootError` when any explicit override points
|
|
363
|
+
to a missing path or non-directory — callers map this to exit 2.
|
|
364
|
+
"""
|
|
365
|
+
if os.environ.get(ROOT_OVERRIDE_ENV) == "1":
|
|
366
|
+
env_value = os.environ.get(PROJECT_ROOT_ENV)
|
|
367
|
+
if env_value:
|
|
368
|
+
return _validate_root_path(Path(env_value), "--root"), ORIGIN_ROOT_FLAG
|
|
369
|
+
if arg is not None and str(arg) != "":
|
|
370
|
+
return _validate_root_path(Path(arg), "--project"), ORIGIN_EXPLICIT
|
|
371
|
+
env_value = os.environ.get(PROJECT_ROOT_ENV)
|
|
372
|
+
if env_value:
|
|
373
|
+
return (
|
|
374
|
+
_validate_root_path(Path(env_value), PROJECT_ROOT_ENV),
|
|
375
|
+
ORIGIN_ENV,
|
|
376
|
+
)
|
|
377
|
+
start = (cwd or Path.cwd()).resolve()
|
|
378
|
+
walked = find_project_root_with_anchor(start)
|
|
379
|
+
if walked is not None:
|
|
380
|
+
return walked
|
|
381
|
+
return start, ORIGIN_CWD_FALLBACK
|
|
382
|
+
|
|
383
|
+
|
|
105
384
|
def _resolve_cascade_paths(
|
|
106
385
|
cwd: Path | None,
|
|
107
386
|
project_path: Path | str | None,
|
|
@@ -111,7 +390,7 @@ def _resolve_cascade_paths(
|
|
|
111
390
|
When ``cwd`` is provided and ``find_project_root(cwd)`` succeeds, the
|
|
112
391
|
list contains every ``<dir>/.agent-settings.yml`` from the repo root
|
|
113
392
|
down to ``cwd`` (inclusive on both ends), shallowest first. When
|
|
114
|
-
``cwd`` is ``None`` or no
|
|
393
|
+
``cwd`` is ``None`` or no anchor is reached, falls back to the
|
|
115
394
|
single legacy project path — back-compat with the pre-cascade
|
|
116
395
|
loader.
|
|
117
396
|
"""
|
package/scripts/agent-config
CHANGED
|
@@ -865,6 +865,70 @@ run_update_check_banner() {
|
|
|
865
865
|
python3 "$banner_script" --cwd "$CONSUMER_ROOT" 2>/dev/null || true
|
|
866
866
|
}
|
|
867
867
|
|
|
868
|
+
# Global `--root <path>` / `--root=<path>` parsing (Step 8 A3).
|
|
869
|
+
# Strips the flag from $@, validates the path is an existing directory,
|
|
870
|
+
# and exports `AGENT_CONFIG_PROJECT_ROOT` + `AGENT_CONFIG_ROOT_OVERRIDE=1`
|
|
871
|
+
# so the Python resolver picks origin=root-flag with fail-loud semantics.
|
|
872
|
+
# Invalid path → exit 2 immediately, no fallback to anchor walk or CWD.
|
|
873
|
+
parse_global_root_flag() {
|
|
874
|
+
local -a filtered=()
|
|
875
|
+
local root_value=""
|
|
876
|
+
local saw_flag=false
|
|
877
|
+
while [[ $# -gt 0 ]]; do
|
|
878
|
+
case "$1" in
|
|
879
|
+
--root)
|
|
880
|
+
saw_flag=true
|
|
881
|
+
if [[ $# -lt 2 ]]; then
|
|
882
|
+
echo "❌ agent-config: --root requires a path argument" >&2
|
|
883
|
+
exit 2
|
|
884
|
+
fi
|
|
885
|
+
root_value="$2"
|
|
886
|
+
shift 2
|
|
887
|
+
;;
|
|
888
|
+
--root=*)
|
|
889
|
+
saw_flag=true
|
|
890
|
+
root_value="${1#--root=}"
|
|
891
|
+
shift
|
|
892
|
+
;;
|
|
893
|
+
*)
|
|
894
|
+
filtered+=("$1")
|
|
895
|
+
shift
|
|
896
|
+
;;
|
|
897
|
+
esac
|
|
898
|
+
done
|
|
899
|
+
if $saw_flag; then
|
|
900
|
+
if [[ -z "$root_value" ]]; then
|
|
901
|
+
echo "❌ agent-config: --root requires a non-empty path" >&2
|
|
902
|
+
exit 2
|
|
903
|
+
fi
|
|
904
|
+
if [[ ! -e "$root_value" ]]; then
|
|
905
|
+
echo "❌ agent-config: --root points to a path that does not exist: $root_value" >&2
|
|
906
|
+
exit 2
|
|
907
|
+
fi
|
|
908
|
+
if [[ ! -d "$root_value" ]]; then
|
|
909
|
+
echo "❌ agent-config: --root points to a non-directory: $root_value" >&2
|
|
910
|
+
exit 2
|
|
911
|
+
fi
|
|
912
|
+
# Absolutize so downstream Python sees a fully-resolved path.
|
|
913
|
+
root_value="$(cd "$root_value" && pwd)"
|
|
914
|
+
export AGENT_CONFIG_PROJECT_ROOT="$root_value"
|
|
915
|
+
export AGENT_CONFIG_ROOT_OVERRIDE=1
|
|
916
|
+
# Wrapper-coupling guard: when invoked through a consumer-root wrapper
|
|
917
|
+
# (CONSUMER_ROOT != root_value), surface a one-line warning on stderr
|
|
918
|
+
# so the operator notices the divergence. Non-fatal: --root is the
|
|
919
|
+
# deliberate override channel.
|
|
920
|
+
if [[ "$CONSUMER_ROOT" != "$root_value" ]]; then
|
|
921
|
+
echo "⚠️ agent-config: --root ($root_value) differs from wrapper CWD ($CONSUMER_ROOT)" >&2
|
|
922
|
+
fi
|
|
923
|
+
fi
|
|
924
|
+
# Re-emit the filtered argv via a global array consumed by main().
|
|
925
|
+
GLOBAL_FILTERED_ARGS=("${filtered[@]+"${filtered[@]}"}")
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
declare -a GLOBAL_FILTERED_ARGS
|
|
929
|
+
parse_global_root_flag "$@"
|
|
930
|
+
set -- "${GLOBAL_FILTERED_ARGS[@]+"${GLOBAL_FILTERED_ARGS[@]}"}"
|
|
931
|
+
|
|
868
932
|
# Pin re-exec runs before dispatch — if it triggers, the process is
|
|
869
933
|
# replaced and nothing else here matters.
|
|
870
934
|
maybe_pin_reexec "$@"
|
package/scripts/install
CHANGED
|
@@ -44,6 +44,12 @@
|
|
|
44
44
|
# for child subprocesses. All bridge content is bundled
|
|
45
45
|
# in the package, so install itself is already offline-safe;
|
|
46
46
|
# this flag is the explicit air-gap / CI guarantee.
|
|
47
|
+
# --minimal Bootstrap only `.agent-settings.yml`, `agents/.gitkeep`,
|
|
48
|
+
# and the `./agent-config` wrapper. No tool payload, no
|
|
49
|
+
# AGENTS.md, no symlinks. Refuses to install inside an
|
|
50
|
+
# existing agent-config project (nested-install guard).
|
|
51
|
+
# See docs/installation.md → "Minimal init".
|
|
52
|
+
# --settings-only Alias for --minimal.
|
|
47
53
|
# --help, -h Show this help
|
|
48
54
|
#
|
|
49
55
|
# Examples:
|
|
@@ -75,12 +81,13 @@ GLOBAL=false
|
|
|
75
81
|
SCOPE=""
|
|
76
82
|
CUSTOM_PATH=""
|
|
77
83
|
OFFLINE=false
|
|
84
|
+
MINIMAL=false
|
|
78
85
|
|
|
79
86
|
# Single source of truth for valid tool IDs (also referenced by install.sh / install.py).
|
|
80
87
|
VALID_TOOLS="claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex roocode continue kilocode zed jetbrains kiro all"
|
|
81
88
|
|
|
82
89
|
show_help() {
|
|
83
|
-
sed -n '3,
|
|
90
|
+
sed -n '3,54p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
list_tools() {
|
|
@@ -157,6 +164,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
157
164
|
--custom-path) CUSTOM_PATH="$2"; shift 2 ;;
|
|
158
165
|
--custom-path=*) CUSTOM_PATH="${1#*=}"; shift ;;
|
|
159
166
|
--offline) OFFLINE=true; shift ;;
|
|
167
|
+
--minimal|--settings-only)
|
|
168
|
+
MINIMAL=true; shift ;;
|
|
160
169
|
--help|-h) show_help; exit 0 ;;
|
|
161
170
|
*) err "Unknown argument: $1"; show_help >&2; exit 1 ;;
|
|
162
171
|
esac
|
|
@@ -202,7 +211,7 @@ prompt_tools() {
|
|
|
202
211
|
echo " ✅ Selected: $TOOLS"
|
|
203
212
|
}
|
|
204
213
|
|
|
205
|
-
if ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && [[ -t 0 && -t 1 ]]; then
|
|
214
|
+
if ! $MINIMAL && ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && [[ -t 0 && -t 1 ]]; then
|
|
206
215
|
prompt_tools
|
|
207
216
|
TOOLS_EXPLICIT=true
|
|
208
217
|
fi
|
|
@@ -280,6 +289,7 @@ run_sync() {
|
|
|
280
289
|
$DRY_RUN && args+=(--dry-run)
|
|
281
290
|
$VERBOSE && args+=(--verbose)
|
|
282
291
|
$QUIET && args+=(--quiet)
|
|
292
|
+
$MINIMAL && args+=(--minimal)
|
|
283
293
|
args+=(--tools="$TOOLS")
|
|
284
294
|
bash "$INSTALL_SH" "${args[@]}"
|
|
285
295
|
}
|
|
@@ -305,12 +315,39 @@ run_bridges() {
|
|
|
305
315
|
[[ -n "$SCOPE" ]] && args+=(--scope="$SCOPE")
|
|
306
316
|
[[ -n "$CUSTOM_PATH" ]] && args+=(--custom-path="$CUSTOM_PATH")
|
|
307
317
|
$OFFLINE && args+=(--offline)
|
|
318
|
+
$MINIMAL && args+=(--minimal)
|
|
308
319
|
args+=(--tools="$TOOLS")
|
|
309
320
|
"$python_bin" "$INSTALL_PY" "${args[@]}"
|
|
310
321
|
}
|
|
311
322
|
|
|
312
323
|
RC=0
|
|
313
324
|
|
|
325
|
+
# Minimal init runs the bridge stage *first* so its nested-install guard
|
|
326
|
+
# (Step 7 Phase 2) fires before any wrapper / file is written. The
|
|
327
|
+
# payload sync stage is then a no-op in minimal mode (install.sh
|
|
328
|
+
# short-circuits) but is still invoked so it can install the
|
|
329
|
+
# `./agent-config` wrapper on a confirmed-clean target.
|
|
330
|
+
if $MINIMAL; then
|
|
331
|
+
if ! $SKIP_BRIDGES; then
|
|
332
|
+
if [[ ! -f "$INSTALL_PY" ]]; then
|
|
333
|
+
err "Missing $INSTALL_PY"
|
|
334
|
+
exit 1
|
|
335
|
+
fi
|
|
336
|
+
run_bridges || RC=$?
|
|
337
|
+
fi
|
|
338
|
+
if [[ $RC -ne 0 ]]; then
|
|
339
|
+
exit $RC
|
|
340
|
+
fi
|
|
341
|
+
if ! $SKIP_SYNC; then
|
|
342
|
+
if [[ ! -f "$INSTALL_SH" ]]; then
|
|
343
|
+
err "Missing $INSTALL_SH"
|
|
344
|
+
exit 1
|
|
345
|
+
fi
|
|
346
|
+
run_sync || RC=$?
|
|
347
|
+
fi
|
|
348
|
+
exit $RC
|
|
349
|
+
fi
|
|
350
|
+
|
|
314
351
|
if ! $SKIP_SYNC; then
|
|
315
352
|
if [[ ! -f "$INSTALL_SH" ]]; then
|
|
316
353
|
err "Missing $INSTALL_SH"
|