@event4u/agent-config 1.41.2 → 2.1.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 (48) hide show
  1. package/.agent-src/commands/fix/{pr-bots.md → pr-bot-comments.md} +3 -3
  2. package/.agent-src/commands/fix/{pr.md → pr-comments.md} +6 -6
  3. package/.agent-src/commands/fix/{pr-developers.md → pr-developer-comments.md} +3 -3
  4. package/.agent-src/commands/fix.md +6 -6
  5. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +2 -2
  6. package/.agent-src/templates/agents/agent-project-settings.example.yml +14 -0
  7. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +120 -11
  8. package/.claude-plugin/marketplace.json +4 -4
  9. package/CHANGELOG.md +54 -0
  10. package/README.md +39 -31
  11. package/config/agent-settings.template.yml +25 -0
  12. package/docs/architecture.md +47 -1
  13. package/docs/catalog.md +3 -3
  14. package/docs/contracts/command-clusters.md +3 -3
  15. package/docs/contracts/file-ownership-matrix.json +9 -9
  16. package/docs/customization.md +125 -9
  17. package/docs/getting-started.md +16 -25
  18. package/docs/installation.md +66 -82
  19. package/docs/migration/v1-to-v2.md +98 -0
  20. package/docs/migrations/commands-1.15.0.md +3 -3
  21. package/docs/setup/per-ide/claude-code.md +0 -17
  22. package/docs/setup/per-ide/claude-desktop.md +35 -48
  23. package/docs/setup/per-ide/windsurf.md +0 -11
  24. package/docs/skills-catalog.md +23 -2
  25. package/docs/troubleshooting.md +20 -32
  26. package/llms.txt +22 -1
  27. package/package.json +1 -6
  28. package/scripts/_cli/__init__.py +0 -0
  29. package/scripts/_cli/cmd_migrate.py +270 -0
  30. package/scripts/_cli/cmd_update.py +226 -0
  31. package/scripts/_lib/agent_settings.py +120 -11
  32. package/scripts/_lib/agents_overlay.py +109 -0
  33. package/scripts/_lib/pin_resolver.py +152 -0
  34. package/scripts/_lib/update_check.py +183 -0
  35. package/scripts/agent-config +73 -1
  36. package/scripts/check_overlay_cascade_subdirs.py +125 -0
  37. package/scripts/check_template_pin_drift.py +112 -0
  38. package/scripts/check_update_banner.py +86 -0
  39. package/scripts/install +27 -40
  40. package/scripts/install.py +17 -228
  41. package/scripts/install.sh +6 -11
  42. package/templates/agent-config-wrapper.sh +40 -25
  43. package/templates/consumer-settings/README.md +2 -2
  44. package/bin/install.php +0 -45
  45. package/composer.json +0 -33
  46. package/scripts/postinstall.sh +0 -76
  47. package/scripts/setup.sh +0 -230
  48. package/templates/global-install-manifest.yml +0 -91
@@ -94,6 +94,10 @@ Commands:
94
94
  Usage: council:run <question> --output <path> --confirm
95
95
  council:render Re-render a saved council responses JSON to markdown
96
96
  Usage: council:render <responses.json>
97
+ update Update the agent_config_version pin in .agent-settings.yml
98
+ Flags: --check (read-only) | --to <version> (explicit pin)
99
+ migrate One-shot migration off legacy composer / npm install paths
100
+ Flags: --dry-run (detect only)
97
101
  help Show this help
98
102
  --version, -V Print package version
99
103
 
@@ -504,6 +508,22 @@ cmd_council() {
504
508
  exec env PYTHONPATH="$PACKAGE_ROOT" python3 "$script" "$sub" "$@"
505
509
  }
506
510
 
511
+ # `agent-config update` — flip the agent_config_version pin in
512
+ # .agent-settings.yml. See scripts/_cli/cmd_update.py (P3.1 of
513
+ # road-to-portable-runtime-and-update-check.md).
514
+ cmd_update() {
515
+ require_python3
516
+ exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_update "$@"
517
+ }
518
+
519
+ # `agent-config migrate` — one-shot migration off legacy composer / npm
520
+ # install paths onto the npx-only runtime. See scripts/_cli/cmd_migrate.py
521
+ # (P3.5 of road-to-portable-runtime-and-update-check.md).
522
+ cmd_migrate() {
523
+ require_python3
524
+ exec env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._cli.cmd_migrate "$@"
525
+ }
526
+
507
527
  main() {
508
528
  local cmd="${1-}"
509
529
  [[ $# -gt 0 ]] && shift || true
@@ -542,6 +562,8 @@ main() {
542
562
  council:estimate) cmd_council estimate "$@" ;;
543
563
  council:run) cmd_council run "$@" ;;
544
564
  council:render) cmd_council render "$@" ;;
565
+ update) cmd_update "$@" ;;
566
+ migrate) cmd_migrate "$@" ;;
545
567
  help|--help|-h|"") usage ;;
546
568
  --version|-V) print_version ;;
547
569
  *)
@@ -552,4 +574,54 @@ main() {
552
574
  esac
553
575
  }
554
576
 
555
- main "$@"
577
+ # Pre-flight pin resolver: when `.agent-settings.yml` carries a
578
+ # non-empty `agent_config_version` that differs from the running
579
+ # package version, re-exec via `npx @event4u/agent-config@<pin>`.
580
+ # Skipped for `--version`, `help`, `update` and `migrate` (so consumers
581
+ # can escape a bad pin or run the legacy-cleanup before a pin exists),
582
+ # and when `AGENT_CONFIG_NO_PIN_REEXEC=1`. See P3.2 of
583
+ # road-to-portable-runtime-and-update-check.md.
584
+ maybe_pin_reexec() {
585
+ local cmd="${1-}"
586
+ case "$cmd" in
587
+ help|--help|-h|--version|-V|update|migrate|"") return 0 ;;
588
+ esac
589
+ if ! command -v python3 >/dev/null 2>&1; then
590
+ return 0
591
+ fi
592
+ local installed
593
+ installed="$(print_version)"
594
+ [[ -z "$installed" || "$installed" == "unknown" ]] && return 0
595
+ env PYTHONPATH="$PACKAGE_ROOT" python3 -m scripts._lib.pin_resolver \
596
+ --cwd "$CONSUMER_ROOT" --installed "$installed" -- "$@" || true
597
+ }
598
+
599
+ # Post-subcommand banner: best-effort daily update-check notice on
600
+ # stderr. Runs after the dispatch returns (subshell wrapper below)
601
+ # so the banner appears post-output and never delays the subcommand.
602
+ # Suppressed in CI, on non-TTY stdout, by AGENT_CONFIG_NO_UPDATE_CHECK=1,
603
+ # or by `update_check.enabled: false` in settings. See P2 of
604
+ # road-to-portable-runtime-and-update-check.md.
605
+ run_update_check_banner() {
606
+ local cmd="${1-}"
607
+ case "$cmd" in
608
+ help|--help|-h|--version|-V|"") return 0 ;;
609
+ esac
610
+ if ! command -v python3 >/dev/null 2>&1; then
611
+ return 0
612
+ fi
613
+ local banner_script="$PACKAGE_ROOT/scripts/check_update_banner.py"
614
+ [[ -f "$banner_script" ]] || return 0
615
+ python3 "$banner_script" --cwd "$CONSUMER_ROOT" 2>/dev/null || true
616
+ }
617
+
618
+ # Pin re-exec runs before dispatch — if it triggers, the process is
619
+ # replaced and nothing else here matters.
620
+ maybe_pin_reexec "$@"
621
+
622
+ # Dispatch in a subshell so internal ``exec`` calls do not replace this
623
+ # process — we still get to run the post-subcommand banner.
624
+ ( main "$@" )
625
+ rc=$?
626
+ run_update_check_banner "${1-}"
627
+ exit "$rc"
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """Guard: ``CASCADE_ELIGIBLE_KINDS`` / ``USER_GLOBAL_OVERLAY_KINDS`` ↔ docs.
3
+
4
+ Phase 1 of road-to-portable-runtime-and-update-check (P1.6). The
5
+ overlay resolver in :mod:`scripts._lib.agents_overlay` ships two
6
+ constants that gate which ``agents/<kind>/`` subdirs participate in
7
+ the cascade and which of those may live at the user-global layer.
8
+ The same lists are restated in
9
+ ``docs/customization.md`` § *"agents/ overlay cascade"* so consumers
10
+ can see them without reading source.
11
+
12
+ Drift between code and docs is the failure mode this guard catches.
13
+
14
+ Exit codes: 0 = clean, 1 = drift detected, 3 = internal error.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ REPO_ROOT = Path(__file__).resolve().parent.parent
23
+
24
+ sys.path.insert(0, str(REPO_ROOT))
25
+
26
+ from scripts._lib.agents_overlay import ( # noqa: E402
27
+ CASCADE_ELIGIBLE_KINDS,
28
+ USER_GLOBAL_OVERLAY_KINDS,
29
+ )
30
+
31
+ DOCS_PATH = REPO_ROOT / "docs" / "customization.md"
32
+
33
+ # Match `agents/<kind>/` in the first column of the overlay table, plus
34
+ # the ✅/❌ markers in columns 2 and 3.
35
+ ROW_RE = re.compile(
36
+ r"^\|\s*`agents/([a-z][a-z0-9_-]*)/`\s*\|\s*(✅|❌)[^|]*\|\s*(✅|❌)[^|]*\|",
37
+ )
38
+
39
+
40
+ def _parse_doc_table(text: str) -> tuple[set[str], set[str], set[str]]:
41
+ """Return (all-kinds-listed, cascade-yes, user-global-yes) from the table."""
42
+ all_kinds: set[str] = set()
43
+ cascade_yes: set[str] = set()
44
+ user_global_yes: set[str] = set()
45
+ for line in text.splitlines():
46
+ match = ROW_RE.match(line)
47
+ if not match:
48
+ continue
49
+ kind, cascade_mark, user_mark = match.groups()
50
+ all_kinds.add(kind)
51
+ if cascade_mark == "✅":
52
+ cascade_yes.add(kind)
53
+ if user_mark == "✅":
54
+ user_global_yes.add(kind)
55
+ return all_kinds, cascade_yes, user_global_yes
56
+
57
+
58
+ def main() -> int:
59
+ if not DOCS_PATH.is_file():
60
+ print(f"❌ {DOCS_PATH} not found", file=sys.stderr)
61
+ return 3
62
+ text = DOCS_PATH.read_text(encoding="utf-8")
63
+ _, doc_cascade, doc_user_global = _parse_doc_table(text)
64
+
65
+ errors: list[str] = []
66
+
67
+ code_cascade = set(CASCADE_ELIGIBLE_KINDS)
68
+ if doc_cascade != code_cascade:
69
+ only_code = sorted(code_cascade - doc_cascade)
70
+ only_doc = sorted(doc_cascade - code_cascade)
71
+ if only_code:
72
+ errors.append(
73
+ "CASCADE_ELIGIBLE_KINDS has entries missing from "
74
+ f"docs/customization.md table: {only_code}",
75
+ )
76
+ if only_doc:
77
+ errors.append(
78
+ "docs/customization.md table marks these as cascade-eligible "
79
+ f"but the code list does not: {only_doc}",
80
+ )
81
+
82
+ code_user_global = set(USER_GLOBAL_OVERLAY_KINDS)
83
+ if doc_user_global != code_user_global:
84
+ only_code = sorted(code_user_global - doc_user_global)
85
+ only_doc = sorted(doc_user_global - code_user_global)
86
+ if only_code:
87
+ errors.append(
88
+ "USER_GLOBAL_OVERLAY_KINDS has entries missing from "
89
+ f"docs/customization.md table: {only_code}",
90
+ )
91
+ if only_doc:
92
+ errors.append(
93
+ "docs/customization.md table marks these as user-global-eligible "
94
+ f"but the code list does not: {only_doc}",
95
+ )
96
+
97
+ # Sanity: user-global subset of cascade-eligible.
98
+ if not code_user_global.issubset(code_cascade):
99
+ errors.append(
100
+ "USER_GLOBAL_OVERLAY_KINDS must be a subset of "
101
+ f"CASCADE_ELIGIBLE_KINDS; surplus: "
102
+ f"{sorted(code_user_global - code_cascade)}",
103
+ )
104
+
105
+ if errors:
106
+ print("❌ agents/ overlay cascade drift detected:", file=sys.stderr)
107
+ for err in errors:
108
+ print(f" - {err}", file=sys.stderr)
109
+ print(
110
+ "\nFix: update either scripts/_lib/agents_overlay.py "
111
+ "or docs/customization.md so they agree.",
112
+ file=sys.stderr,
113
+ )
114
+ return 1
115
+
116
+ print(
117
+ f"✅ agents/ overlay cascade in sync · "
118
+ f"{len(code_cascade)} cascade-eligible, "
119
+ f"{len(code_user_global)} user-global-eligible",
120
+ )
121
+ return 0
122
+
123
+
124
+ if __name__ == "__main__":
125
+ sys.exit(main())
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """Fail when ``package.json.version`` and the project-template pin drift.
3
+
4
+ CI guard for P3.3 of road-to-portable-runtime-and-update-check.md.
5
+
6
+ A release bump of ``package.json`` must update
7
+ ``agent_config_version`` in
8
+ ``.agent-src.uncompressed/templates/agents/agent-project-settings.example.yml``
9
+ (and its compressed twin under ``.agent-src/``) in lockstep. Otherwise
10
+ a fresh ``init`` on a new project would bootstrap onto a stale pin,
11
+ and the pin-resolver would re-exec back to the older version.
12
+
13
+ Exit codes:
14
+ 0 — pin matches package.json.version (or pin is empty and the
15
+ ``--allow-empty`` flag is set, used for early development).
16
+ 1 — pin missing, drift detected, or template file unreadable.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ REPO_ROOT = Path(__file__).resolve().parents[1]
27
+
28
+ PACKAGE_JSON = REPO_ROOT / "package.json"
29
+ TEMPLATE_FILES = (
30
+ REPO_ROOT / ".agent-src.uncompressed" / "templates" / "agents" / "agent-project-settings.example.yml",
31
+ REPO_ROOT / ".agent-src" / "templates" / "agents" / "agent-project-settings.example.yml",
32
+ )
33
+ PIN_LINE_RE = re.compile(r"^\s*agent_config_version\s*:\s*\"?([^\"\s#]*)\"?")
34
+
35
+
36
+ def _read_package_version() -> str | None:
37
+ try:
38
+ data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
39
+ except (OSError, ValueError, json.JSONDecodeError):
40
+ return None
41
+ version = data.get("version")
42
+ return version.strip() if isinstance(version, str) else None
43
+
44
+
45
+ def _read_template_pin(path: Path) -> str | None:
46
+ try:
47
+ for line in path.read_text(encoding="utf-8").splitlines():
48
+ match = PIN_LINE_RE.match(line)
49
+ if match:
50
+ return match.group(1).strip()
51
+ except OSError:
52
+ return None
53
+ return None
54
+
55
+
56
+ def main(argv: list[str] | None = None) -> int:
57
+ parser = argparse.ArgumentParser(
58
+ description="Fail when package.json version and template pin drift.",
59
+ )
60
+ parser.add_argument(
61
+ "--allow-empty",
62
+ action="store_true",
63
+ help="Accept an empty pin (used during early-stage development).",
64
+ )
65
+ args = parser.parse_args(argv)
66
+
67
+ pkg_version = _read_package_version()
68
+ if not pkg_version:
69
+ print("❌ check_template_pin_drift: failed to read package.json version.", file=sys.stderr)
70
+ return 1
71
+
72
+ failures: list[str] = []
73
+ for template in TEMPLATE_FILES:
74
+ if not template.is_file():
75
+ failures.append(f"missing template file: {template.relative_to(REPO_ROOT)}")
76
+ continue
77
+ pin = _read_template_pin(template)
78
+ rel = template.relative_to(REPO_ROOT)
79
+ if pin is None:
80
+ failures.append(f"{rel}: no `agent_config_version:` line found")
81
+ continue
82
+ if pin == "":
83
+ if not args.allow_empty:
84
+ failures.append(
85
+ f"{rel}: agent_config_version is empty; expected {pkg_version}"
86
+ )
87
+ continue
88
+ if pin != pkg_version:
89
+ failures.append(
90
+ f"{rel}: agent_config_version={pin!r} does not match package.json version {pkg_version!r}"
91
+ )
92
+
93
+ if failures:
94
+ print(
95
+ "❌ check_template_pin_drift: template pin drift detected.",
96
+ file=sys.stderr,
97
+ )
98
+ for line in failures:
99
+ print(f" - {line}", file=sys.stderr)
100
+ print(
101
+ " Fix: update `agent_config_version:` in the listed template(s) to "
102
+ f"{pkg_version!r} before releasing.",
103
+ file=sys.stderr,
104
+ )
105
+ return 1
106
+
107
+ print(f"✅ template pin = package.json version ({pkg_version}).")
108
+ return 0
109
+
110
+
111
+ if __name__ == "__main__":
112
+ sys.exit(main())
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ """Thin CLI wrapper: emit the daily update-check banner to stderr.
3
+
4
+ Invoked by the ``scripts/agent-config`` dispatcher **after** a
5
+ subcommand finishes. Never raises, never exits non-zero — banner is
6
+ best-effort. See ``scripts/_lib/update_check.py`` for the decision
7
+ logic.
8
+
9
+ Usage:
10
+ python3 scripts/check_update_banner.py [--installed-version X.Y.Z]
11
+
12
+ When ``--installed-version`` is omitted, reads ``package.json`` next to
13
+ the package root (``$PACKAGE_ROOT/package.json``).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ ROOT = Path(__file__).resolve().parent.parent
23
+ sys.path.insert(0, str(ROOT))
24
+
25
+ from scripts._lib import agent_settings # noqa: E402
26
+ from scripts._lib import update_check # noqa: E402
27
+
28
+
29
+ def _read_installed_version(package_root: Path) -> str:
30
+ candidate = package_root / "package.json"
31
+ try:
32
+ data = json.loads(candidate.read_text(encoding="utf-8"))
33
+ version = data.get("version")
34
+ if isinstance(version, str) and version.strip():
35
+ return version.strip()
36
+ except (OSError, ValueError, json.JSONDecodeError):
37
+ pass
38
+ return ""
39
+
40
+
41
+ def _read_settings_flag(cwd: Path) -> bool:
42
+ try:
43
+ settings = agent_settings.load_agent_settings(cwd=cwd)
44
+ except Exception:
45
+ return True
46
+ block = settings.get("update_check")
47
+ if isinstance(block, dict) and block.get("enabled") is False:
48
+ return False
49
+ return True
50
+
51
+
52
+ def main(argv: list[str] | None = None) -> int:
53
+ parser = argparse.ArgumentParser(prog="check-update-banner", add_help=False)
54
+ parser.add_argument("--installed-version", default="")
55
+ parser.add_argument("--cwd", default=str(Path.cwd()))
56
+ parser.add_argument("--help", "-h", action="store_true")
57
+ args, _unknown = parser.parse_known_args(argv)
58
+ if args.help:
59
+ print(__doc__ or "")
60
+ return 0
61
+
62
+ installed = args.installed_version or _read_installed_version(ROOT)
63
+ if not installed:
64
+ return 0
65
+
66
+ try:
67
+ cwd = Path(args.cwd).resolve()
68
+ except OSError:
69
+ cwd = ROOT
70
+ enabled = _read_settings_flag(cwd)
71
+
72
+ try:
73
+ banner = update_check.check_for_update(
74
+ installed,
75
+ settings_enabled=enabled,
76
+ )
77
+ except Exception:
78
+ return 0
79
+
80
+ if banner:
81
+ print(banner, file=sys.stderr)
82
+ return 0
83
+
84
+
85
+ if __name__ == "__main__":
86
+ sys.exit(main())
package/scripts/install CHANGED
@@ -26,12 +26,6 @@
26
26
  # --quiet Suppress non-error output
27
27
  # --skip-sync Skip payload sync (install.sh)
28
28
  # --skip-bridges Skip bridge files (install.py)
29
- # --global Phase-3: ship kernel rules + curated skills to user-scope
30
- # dirs (~/.claude/, ~/.cursor/, ~/.codeium/windsurf/,
31
- # ~/.config/agent-config/) so the agent has them in every
32
- # project. Pair with --tools to scope surfaces; default = all.
33
- # --uninstall With --global: remove the event4u/ namespace dir from each
34
- # enabled surface (no effect on user-added files).
35
29
  # --help, -h Show this help
36
30
  #
37
31
  # Examples:
@@ -59,14 +53,12 @@ QUIET=false
59
53
  SKIP_SYNC=false
60
54
  SKIP_BRIDGES=false
61
55
  LIST_TOOLS=false
62
- GLOBAL_INSTALL=false
63
- UNINSTALL=false
64
56
 
65
57
  # Single source of truth for valid tool IDs (also referenced by install.sh / install.py).
66
58
  VALID_TOOLS="claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex all"
67
59
 
68
60
  show_help() {
69
- sed -n '3,35p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
61
+ sed -n '3,29p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
70
62
  }
71
63
 
72
64
  list_tools() {
@@ -128,8 +120,6 @@ while [[ $# -gt 0 ]]; do
128
120
  --quiet) QUIET=true; shift ;;
129
121
  --skip-sync) SKIP_SYNC=true; shift ;;
130
122
  --skip-bridges) SKIP_BRIDGES=true; shift ;;
131
- --global) GLOBAL_INSTALL=true; shift ;;
132
- --uninstall) UNINSTALL=true; shift ;;
133
123
  --help|-h) show_help; exit 0 ;;
134
124
  *) err "Unknown argument: $1"; show_help >&2; exit 1 ;;
135
125
  esac
@@ -175,16 +165,11 @@ prompt_tools() {
175
165
  echo " ✅ Selected: $TOOLS"
176
166
  }
177
167
 
178
- if ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && ! $GLOBAL_INSTALL && [[ -t 0 && -t 1 ]]; then
168
+ if ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && [[ -t 0 && -t 1 ]]; then
179
169
  prompt_tools
180
170
  TOOLS_EXPLICIT=true
181
171
  fi
182
172
 
183
- if $UNINSTALL && ! $GLOBAL_INSTALL; then
184
- err "--uninstall is only valid combined with --global"
185
- exit 1
186
- fi
187
-
188
173
  # Default = "all": backward compatible with pre-Phase-1 invocations. An
189
174
  # explicit --tools= (empty value) is rejected by validate_tools — only an
190
175
  # absent flag falls through to "all".
@@ -201,12 +186,10 @@ if [[ -z "$SOURCE_DIR" ]]; then
201
186
  SOURCE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
202
187
  fi
203
188
 
204
- # Auto-detect target: PROJECT_ROOT env, or derive from vendor/node_modules path, else cwd
189
+ # Auto-detect target: PROJECT_ROOT env, or derive from node_modules path, else cwd
205
190
  if [[ -z "$TARGET_DIR" ]]; then
206
191
  if [[ -n "${PROJECT_ROOT:-}" ]]; then
207
192
  TARGET_DIR="$PROJECT_ROOT"
208
- elif [[ "$SOURCE_DIR" == */vendor/event4u/agent-config ]]; then
209
- TARGET_DIR="$(cd "$SOURCE_DIR/../../.." && pwd)"
210
193
  elif [[ "$SOURCE_DIR" == */node_modules/@event4u/agent-config ]]; then
211
194
  TARGET_DIR="$(cd "$SOURCE_DIR/../../.." && pwd)"
212
195
  elif [[ "$SOURCE_DIR" == */node_modules/*/agent-config ]]; then
@@ -216,6 +199,30 @@ if [[ -z "$TARGET_DIR" ]]; then
216
199
  fi
217
200
  fi
218
201
 
202
+ # Source-repo guard: refuse to install into the agent-config dev tree itself.
203
+ # Mirrors packages/create-agent-config/src/install.js — defense-in-depth so a
204
+ # direct `bash scripts/install` from inside the source checkout (without the
205
+ # Node wrapper) cannot corrupt .augment/ symlinks. Override for self-tests:
206
+ # AGENT_CONFIG_ALLOW_SELF_INSTALL=1.
207
+ if [[ "${AGENT_CONFIG_ALLOW_SELF_INSTALL:-0}" != "1" ]]; then
208
+ self_marker=""
209
+ if [[ -d "$TARGET_DIR/.agent-src.uncompressed" ]]; then
210
+ self_marker=".agent-src.uncompressed/"
211
+ elif [[ -f "$TARGET_DIR/package.json" ]] && \
212
+ grep -qE '"name"[[:space:]]*:[[:space:]]*"@event4u/(create-)?agent-config"' "$TARGET_DIR/package.json" 2>/dev/null; then
213
+ self_marker='package.json::name === "@event4u/(create-)agent-config"'
214
+ fi
215
+ if [[ -n "$self_marker" ]]; then
216
+ err "Refusing to install agent-config into its own source checkout."
217
+ echo " Target: $TARGET_DIR" >&2
218
+ echo " Detected: $self_marker" >&2
219
+ echo " Run \`task sync\` to regenerate .agent-src/ + .augment/ from" >&2
220
+ echo " .agent-src.uncompressed/ instead. To force this anyway, set" >&2
221
+ echo " AGENT_CONFIG_ALLOW_SELF_INSTALL=1." >&2
222
+ exit 2
223
+ fi
224
+ fi
225
+
219
226
  # Find python3 for the bridges stage (optional until SKIP_BRIDGES=false)
220
227
  find_python() {
221
228
  local candidate path
@@ -262,26 +269,6 @@ run_bridges() {
262
269
 
263
270
  RC=0
264
271
 
265
- # --global: dedicated user-scope path. Skips the project-bridge sync entirely
266
- # and forwards to install.py --global (Phase 3 / S13). --uninstall pairs with
267
- # --global to wipe the event4u/ namespace dir under each surface.
268
- if $GLOBAL_INSTALL; then
269
- if ! python_bin="$(find_python)"; then
270
- err "Python 3 not found — required for --global install"
271
- exit 1
272
- fi
273
- if [[ ! -f "$INSTALL_PY" ]]; then
274
- err "Missing $INSTALL_PY"
275
- exit 1
276
- fi
277
- args=(--package "$SOURCE_DIR" --global --tools="$TOOLS")
278
- $FORCE && args+=(--force)
279
- $QUIET && args+=(--quiet)
280
- $UNINSTALL && args+=(--uninstall)
281
- "$python_bin" "$INSTALL_PY" "${args[@]}"
282
- exit $?
283
- fi
284
-
285
272
  if ! $SKIP_SYNC; then
286
273
  if [[ ! -f "$INSTALL_SH" ]]; then
287
274
  err "Missing $INSTALL_SH"