@event4u/agent-config 1.38.0 → 1.40.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 (56) hide show
  1. package/.agent-src/commands/onboard.md +131 -50
  2. package/.agent-src/commands/orchestrate.md +123 -0
  3. package/.agent-src/commands/sync-gitignore/fix.md +135 -0
  4. package/.agent-src/commands/sync-gitignore.md +31 -5
  5. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +30 -2
  6. package/.agent-src/skills/subagent-orchestration/SKILL.md +9 -0
  7. package/.agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  8. package/.agent-src/templates/agent-settings.md +9 -0
  9. package/.agent-src/templates/agents/agent-project-settings.example.yml +9 -2
  10. package/.agent-src/templates/scripts/work_engine/_lib/__init__.py +7 -0
  11. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +168 -0
  12. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +18 -19
  13. package/.agent-src/templates/scripts/work_engine/orchestration.py +168 -0
  14. package/.claude-plugin/marketplace.json +3 -1
  15. package/AGENTS.md +4 -4
  16. package/CHANGELOG.md +76 -0
  17. package/README.md +17 -6
  18. package/bin/install.php +13 -6
  19. package/config/agent-settings.template.yml +21 -0
  20. package/docs/DISTRIBUTION_CHECKLIST.md +169 -0
  21. package/docs/architecture.md +1 -1
  22. package/docs/catalog.md +3 -2
  23. package/docs/contracts/audit-log-v1.md +142 -0
  24. package/docs/contracts/command-clusters.md +2 -0
  25. package/docs/contracts/file-ownership-matrix.json +20 -0
  26. package/docs/contracts/orchestration-dsl-v1.md +152 -0
  27. package/docs/customization.md +45 -0
  28. package/docs/getting-started.md +1 -1
  29. package/docs/guidelines/agent-infra/layered-settings.md +54 -17
  30. package/docs/installation.md +132 -0
  31. package/docs/setup/mcp-client-config.md +152 -0
  32. package/docs/setup/mcp-cloud-endpoints.md +16 -0
  33. package/docs/setup/per-ide/aider.md +48 -0
  34. package/docs/setup/per-ide/claude-code.md +108 -0
  35. package/docs/setup/per-ide/claude-desktop.md +148 -0
  36. package/docs/setup/per-ide/cline.md +43 -0
  37. package/docs/setup/per-ide/codex.md +46 -0
  38. package/docs/setup/per-ide/copilot.md +80 -0
  39. package/docs/setup/per-ide/cursor.md +125 -0
  40. package/docs/setup/per-ide/gemini-cli.md +45 -0
  41. package/docs/setup/per-ide/windsurf.md +120 -0
  42. package/package.json +1 -1
  43. package/scripts/_lib/agent_settings.py +168 -0
  44. package/scripts/compress.py +153 -1
  45. package/scripts/extract_audit_patterns.py +202 -0
  46. package/scripts/install +156 -1
  47. package/scripts/install.py +270 -10
  48. package/scripts/install.sh +52 -7
  49. package/scripts/lint_orchestration_dsl.py +214 -0
  50. package/scripts/skill_linter.py +9 -0
  51. package/scripts/sync_gitignore.py +56 -1
  52. package/templates/claude_desktop_config.json.template +21 -0
  53. package/templates/cursor-rule.mdc.j2 +7 -0
  54. package/templates/global-install-manifest.yml +91 -0
  55. package/templates/marketing-copy.yml +64 -0
  56. package/templates/windsurf-rule.md.j2 +7 -0
package/scripts/install CHANGED
@@ -15,13 +15,30 @@
15
15
  # --source <dir> Package source directory (default: auto-detect)
16
16
  # --target <dir> Target project root (default: cwd)
17
17
  # --profile <name> Cost profile for bridges (minimal|balanced|full)
18
+ # --tools <list> Comma-separated tool IDs to install (default: all).
19
+ # Valid: claude-code,claude-desktop,cursor,windsurf,
20
+ # cline,gemini-cli,copilot,augment,aider,codex,all
21
+ # --list-tools Print supported tool IDs with descriptions, then exit
22
+ # --yes, -y Non-interactive mode: do not prompt (default for non-TTY)
18
23
  # --force Overwrite existing bridge files
19
24
  # --dry-run Show what payload sync would do (does not run bridges)
20
25
  # --verbose Detailed payload sync output
21
26
  # --quiet Suppress non-error output
22
27
  # --skip-sync Skip payload sync (install.sh)
23
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).
24
35
  # --help, -h Show this help
36
+ #
37
+ # Examples:
38
+ # bash scripts/install # everything (default)
39
+ # bash scripts/install --tools=claude-code,cursor # only those two
40
+ # bash scripts/install --tools=cursor --yes # CI-friendly
41
+ # bash scripts/install --list-tools # show catalog
25
42
 
26
43
  set -uo pipefail
27
44
 
@@ -32,19 +49,67 @@ INSTALL_PY="$SCRIPT_DIR/install.py"
32
49
  SOURCE_DIR=""
33
50
  TARGET_DIR=""
34
51
  PROFILE=""
52
+ TOOLS=""
53
+ TOOLS_EXPLICIT=false
54
+ YES=false
35
55
  FORCE=false
36
56
  DRY_RUN=false
37
57
  VERBOSE=false
38
58
  QUIET=false
39
59
  SKIP_SYNC=false
40
60
  SKIP_BRIDGES=false
61
+ LIST_TOOLS=false
62
+ GLOBAL_INSTALL=false
63
+ UNINSTALL=false
64
+
65
+ # Single source of truth for valid tool IDs (also referenced by install.sh / install.py).
66
+ VALID_TOOLS="claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex all"
41
67
 
42
68
  show_help() {
43
- sed -n '3,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
69
+ sed -n '3,35p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
70
+ }
71
+
72
+ list_tools() {
73
+ cat <<'EOF'
74
+ Supported --tools IDs (default: all):
75
+
76
+ claude-code .claude/rules, .claude/skills, .claude/commands, .claude/settings.json
77
+ claude-desktop ~/Library/Application Support/Claude/ (global, see Phase 4 docs)
78
+ cursor .cursor/rules, .cursor/commands (legacy .cursorrules also written)
79
+ windsurf .windsurf/rules, .windsurf/workflows (legacy .windsurfrules also written)
80
+ cline .clinerules/ symlinks
81
+ gemini-cli GEMINI.md, .gemini/settings.json
82
+ copilot .github/copilot-instructions.md, .vscode/settings.json
83
+ augment .augment/ payload + settings (substrate — recommended for every install)
84
+ aider AGENTS.md (Linux Foundation cross-tool contract; always written)
85
+ codex AGENTS.md (same as aider — no extra action)
86
+ all every ID above (the default; backward-compatible)
87
+
88
+ Examples:
89
+ --tools=claude-code,cursor project-local install for those two surfaces
90
+ --tools=all equivalent to omitting the flag
91
+ EOF
44
92
  }
45
93
 
46
94
  err() { echo " ❌ $*" >&2; }
47
95
 
96
+ # Validate a comma-separated tool list against $VALID_TOOLS. Empty input is
97
+ # rejected so a stray --tools= does not silently behave like --tools=all.
98
+ validate_tools() {
99
+ local raw="$1"
100
+ [[ -z "$raw" ]] && { err "--tools requires a non-empty value (use --list-tools to see options)"; return 1; }
101
+ local IFS=','
102
+ local item
103
+ for item in $raw; do
104
+ [[ -z "$item" ]] && { err "--tools contains an empty entry"; return 1; }
105
+ if [[ " $VALID_TOOLS " != *" $item "* ]]; then
106
+ err "Unknown tool ID: $item (run --list-tools for the catalog)"
107
+ return 1
108
+ fi
109
+ done
110
+ return 0
111
+ }
112
+
48
113
  while [[ $# -gt 0 ]]; do
49
114
  case "$1" in
50
115
  --source) SOURCE_DIR="$2"; shift 2 ;;
@@ -53,17 +118,84 @@ while [[ $# -gt 0 ]]; do
53
118
  --target=*) TARGET_DIR="${1#*=}"; shift ;;
54
119
  --profile) PROFILE="$2"; shift 2 ;;
55
120
  --profile=*) PROFILE="${1#*=}"; shift ;;
121
+ --tools) TOOLS="$2"; TOOLS_EXPLICIT=true; shift 2 ;;
122
+ --tools=*) TOOLS="${1#*=}"; TOOLS_EXPLICIT=true; shift ;;
123
+ --list-tools) LIST_TOOLS=true; shift ;;
124
+ --yes|-y) YES=true; shift ;;
56
125
  --force) FORCE=true; shift ;;
57
126
  --dry-run) DRY_RUN=true; shift ;;
58
127
  --verbose) VERBOSE=true; shift ;;
59
128
  --quiet) QUIET=true; shift ;;
60
129
  --skip-sync) SKIP_SYNC=true; shift ;;
61
130
  --skip-bridges) SKIP_BRIDGES=true; shift ;;
131
+ --global) GLOBAL_INSTALL=true; shift ;;
132
+ --uninstall) UNINSTALL=true; shift ;;
62
133
  --help|-h) show_help; exit 0 ;;
63
134
  *) err "Unknown argument: $1"; show_help >&2; exit 1 ;;
64
135
  esac
65
136
  done
66
137
 
138
+ if $LIST_TOOLS; then
139
+ list_tools
140
+ exit 0
141
+ fi
142
+
143
+ # Interactive --tools picker (S9). Fires only when:
144
+ # - --tools was not explicitly passed
145
+ # - --yes / -y was not passed (CI / non-interactive opt-out)
146
+ # - stdin AND stdout are both TTYs (so we're not in a pipe / curl|bash flow)
147
+ # - --dry-run, --quiet, --skip-sync, --skip-bridges did not opt out of UX
148
+ # Otherwise we fall through to the backward-compatible "all" default.
149
+ prompt_tools() {
150
+ local choice picked tool i=0
151
+ local -a menu=(claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex)
152
+ echo ""
153
+ echo " 📦 Pick the tools to install (comma-separated numbers, blank = all):"
154
+ for tool in "${menu[@]}"; do
155
+ i=$((i+1))
156
+ printf " %2d) %s\n" "$i" "$tool"
157
+ done
158
+ echo " a) all (default)"
159
+ echo ""
160
+ printf " Selection: "
161
+ IFS= read -r choice || choice=""
162
+ choice="${choice// /}"
163
+ if [[ -z "$choice" || "$choice" == "a" || "$choice" == "all" ]]; then
164
+ TOOLS="all"; return
165
+ fi
166
+ picked=""
167
+ local IFS=','
168
+ for n in $choice; do
169
+ if ! [[ "$n" =~ ^[0-9]+$ ]] || (( n < 1 || n > ${#menu[@]} )); then
170
+ err "Invalid selection: $n (expected 1-${#menu[@]} or 'a')"; exit 1
171
+ fi
172
+ picked+="${menu[$((n-1))]},"
173
+ done
174
+ TOOLS="${picked%,}"
175
+ echo " ✅ Selected: $TOOLS"
176
+ }
177
+
178
+ if ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && ! $GLOBAL_INSTALL && [[ -t 0 && -t 1 ]]; then
179
+ prompt_tools
180
+ TOOLS_EXPLICIT=true
181
+ fi
182
+
183
+ if $UNINSTALL && ! $GLOBAL_INSTALL; then
184
+ err "--uninstall is only valid combined with --global"
185
+ exit 1
186
+ fi
187
+
188
+ # Default = "all": backward compatible with pre-Phase-1 invocations. An
189
+ # explicit --tools= (empty value) is rejected by validate_tools — only an
190
+ # absent flag falls through to "all".
191
+ if ! $TOOLS_EXPLICIT && [[ -z "$TOOLS" ]]; then
192
+ TOOLS="all"
193
+ fi
194
+
195
+ if ! validate_tools "$TOOLS"; then
196
+ exit 1
197
+ fi
198
+
67
199
  # Auto-detect source: directory above this script
68
200
  if [[ -z "$SOURCE_DIR" ]]; then
69
201
  SOURCE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -103,6 +235,7 @@ run_sync() {
103
235
  $DRY_RUN && args+=(--dry-run)
104
236
  $VERBOSE && args+=(--verbose)
105
237
  $QUIET && args+=(--quiet)
238
+ args+=(--tools="$TOOLS")
106
239
  bash "$INSTALL_SH" "${args[@]}"
107
240
  }
108
241
 
@@ -123,10 +256,32 @@ run_bridges() {
123
256
  [[ -n "$PROFILE" ]] && args+=(--profile="$PROFILE")
124
257
  $FORCE && args+=(--force)
125
258
  $QUIET && args+=(--quiet)
259
+ args+=(--tools="$TOOLS")
126
260
  "$python_bin" "$INSTALL_PY" "${args[@]}"
127
261
  }
128
262
 
129
263
  RC=0
264
+
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
+
130
285
  if ! $SKIP_SYNC; then
131
286
  if [[ ! -f "$INSTALL_SH" ]]; then
132
287
  err "Missing $INSTALL_SH"
@@ -1217,6 +1217,189 @@ def _smoke_test_hooks(project_root: Path, package_root: Path) -> int:
1217
1217
  return 1 if failed else 0
1218
1218
 
1219
1219
 
1220
+ # --- Global user-level install (Phase 3 — road-to-simplicity-and-everywhere) ---
1221
+ #
1222
+ # `scripts/install --global` ships kernel rules + a curated top-N of skills
1223
+ # into per-tool user-scope directories so the agent has them in every
1224
+ # project on the machine. Curation lives in templates/global-install-manifest.yml.
1225
+ #
1226
+ # Files are written under an `event4u/` namespace so `--global --uninstall`
1227
+ # can wipe the namespace dir without touching user-added files.
1228
+
1229
+ GLOBAL_NAMESPACE = "event4u"
1230
+ GLOBAL_MANIFEST_REL = Path("templates") / "global-install-manifest.yml"
1231
+
1232
+ # Per-tool global directories. Each surface gets a `rules/` and `skills/`
1233
+ # bucket under the event4u/ namespace. claude-desktop reuses claude-code's
1234
+ # ~/.claude/ (the two surfaces share the dir per Anthropic's docs).
1235
+ GLOBAL_TOOL_DIRS: dict[str, dict[str, Path]] = {
1236
+ "claude-code": {
1237
+ "rules": Path.home() / ".claude" / "rules" / GLOBAL_NAMESPACE,
1238
+ "skills": Path.home() / ".claude" / "skills" / GLOBAL_NAMESPACE,
1239
+ },
1240
+ "cursor": {
1241
+ "rules": Path.home() / ".cursor" / "rules" / "imported" / GLOBAL_NAMESPACE / "rules",
1242
+ "skills": Path.home() / ".cursor" / "rules" / "imported" / GLOBAL_NAMESPACE / "skills",
1243
+ },
1244
+ "windsurf": {
1245
+ "rules": Path.home() / ".codeium" / "windsurf" / "global_workflows" / GLOBAL_NAMESPACE / "rules",
1246
+ "skills": Path.home() / ".codeium" / "windsurf" / "global_workflows" / GLOBAL_NAMESPACE / "skills",
1247
+ },
1248
+ "fallback": {
1249
+ "rules": Path.home() / ".config" / "agent-config" / "rules" / GLOBAL_NAMESPACE,
1250
+ "skills": Path.home() / ".config" / "agent-config" / "skills" / GLOBAL_NAMESPACE,
1251
+ },
1252
+ }
1253
+
1254
+
1255
+ def _load_global_manifest(package_root: Path) -> dict:
1256
+ """Parse templates/global-install-manifest.yml without a YAML dep.
1257
+
1258
+ Tiny hand-rolled parser — only handles the manifest's flat shape:
1259
+ `key: value`, `- item`, and `- key: value` indented under a parent
1260
+ list. We avoid pulling in PyYAML so install.py stays zero-dep.
1261
+ """
1262
+ path = package_root / GLOBAL_MANIFEST_REL
1263
+ if not path.is_file():
1264
+ fail(f"global manifest missing: {path}")
1265
+ out: dict = {"kernel_rules": [], "top_skills": []}
1266
+ section: str | None = None
1267
+ pending: dict | None = None
1268
+ for raw in path.read_text(encoding="utf-8").splitlines():
1269
+ line = raw.split("#", 1)[0].rstrip()
1270
+ if not line.strip():
1271
+ continue
1272
+ if line.startswith("kernel_rules:"):
1273
+ section = "kernel_rules"; pending = None; continue
1274
+ if line.startswith("top_skills:"):
1275
+ section = "top_skills"; pending = None; continue
1276
+ stripped = line.lstrip()
1277
+ indent = len(line) - len(stripped)
1278
+ if section == "kernel_rules" and stripped.startswith("- "):
1279
+ out["kernel_rules"].append(stripped[2:].strip())
1280
+ elif section == "top_skills":
1281
+ if stripped.startswith("- id:"):
1282
+ if pending is not None:
1283
+ out["top_skills"].append(pending)
1284
+ pending = {"id": stripped.split(":", 1)[1].strip(), "surfaces": []}
1285
+ elif pending is not None and stripped.startswith("surfaces:"):
1286
+ raw_list = stripped.split(":", 1)[1].strip()
1287
+ if raw_list.startswith("[") and raw_list.endswith("]"):
1288
+ pending["surfaces"] = [s.strip() for s in raw_list[1:-1].split(",") if s.strip()]
1289
+ elif indent == 0 and not stripped.startswith("-"):
1290
+ section = None
1291
+ if pending is not None:
1292
+ out["top_skills"].append(pending)
1293
+ return out
1294
+
1295
+
1296
+ def _resolve_skill_source(package_root: Path, skill_id: str) -> Path | None:
1297
+ """Locate a skill's SKILL.md in the package. Prefers .claude/skills/."""
1298
+ for base in (package_root / ".claude" / "skills",
1299
+ package_root / ".agent-src" / "skills"):
1300
+ candidate = base / skill_id / "SKILL.md"
1301
+ if candidate.is_file():
1302
+ return candidate.parent
1303
+ return None
1304
+
1305
+
1306
+ def _resolve_rule_source(package_root: Path, rule_id: str) -> Path | None:
1307
+ """Locate a rule's .md in the package. Prefers .agent-src/rules/."""
1308
+ for base in (package_root / ".agent-src" / "rules",
1309
+ package_root / ".augment" / "rules"):
1310
+ candidate = base / f"{rule_id}.md"
1311
+ if candidate.is_file():
1312
+ return candidate
1313
+ return None
1314
+
1315
+
1316
+ def _global_targets(tools: set[str]) -> dict[str, dict[str, Path]]:
1317
+ """Return the subset of GLOBAL_TOOL_DIRS to write to. Fallback always on."""
1318
+ targets: dict[str, dict[str, Path]] = {"fallback": GLOBAL_TOOL_DIRS["fallback"]}
1319
+ for tool_id, dirs in GLOBAL_TOOL_DIRS.items():
1320
+ if tool_id == "fallback":
1321
+ continue
1322
+ # claude-desktop shares claude-code's dir — both flip the same target.
1323
+ if tool_id == "claude-code" and ("claude-code" in tools or "claude-desktop" in tools):
1324
+ targets[tool_id] = dirs
1325
+ elif tool_id in tools:
1326
+ targets[tool_id] = dirs
1327
+ return targets
1328
+
1329
+
1330
+ def install_global(package_root: Path, tools: set[str], force: bool) -> int:
1331
+ """S13/S14: ship kernel rules + curated skills to user-scope dirs."""
1332
+ import shutil
1333
+ manifest = _load_global_manifest(package_root)
1334
+ targets = _global_targets(tools)
1335
+
1336
+ if not QUIET:
1337
+ info(f"Global install — surfaces: {', '.join(sorted(targets))}")
1338
+ info(f" rules: {len(manifest['kernel_rules'])}")
1339
+ info(f" skills: {len(manifest['top_skills'])}")
1340
+
1341
+ for surface, dirs in targets.items():
1342
+ for kind in ("rules", "skills"):
1343
+ dirs[kind].mkdir(parents=True, exist_ok=True)
1344
+ # Rules: copy each kernel rule (file).
1345
+ for rule_id in manifest["kernel_rules"]:
1346
+ src = _resolve_rule_source(package_root, rule_id)
1347
+ if src is None:
1348
+ warn(f"global: rule '{rule_id}' missing in package — skipped")
1349
+ continue
1350
+ dst = dirs["rules"] / f"{rule_id}.md"
1351
+ if dst.exists() and not force and dst.read_bytes() == src.read_bytes():
1352
+ continue
1353
+ dst.write_bytes(src.read_bytes())
1354
+ # Skills: copy each curated skill (directory).
1355
+ for entry in manifest["top_skills"]:
1356
+ if surface != "fallback" and surface not in entry.get("surfaces", []):
1357
+ continue
1358
+ src_dir = _resolve_skill_source(package_root, entry["id"])
1359
+ if src_dir is None:
1360
+ warn(f"global: skill '{entry['id']}' missing in package — skipped")
1361
+ continue
1362
+ dst_dir = dirs["skills"] / entry["id"]
1363
+ if dst_dir.exists() and not force:
1364
+ shutil.rmtree(dst_dir)
1365
+ shutil.copytree(src_dir, dst_dir)
1366
+ if not QUIET:
1367
+ success(f" {surface}: {dirs['rules']}, {dirs['skills']}")
1368
+ return 0
1369
+
1370
+
1371
+ def uninstall_global(tools: set[str]) -> int:
1372
+ """S15: remove the event4u/ namespace dir from each enabled surface."""
1373
+ import shutil
1374
+ targets = _global_targets(tools)
1375
+ removed: list[str] = []
1376
+ # Collect ancestor dirs named GLOBAL_NAMESPACE so we can drop the
1377
+ # shared parent (e.g. cursor/windsurf put rules + skills as siblings
1378
+ # under one event4u/ dir; removing both leaves a stranded namespace
1379
+ # dir). We never delete anything not literally named event4u/.
1380
+ namespace_parents: set[Path] = set()
1381
+ for surface, dirs in targets.items():
1382
+ for kind in ("rules", "skills"):
1383
+ d = dirs[kind]
1384
+ if d.exists():
1385
+ shutil.rmtree(d)
1386
+ removed.append(str(d))
1387
+ for ancestor in d.parents:
1388
+ if ancestor.name == GLOBAL_NAMESPACE:
1389
+ namespace_parents.add(ancestor)
1390
+ for parent in namespace_parents:
1391
+ if parent.is_dir() and not any(parent.iterdir()):
1392
+ parent.rmdir()
1393
+ removed.append(str(parent))
1394
+ if not QUIET:
1395
+ if removed:
1396
+ for r in removed:
1397
+ success(f"removed {r}")
1398
+ else:
1399
+ skip("global uninstall: nothing to remove")
1400
+ return 0
1401
+
1402
+
1220
1403
  # --- Argument parsing ---
1221
1404
 
1222
1405
  def parse_options(argv: list[str]) -> argparse.Namespace:
@@ -1260,14 +1443,70 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
1260
1443
  parser.add_argument("--project", default=None, help="project root (default: cwd or PROJECT_ROOT env)")
1261
1444
  parser.add_argument("--package", default=None, help="package root (default: auto-detect under project)")
1262
1445
  parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
1446
+ parser.add_argument(
1447
+ "--tools",
1448
+ default="all",
1449
+ help=(
1450
+ "comma-separated tool IDs to install bridges for "
1451
+ "(claude-code,cursor,windsurf,cline,gemini-cli,copilot,augment,aider,codex,all). "
1452
+ "Default: all (backward-compatible)."
1453
+ ),
1454
+ )
1263
1455
  parser.add_argument(
1264
1456
  "--no-smoke",
1265
1457
  action="store_true",
1266
1458
  help="skip the post-install hook smoke test (default: dry-fire dispatch:hook against every installed bridge)",
1267
1459
  )
1460
+ parser.add_argument(
1461
+ "--global",
1462
+ dest="global_install",
1463
+ action="store_true",
1464
+ help=(
1465
+ "Phase-3 mode: ship kernel rules + curated top-N skills to user-scope "
1466
+ "dirs (~/.claude/, ~/.cursor/, ~/.codeium/windsurf/, ~/.config/agent-config/) "
1467
+ "so the agent has them in every project. Curation: templates/global-install-manifest.yml."
1468
+ ),
1469
+ )
1470
+ parser.add_argument(
1471
+ "--uninstall",
1472
+ action="store_true",
1473
+ help="With --global: remove the event4u/ namespace dir from each enabled surface.",
1474
+ )
1268
1475
  return parser.parse_args(argv)
1269
1476
 
1270
1477
 
1478
+ # Mapping of --tools IDs accepted by install.py. Mirrors VALID_TOOLS in
1479
+ # scripts/install. Bridges keyed off these IDs are gated; substrate
1480
+ # bridges (vscode, augment) always run.
1481
+ _VALID_TOOLS = {
1482
+ "claude-code", "claude-desktop", "cursor", "windsurf", "cline",
1483
+ "gemini-cli", "copilot", "augment", "aider", "codex", "all",
1484
+ }
1485
+
1486
+
1487
+ def _parse_tools(raw: str) -> set[str]:
1488
+ """Parse and validate --tools value. Returns set of normalized IDs.
1489
+
1490
+ "all" expands to every concrete ID. Empty input is rejected.
1491
+ Unknown IDs raise SystemExit so the caller's message reaches stderr.
1492
+ """
1493
+ if not raw or not raw.strip():
1494
+ fail("--tools requires a non-empty value")
1495
+ items = [s.strip() for s in raw.split(",") if s.strip()]
1496
+ if not items:
1497
+ fail("--tools requires at least one ID")
1498
+ unknown = [s for s in items if s not in _VALID_TOOLS]
1499
+ if unknown:
1500
+ fail(f"--tools: unknown ID(s): {', '.join(unknown)} (valid: {', '.join(sorted(_VALID_TOOLS))})")
1501
+ if "all" in items:
1502
+ return {t for t in _VALID_TOOLS if t != "all"}
1503
+ return set(items)
1504
+
1505
+
1506
+ def _is_tool_enabled(tools: set[str], tool_id: str) -> bool:
1507
+ return tool_id in tools
1508
+
1509
+
1271
1510
  # --- Main ---
1272
1511
 
1273
1512
  def main(argv: list[str]) -> int:
@@ -1300,31 +1539,52 @@ def main(argv: list[str]) -> int:
1300
1539
  info(f"Profile: {opts.profile}")
1301
1540
  print()
1302
1541
 
1542
+ # --global: short-circuits the project-bridge path. Ships kernel rules
1543
+ # + curated skills to user-scope dirs and exits. --uninstall pairs
1544
+ # with --global to remove the namespace dir.
1545
+ if opts.global_install:
1546
+ tools = _parse_tools(opts.tools)
1547
+ if opts.uninstall:
1548
+ return uninstall_global(tools)
1549
+ return install_global(package_root, tools, opts.force)
1550
+ if opts.uninstall:
1551
+ fail("--uninstall is only valid combined with --global")
1552
+
1303
1553
  ensure_agent_settings(project_root, package_root, opts.profile, opts.force)
1304
1554
 
1555
+ tools = _parse_tools(opts.tools)
1556
+
1305
1557
  if not opts.skip_bridges:
1558
+ # Substrate bridges (always written; other tools symlink/depend on them).
1306
1559
  ensure_vscode_bridge(project_root, package_type, opts.force)
1307
1560
  ensure_augment_bridge(project_root, opts.force)
1308
- ensure_claude_bridge(project_root, opts.force)
1309
- ensure_cursor_bridge(project_root, opts.force)
1310
- ensure_cline_bridge(project_root, opts.force)
1311
- ensure_windsurf_bridge(project_root, opts.force)
1312
- ensure_gemini_bridge(project_root, opts.force)
1313
- ensure_copilot_bridge(project_root, opts.force)
1561
+ # Tool-specific bridges (gated by --tools selection).
1562
+ if _is_tool_enabled(tools, "claude-code"):
1563
+ ensure_claude_bridge(project_root, opts.force)
1564
+ if _is_tool_enabled(tools, "cursor"):
1565
+ ensure_cursor_bridge(project_root, opts.force)
1566
+ if _is_tool_enabled(tools, "cline"):
1567
+ ensure_cline_bridge(project_root, opts.force)
1568
+ if _is_tool_enabled(tools, "windsurf"):
1569
+ ensure_windsurf_bridge(project_root, opts.force)
1570
+ if _is_tool_enabled(tools, "gemini-cli"):
1571
+ ensure_gemini_bridge(project_root, opts.force)
1572
+ if _is_tool_enabled(tools, "copilot"):
1573
+ ensure_copilot_bridge(project_root, opts.force)
1314
1574
 
1315
1575
  if opts.augment_user_hooks:
1316
1576
  ensure_augment_user_hooks(package_root, opts.force)
1317
1577
 
1318
- if opts.cursor_user_hooks:
1578
+ if opts.cursor_user_hooks and _is_tool_enabled(tools, "cursor"):
1319
1579
  ensure_cursor_user_hooks(package_root, opts.force)
1320
1580
 
1321
- if opts.cline_user_hooks:
1581
+ if opts.cline_user_hooks and _is_tool_enabled(tools, "cline"):
1322
1582
  ensure_cline_user_hooks(package_root, opts.force)
1323
1583
 
1324
- if opts.windsurf_user_hooks:
1584
+ if opts.windsurf_user_hooks and _is_tool_enabled(tools, "windsurf"):
1325
1585
  ensure_windsurf_user_hooks(package_root, opts.force)
1326
1586
 
1327
- if opts.gemini_user_hooks:
1587
+ if opts.gemini_user_hooks and _is_tool_enabled(tools, "gemini-cli"):
1328
1588
  ensure_gemini_user_hooks(package_root, opts.force)
1329
1589
 
1330
1590
  if not opts.skip_bridges and not opts.no_smoke: