@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.
@@ -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 contains ``.git`` (directory
21
- **or** file submodule support). The walk stops there — it never drifts
22
- into a parent repo or ``$HOME``. When ``cwd`` is ``None`` (default), the
23
- loader behaves identically to the pre-cascade contract: project file +
24
- user-global only, no ancestor walk. Back-compat is hard.
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
- def find_project_root(start: Path) -> Path | None:
85
- """Walk up from ``start`` looking for ``.git`` (file or directory).
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
- Returns the first ancestor that contains ``.git`` as a file (submodule
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
- Pure read-only; never touches the filesystem beyond ``exists()``
94
- probes on the ``.git`` entry.
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
- # ``Path.parents`` excludes ``current`` itself, so probe it first.
98
- for candidate in [current, *current.parents]:
99
- git_marker = candidate / ".git"
100
- if git_marker.exists():
101
- return candidate
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 ``.git`` is reached, falls back to the
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
  """
@@ -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,48p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
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"