@event4u/agent-config 2.1.0 → 2.2.1

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 (38) hide show
  1. package/.agent-src/rules/no-cheap-questions.md +11 -2
  2. package/.agent-src/skills/readme-writing-package/SKILL.md +24 -0
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +69 -0
  5. package/README.md +71 -6
  6. package/docs/DISTRIBUTION_CHECKLIST.md +7 -6
  7. package/docs/architecture.md +1 -1
  8. package/docs/contracts/tier-3-contrib-plugin.md +129 -0
  9. package/docs/decisions/ADR-007-agent-discovery-scopes.md +286 -0
  10. package/docs/decisions/ADR-008-installed-tools-manifest.md +160 -0
  11. package/docs/decisions/INDEX.md +2 -0
  12. package/docs/getting-started.md +1 -1
  13. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +32 -0
  14. package/docs/guidelines/agent-infra/installed-tools-manifest.md +135 -0
  15. package/docs/installation.md +83 -27
  16. package/docs/setup/per-ide/aider.md +1 -1
  17. package/docs/setup/per-ide/claude-code.md +1 -1
  18. package/docs/setup/per-ide/claude-desktop.md +8 -4
  19. package/docs/setup/per-ide/cline.md +2 -2
  20. package/docs/setup/per-ide/codex.md +1 -1
  21. package/docs/setup/per-ide/copilot.md +2 -2
  22. package/docs/setup/per-ide/cursor.md +2 -2
  23. package/docs/setup/per-ide/gemini-cli.md +1 -1
  24. package/docs/setup/per-ide/windsurf.md +2 -2
  25. package/docs/troubleshooting.md +4 -4
  26. package/package.json +1 -1
  27. package/scripts/_cli/cmd_export.py +157 -0
  28. package/scripts/_cli/cmd_sync.py +162 -0
  29. package/scripts/_cli/cmd_update.py +23 -1
  30. package/scripts/_cli/cmd_validate.py +164 -0
  31. package/scripts/_lib/installed_lock.py +160 -0
  32. package/scripts/_lib/installed_tools.py +237 -0
  33. package/scripts/agent-config +78 -1
  34. package/scripts/install +43 -10
  35. package/scripts/install.py +975 -14
  36. package/templates/agent-config-wrapper.sh +1 -1
  37. package/templates/consumer-settings/README.md +1 -1
  38. package/templates/marketing-copy.yml +6 -5
@@ -107,7 +107,7 @@ def detect_package_root(project_root: Path) -> Path:
107
107
 
108
108
  fail(
109
109
  "Could not find agent-config package. Install via "
110
- "`npx @event4u/create-agent-config init` or `npm install --save-dev @event4u/agent-config`."
110
+ "`npx @event4u/agent-config init` or `npm install -g @event4u/agent-config`."
111
111
  )
112
112
  return project_root # unreachable
113
113
 
@@ -1106,6 +1106,326 @@ def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
1106
1106
  success(".github/plugin/marketplace.json created")
1107
1107
 
1108
1108
 
1109
+ # Roo Code (https://docs.roocode.com/) is a Cline-derived VS Code extension
1110
+ # that auto-discovers `.roo/rules/*.md` as system-level instructions per
1111
+ # project. No hook protocol is exposed yet (2026-05), so the bridge is a
1112
+ # minimal marker file pointing the user at the canonical rule source. Phase
1113
+ # 2.0 validation gate — keep imperative and minimal; revisit when Roo Code
1114
+ # ships a programmatic hook surface.
1115
+ ROOCODE_MARKER = """# Agent Config bridge
1116
+
1117
+ This file marks the project as an `event4u/agent-config` consumer.
1118
+
1119
+ Roo Code reads `.roo/rules/*.md` as system-level instructions. The
1120
+ canonical rule and skill source lives under `.augment/` (Augment
1121
+ portability mirror — see `AGENTS.md` for orientation).
1122
+
1123
+ Run `./agent-config --help` for available commands.
1124
+ """
1125
+
1126
+
1127
+ def ensure_roocode_bridge(project_root: Path, force: bool) -> None:
1128
+ """Deploy `.roo/rules/agent-config.md` (project scope) marker.
1129
+
1130
+ Roo Code auto-discovers `.roo/rules/*.md` as system instructions — no
1131
+ hook protocol exposed yet. Bridge is intentionally minimal: a single
1132
+ marker file pointing developers at the canonical rule source. Phase
1133
+ 2.0 validation gate (road-to-global-first-install § 2.0).
1134
+ """
1135
+ target = project_root / ".roo" / "rules" / "agent-config.md"
1136
+
1137
+ if target.exists() and not force:
1138
+ skip(".roo/rules/agent-config.md already exists")
1139
+ return
1140
+
1141
+ write_file(target, ROOCODE_MARKER)
1142
+ success(".roo/rules/agent-config.md created")
1143
+
1144
+
1145
+ # Claude Desktop (https://claude.ai/download) reads config from
1146
+ # `~/Library/Application Support/Claude/` on macOS — no project-local
1147
+ # discovery. The project bridge is informational only: a marker file that
1148
+ # documents the link and tells humans where the canonical rules live.
1149
+ # Phase 2.3 will formalize this as scope=global-only via SCOPE_SUPPORT.
1150
+ CLAUDE_DESKTOP_MARKER = """# Agent Config bridge — Claude Desktop
1151
+
1152
+ This file marks the project as an `event4u/agent-config` consumer.
1153
+
1154
+ Claude Desktop is a **global-scope** tool — it reads config from
1155
+ `~/Library/Application Support/Claude/` (macOS) and does not
1156
+ auto-discover project files. This marker is informational only.
1157
+
1158
+ To wire Claude Desktop to this project's rules, run:
1159
+ `npx @event4u/agent-config init --ai claude-desktop --global`
1160
+
1161
+ Canonical rule and skill source: `.augment/` (see `AGENTS.md`).
1162
+ """
1163
+
1164
+
1165
+ def ensure_claude_desktop_bridge(project_root: Path, force: bool) -> None:
1166
+ """Deploy `.claude-desktop/agent-config.md` informational marker.
1167
+
1168
+ Claude Desktop has no project-local discovery (global config only,
1169
+ macOS path `~/Library/Application Support/Claude/`). The marker is
1170
+ informational — Phase 2.3 will gate this bridge behind scope=global.
1171
+ """
1172
+ target = project_root / ".claude-desktop" / "agent-config.md"
1173
+
1174
+ if target.exists() and not force:
1175
+ skip(".claude-desktop/agent-config.md already exists")
1176
+ return
1177
+
1178
+ write_file(target, CLAUDE_DESKTOP_MARKER)
1179
+ success(".claude-desktop/agent-config.md created")
1180
+
1181
+
1182
+ # Aider (https://aider.chat) reads `.aider.conf.yml` per project and a
1183
+ # `CONVENTIONS.md` (or any path declared in `read:`) for system-level
1184
+ # instructions. The bridge drops a marker and documents the `read:`
1185
+ # wiring; we do not mutate `.aider.conf.yml` to avoid clobbering user
1186
+ # overrides. Phase 2.5 may upgrade this to a declarative emitter.
1187
+ AIDER_MARKER = """# Agent Config bridge — Aider
1188
+
1189
+ This file marks the project as an `event4u/agent-config` consumer.
1190
+
1191
+ Aider does not auto-discover this file. To activate it, add the
1192
+ following to `.aider.conf.yml` (create if missing):
1193
+
1194
+ ```yaml
1195
+ read:
1196
+ - .aider/agent-config.md
1197
+ ```
1198
+
1199
+ Or pass `--read .aider/agent-config.md` on the command line.
1200
+
1201
+ Canonical rule and skill source: `.augment/` (see `AGENTS.md`).
1202
+ """
1203
+
1204
+
1205
+ def ensure_aider_bridge(project_root: Path, force: bool) -> None:
1206
+ """Deploy `.aider/agent-config.md` marker; do not touch `.aider.conf.yml`.
1207
+
1208
+ Aider reads `read:` entries from `.aider.conf.yml`. We intentionally
1209
+ avoid mutating that file — the marker documents the manual wiring so
1210
+ user overrides stay intact. Phase 2.5 may declarative-emit if needed.
1211
+ """
1212
+ target = project_root / ".aider" / "agent-config.md"
1213
+
1214
+ if target.exists() and not force:
1215
+ skip(".aider/agent-config.md already exists")
1216
+ return
1217
+
1218
+ write_file(target, AIDER_MARKER)
1219
+ success(".aider/agent-config.md created")
1220
+
1221
+
1222
+ # OpenAI Codex CLI (https://github.com/openai/codex) auto-discovers
1223
+ # `AGENTS.md` at the project root as system instructions. The repo
1224
+ # already ships an `AGENTS.md` (Thin-Root contract), so the bridge is a
1225
+ # secondary marker confirming agent-config ownership — Codex will read
1226
+ # `AGENTS.md` directly regardless. Phase 2.5 may collapse if redundant.
1227
+ CODEX_MARKER = """# Agent Config bridge — Codex CLI
1228
+
1229
+ This file marks the project as an `event4u/agent-config` consumer.
1230
+
1231
+ Codex CLI auto-discovers `AGENTS.md` at the project root — that file
1232
+ is the canonical entry point. This marker is informational and tells
1233
+ developers where the rules and skills live.
1234
+
1235
+ Canonical rule and skill source: `.augment/` (see project `AGENTS.md`).
1236
+ """
1237
+
1238
+
1239
+ def ensure_codex_bridge(project_root: Path, force: bool) -> None:
1240
+ """Deploy `.codex/agent-config.md` informational marker.
1241
+
1242
+ Codex CLI reads `AGENTS.md` at project root directly — the marker
1243
+ is informational. Phase 2.5 may collapse if redundant with AGENTS.md.
1244
+ """
1245
+ target = project_root / ".codex" / "agent-config.md"
1246
+
1247
+ if target.exists() and not force:
1248
+ skip(".codex/agent-config.md already exists")
1249
+ return
1250
+
1251
+ write_file(target, CODEX_MARKER)
1252
+ success(".codex/agent-config.md created")
1253
+
1254
+
1255
+ # Continue.dev (https://continue.dev) auto-discovers `.continue/rules/*.md`
1256
+ # as system-level rules per project — same pattern as Roo Code. Bridge is
1257
+ # a single marker file in the rules directory; Continue will read it
1258
+ # directly on every session.
1259
+ CONTINUE_MARKER = """# Agent Config bridge — Continue.dev
1260
+
1261
+ This file marks the project as an `event4u/agent-config` consumer.
1262
+
1263
+ Continue.dev auto-discovers `.continue/rules/*.md` as system-level
1264
+ rules per session. The canonical rule and skill source lives under
1265
+ `.augment/` (Augment portability mirror — see `AGENTS.md` for
1266
+ orientation).
1267
+ """
1268
+
1269
+
1270
+ def ensure_continue_bridge(project_root: Path, force: bool) -> None:
1271
+ """Deploy `.continue/rules/agent-config.md` (project scope) marker.
1272
+
1273
+ Continue.dev auto-discovers `.continue/rules/*.md` per project —
1274
+ mirror of the Roo Code pattern. Single marker file pointing
1275
+ developers at the canonical rule source under `.augment/`.
1276
+ """
1277
+ target = project_root / ".continue" / "rules" / "agent-config.md"
1278
+
1279
+ if target.exists() and not force:
1280
+ skip(".continue/rules/agent-config.md already exists")
1281
+ return
1282
+
1283
+ write_file(target, CONTINUE_MARKER)
1284
+ success(".continue/rules/agent-config.md created")
1285
+
1286
+
1287
+ # Kilo Code (https://kilocode.ai/) is a Cline-derived VS Code extension —
1288
+ # Roo Code's fork-cousin. Auto-discovers `.kilocode/rules/*.md` as
1289
+ # system-level instructions per project. Marker-only bridge in the same
1290
+ # spirit as Roo Code / Continue.dev.
1291
+ KILOCODE_MARKER = """# Agent Config bridge — Kilo Code
1292
+
1293
+ This file marks the project as an `event4u/agent-config` consumer.
1294
+
1295
+ Kilo Code auto-discovers `.kilocode/rules/*.md` as system-level rules
1296
+ per session. The canonical rule and skill source lives under
1297
+ `.augment/` (Augment portability mirror — see `AGENTS.md` for
1298
+ orientation).
1299
+ """
1300
+
1301
+
1302
+ def ensure_kilocode_bridge(project_root: Path, force: bool) -> None:
1303
+ """Deploy `.kilocode/rules/agent-config.md` (project scope) marker.
1304
+
1305
+ Kilo Code auto-discovers `.kilocode/rules/*.md` per project — Cline
1306
+ fork pattern mirroring Roo Code. Single marker file pointing
1307
+ developers at the canonical rule source under `.augment/`.
1308
+ """
1309
+ target = project_root / ".kilocode" / "rules" / "agent-config.md"
1310
+
1311
+ if target.exists() and not force:
1312
+ skip(".kilocode/rules/agent-config.md already exists")
1313
+ return
1314
+
1315
+ write_file(target, KILOCODE_MARKER)
1316
+ success(".kilocode/rules/agent-config.md created")
1317
+
1318
+
1319
+ # Zed (https://zed.dev) reads `.rules` at the project root as the
1320
+ # canonical system-instruction file. The bridge drops an informational
1321
+ # marker under `.zed/` documenting agent-config ownership — Zed itself
1322
+ # does not auto-discover `.zed/agent-config.md`. Phase 2.5 may upgrade
1323
+ # to a `.rules` emitter, but mirrors the AGENTS.md story until then.
1324
+ ZED_MARKER = """# Agent Config bridge — Zed
1325
+
1326
+ This file marks the project as an `event4u/agent-config` consumer.
1327
+
1328
+ Zed reads `.rules` at the project root as system-level instructions —
1329
+ that file is the canonical entry point. This marker is informational
1330
+ and tells developers where the rules and skills live.
1331
+
1332
+ To activate agent-config under Zed, point Zed's `.rules` at the
1333
+ canonical source (or symlink it):
1334
+
1335
+ ```
1336
+ # Append to .rules at project root
1337
+ @.augment/AGENTS.md
1338
+ ```
1339
+
1340
+ Canonical rule and skill source: `.augment/` (see `AGENTS.md`).
1341
+ """
1342
+
1343
+
1344
+ def ensure_zed_bridge(project_root: Path, force: bool) -> None:
1345
+ """Deploy `.zed/agent-config.md` informational marker.
1346
+
1347
+ Zed reads `.rules` at the project root directly — the marker is
1348
+ informational and documents the wiring. Phase 2.5 may upgrade to a
1349
+ declarative `.rules` emitter.
1350
+ """
1351
+ target = project_root / ".zed" / "agent-config.md"
1352
+
1353
+ if target.exists() and not force:
1354
+ skip(".zed/agent-config.md already exists")
1355
+ return
1356
+
1357
+ write_file(target, ZED_MARKER)
1358
+ success(".zed/agent-config.md created")
1359
+
1360
+
1361
+ # JetBrains AI Assistant (https://www.jetbrains.com/ai/) reads guidelines
1362
+ # from project-level config files under `.idea/`. To avoid colliding with
1363
+ # the team-shared `.idea/` workspace, the bridge writes to
1364
+ # `.jetbrains/agent-config.md` (informational marker) and documents the
1365
+ # manual wiring step. Phase 2.5 may automate the `.idea/` write behind a
1366
+ # `--force` gate.
1367
+ JETBRAINS_MARKER = """# Agent Config bridge — JetBrains AI Assistant
1368
+
1369
+ This file marks the project as an `event4u/agent-config` consumer.
1370
+
1371
+ JetBrains AI Assistant reads custom prompts and guidelines from
1372
+ project-level config (`.idea/`) and user-scope settings. This marker
1373
+ is informational — to wire agent-config into JetBrains AI, point the
1374
+ assistant's custom-prompts path at `.augment/` or copy the relevant
1375
+ rules into your JetBrains profile.
1376
+
1377
+ Canonical rule and skill source: `.augment/` (see `AGENTS.md`).
1378
+ """
1379
+
1380
+
1381
+ def ensure_jetbrains_bridge(project_root: Path, force: bool) -> None:
1382
+ """Deploy `.jetbrains/agent-config.md` informational marker.
1383
+
1384
+ JetBrains AI reads config from `.idea/` and user-scope paths — we
1385
+ avoid mutating `.idea/` (team-shared workspace) and ship a marker
1386
+ documenting the manual wiring instead.
1387
+ """
1388
+ target = project_root / ".jetbrains" / "agent-config.md"
1389
+
1390
+ if target.exists() and not force:
1391
+ skip(".jetbrains/agent-config.md already exists")
1392
+ return
1393
+
1394
+ write_file(target, JETBRAINS_MARKER)
1395
+ success(".jetbrains/agent-config.md created")
1396
+
1397
+
1398
+ # Kiro (https://kiro.dev) is Amazon's agentic IDE. It auto-discovers
1399
+ # `.kiro/steering/*.md` as steering documents per project — same pattern
1400
+ # as Roo Code / Continue.dev / Kilo Code. Bridge is a single marker
1401
+ # under the steering directory.
1402
+ KIRO_MARKER = """# Agent Config bridge — Kiro
1403
+
1404
+ This file marks the project as an `event4u/agent-config` consumer.
1405
+
1406
+ Kiro auto-discovers `.kiro/steering/*.md` as steering documents per
1407
+ session. The canonical rule and skill source lives under `.augment/`
1408
+ (Augment portability mirror — see `AGENTS.md` for orientation).
1409
+ """
1410
+
1411
+
1412
+ def ensure_kiro_bridge(project_root: Path, force: bool) -> None:
1413
+ """Deploy `.kiro/steering/agent-config.md` (project scope) marker.
1414
+
1415
+ Kiro auto-discovers `.kiro/steering/*.md` per project — Cline /
1416
+ Continue.dev pattern. Single marker file pointing developers at
1417
+ the canonical rule source under `.augment/`.
1418
+ """
1419
+ target = project_root / ".kiro" / "steering" / "agent-config.md"
1420
+
1421
+ if target.exists() and not force:
1422
+ skip(".kiro/steering/agent-config.md already exists")
1423
+ return
1424
+
1425
+ write_file(target, KIRO_MARKER)
1426
+ success(".kiro/steering/agent-config.md created")
1427
+
1428
+
1109
1429
  # --- Post-install smoke test ---
1110
1430
 
1111
1431
  # (platform, native event used for the dry-fire). Probe events are
@@ -1207,16 +1527,547 @@ def _smoke_test_hooks(project_root: Path, package_root: Path) -> int:
1207
1527
  return 1 if failed else 0
1208
1528
 
1209
1529
 
1210
- # --- Global user-level install — RETIRED (road-to-portable-runtime P0.5) ---
1530
+ # --- Global user-level install — re-introduced under ADR-007 ---
1531
+ #
1532
+ # The pre-`5388de25` `--global` was an in-project symlink scheme driven by
1533
+ # `templates/global-install-manifest.yml`. ADR-007 (2026-05-12) rebuilds the
1534
+ # flag as a real-file user-scope install — writes to `~/.claude/`,
1535
+ # `~/.cursor/`, `~/.augment/`, etc. per the per-agent discovery matrix. No
1536
+ # symlinks; no manifest revival. This module currently exposes the dispatch
1537
+ # scaffold; concrete file writes are owned by later roadmap tasks
1538
+ # (export subcommand, lockfile lifecycle).
1539
+
1540
+ # Per-tool user-scope anchor paths per ADR-007 matrix. Listed in tool-ID
1541
+ # order matching `_VALID_TOOLS` so the scaffold output stays predictable.
1542
+ USER_SCOPE_PATHS = {
1543
+ "claude-code": "~/.claude/",
1544
+ "claude-desktop": "~/Library/Application Support/Claude/",
1545
+ "cursor": "~/.cursor/",
1546
+ "windsurf": "~/.codeium/windsurf/",
1547
+ "cline": "~/Documents/Cline/Rules/",
1548
+ "gemini-cli": "~/.gemini/",
1549
+ "copilot": "~/.copilot/",
1550
+ "augment": "~/.augment/",
1551
+ "aider": "~/.aider.conf.yml",
1552
+ "codex": "~/.codex/",
1553
+ "roocode": "~/.roo/",
1554
+ "continue": "~/.continue/",
1555
+ "kilocode": "~/.kilocode/",
1556
+ "zed": "~/.config/zed/",
1557
+ "jetbrains": "~/.config/JetBrains/",
1558
+ "kiro": "~/.kiro/",
1559
+ }
1560
+
1561
+
1562
+ # Per-tool scope support per ADR-007 matrix + Tier-1/2 verification.
1563
+ # Values: "both" · "project" · "global". Used by _validate_scope() to
1564
+ # reject explicit `--tools=X` selections that conflict with the chosen
1565
+ # scope (project default or `--global`). `--tools=all` silently filters
1566
+ # incompatible IDs so the default install path stays backward-compatible.
1211
1567
  #
1212
- # The `--global` symlink-install scheme was retired under the npx-only
1213
- # distribution model. The curated manifest (templates/global-install-manifest.yml)
1214
- # and its helpers/dispatch are gone. The replacement is `npx @event4u/agent-config`
1215
- # resolving the runtime per-invocation; the project's `.agent-settings.yml`
1216
- # carries the version pin.
1568
+ # Rationale:
1569
+ # - claude-desktop has no project discovery (informational marker only
1570
+ # in project trees); --project rejects it explicitly.
1571
+ # - jetbrains avoids mutating team-shared .idea/; --project marker is
1572
+ # informational only; canonical scope is global.
1573
+ # - roocode / kilocode auto-discover `.roo/rules/` and `.kilocode/rules/`
1574
+ # per project; no user-scope discovery convention; --global rejects.
1575
+ SCOPE_SUPPORT = {
1576
+ "claude-code": "both",
1577
+ "claude-desktop": "global",
1578
+ "cursor": "both",
1579
+ "windsurf": "both",
1580
+ "cline": "both",
1581
+ "gemini-cli": "both",
1582
+ "copilot": "both",
1583
+ "augment": "both",
1584
+ "aider": "both",
1585
+ "codex": "both",
1586
+ "roocode": "project",
1587
+ "continue": "both",
1588
+ "kilocode": "project",
1589
+ "zed": "both",
1590
+ "jetbrains": "global",
1591
+ "kiro": "both",
1592
+ }
1593
+
1594
+
1595
+ # Per-tool bridge marker paths used by the project-scope manifest (ADR-008
1596
+ # Phase 3.2). The value is the relative path inside the project tree (for
1597
+ # `scope=project`) or the absolute / `~`-prefixed user-scope path (for
1598
+ # `scope=global`). `validate` (Phase 3.4) checks that this file exists; the
1599
+ # manifest stores the path verbatim so a relocation of a bridge stays visible
1600
+ # in the lockfile.
1601
+ PROJECT_BRIDGE_MARKERS = {
1602
+ "claude-code": ".claude/settings.json",
1603
+ "claude-desktop": ".claude-desktop/agent-config.md",
1604
+ "cursor": ".cursor/hooks.json",
1605
+ "windsurf": ".windsurf/hooks.json",
1606
+ "cline": ".clinerules/hooks",
1607
+ "gemini-cli": ".gemini/settings.json",
1608
+ "copilot": ".github/plugin/marketplace.json",
1609
+ "augment": ".augment/settings.json",
1610
+ "aider": ".aider/agent-config.md",
1611
+ "codex": ".codex/agent-config.md",
1612
+ "roocode": ".roo/rules/agent-config.md",
1613
+ "continue": ".continue/rules/agent-config.md",
1614
+ "kilocode": ".kilocode/rules/agent-config.md",
1615
+ "zed": ".zed/agent-config.md",
1616
+ "jetbrains": ".jetbrains/agent-config.md",
1617
+ "kiro": ".kiro/steering/agent-config.md",
1618
+ }
1619
+
1620
+
1621
+ def _bridge_marker(tool_id: str, scope: str) -> str:
1622
+ """Return the canonical bridge-marker path for ``(tool_id, scope)``.
1623
+
1624
+ Project scope returns the repo-relative marker (e.g. ``.roo/rules/agent-
1625
+ config.md``). Global scope returns the user-scope anchor from
1626
+ :data:`USER_SCOPE_PATHS` (e.g. ``~/.claude/``). ADR-008 stores both as
1627
+ opaque strings; `validate` (Phase 3.4) handles the existence check.
1628
+ """
1629
+ if scope == "global":
1630
+ return USER_SCOPE_PATHS.get(tool_id, "")
1631
+ return PROJECT_BRIDGE_MARKERS.get(tool_id, "")
1632
+
1633
+
1634
+ def _validate_scope(tools: set[str], scope: str, was_all: bool) -> set[str]:
1635
+ """Validate tools against requested scope per SCOPE_SUPPORT.
1636
+
1637
+ `scope` is "project" or "global". When `was_all` is True (user passed
1638
+ `--tools=all` or omitted the flag), incompatible tools are silently
1639
+ filtered so the default install stays backward-compatible. Explicit
1640
+ tool lists hard-reject with a directive error per Phase 2.3.
1641
+ """
1642
+ if scope not in ("project", "global"):
1643
+ fail(f"_validate_scope: unknown scope '{scope}'")
1644
+ incompatible = sorted(
1645
+ t for t in tools
1646
+ if SCOPE_SUPPORT.get(t, "both") not in ("both", scope)
1647
+ )
1648
+ if not incompatible:
1649
+ return tools
1650
+ if was_all:
1651
+ return {t for t in tools if t not in incompatible}
1652
+ hint = (
1653
+ "drop --global (project is the default scope)"
1654
+ if scope == "global" else "use --global"
1655
+ )
1656
+ fail(
1657
+ f"--tools: {', '.join(incompatible)} does not support "
1658
+ f"--{scope} scope ({hint})"
1659
+ )
1660
+ return tools # unreachable; fail() exits
1661
+
1662
+
1663
+ def _resolve_scope(
1664
+ opts: "argparse.Namespace",
1665
+ detected: str,
1666
+ detect_reason: str,
1667
+ custom_path: "Path | None",
1668
+ ) -> str:
1669
+ """Phase 1.4 — turn flags + detection into a concrete scope.
1670
+
1671
+ Precedence:
1672
+
1673
+ 1. ``--scope=project|global`` — explicit override; ``--custom-path``
1674
+ may steer the project root.
1675
+ 2. ``--scope=prompt`` — force the interactive chooser; ``--custom-path``
1676
+ pre-fills the Custom branch.
1677
+ 3. ``--scope=auto`` — honor detection; trigger the prompt on
1678
+ ``"prompt"``.
1679
+ 4. ``--global`` — legacy alias for ``--scope=global``.
1680
+ 5. No flag — backward-compatible default of ``project``, EXCEPT
1681
+ when detection returns ``"prompt"``, in which case the
1682
+ interactive chooser fires (matches ADR-007 D2 collision UX).
1683
+
1684
+ Returns ``"project"`` or ``"global"``. Never returns ``"prompt"`` —
1685
+ that intermediate state is resolved here. Aborts via ``fail()`` if
1686
+ a prompt is required but stdin is not a TTY.
1687
+ """
1688
+ # Explicit --scope wins.
1689
+ if opts.scope == "project":
1690
+ return "project"
1691
+ if opts.scope == "global":
1692
+ return "global"
1693
+ if opts.scope == "prompt":
1694
+ return _run_scope_prompt(opts, detect_reason or "forced by --scope=prompt", custom_path)
1695
+ if opts.scope == "auto":
1696
+ if detected == "prompt":
1697
+ return _run_scope_prompt(opts, detect_reason, custom_path)
1698
+ if not QUIET:
1699
+ info(f"Scope: {detected} (auto-detected; {detect_reason})")
1700
+ return detected
1701
+
1702
+ # --global legacy alias.
1703
+ if opts.global_install:
1704
+ return "global"
1705
+
1706
+ # No flag — legacy default with collision auto-prompt.
1707
+ if detected == "prompt":
1708
+ return _run_scope_prompt(opts, detect_reason, custom_path)
1709
+ if not QUIET:
1710
+ info(f"Scope detection: {detected} ({detect_reason}). Using project default for backward compatibility; pass --scope=auto to honor detection.")
1711
+ return "project"
1712
+
1713
+
1714
+ def _run_scope_prompt(
1715
+ opts: "argparse.Namespace",
1716
+ reason: str,
1717
+ custom_path: "Path | None",
1718
+ ) -> str:
1719
+ """Drive the interactive scope chooser, mutating ``opts.custom_path``
1720
+ when the user picks the Custom branch.
1721
+
1722
+ Fails fast (no prompt) when stdin is not a TTY and ``--custom-path``
1723
+ was not pre-supplied — CI callers must use ``--scope=project|global``.
1724
+ """
1725
+ if not sys.stdin.isatty() and custom_path is None:
1726
+ fail(
1727
+ "Ambiguous install scope detected and stdin is not a TTY. "
1728
+ "Pass --scope=project|global (or --custom-path=<dir>) to override."
1729
+ )
1730
+ choice = prompt_scope_choice(reason)
1731
+ if choice == "project":
1732
+ return "project"
1733
+ if choice == "global":
1734
+ return "global"
1735
+ # SCOPE_CUSTOM — resolve a destination path.
1736
+ if custom_path is None:
1737
+ try:
1738
+ raw = _read_line("Custom destination path: ")
1739
+ except EOFError:
1740
+ fail("Custom-path prompt aborted (EOF on stdin)")
1741
+ if not raw:
1742
+ fail("Custom-path prompt requires a non-empty path")
1743
+ custom_path = Path(raw).expanduser().resolve()
1744
+ opts.custom_path = str(custom_path)
1745
+ if not QUIET:
1746
+ info(f"Custom destination: {custom_path}")
1747
+ return "project"
1748
+
1749
+
1750
+ # Manifest files used by multi-signal scope detection (Phase 1.3). Listed
1751
+ # in the order they are most commonly the canonical project signal; the
1752
+ # detector short-circuits on the first hit. `.git/` is intentionally
1753
+ # absent — monorepos, dotfile-git repos and Hg/SVN workspaces all break
1754
+ # that signal (ADR-007 D2).
1755
+ SCOPE_DETECT_MANIFESTS = (
1756
+ "package.json", "composer.json", "pyproject.toml",
1757
+ "Cargo.toml", "go.mod", "Gemfile",
1758
+ )
1759
+
1760
+ # Project-local AI-tool config that, if present alongside a manifest,
1761
+ # triggers the ambiguity prompt. Directories first, then well-known
1762
+ # top-level files. Conservative on purpose: false negatives (skip prompt,
1763
+ # install global) are recoverable via `--project`; false positives (prompt
1764
+ # in an empty dir) are a UX paper-cut.
1765
+ SCOPE_DETECT_AI_DIRS = (
1766
+ ".claude", ".cursor", ".windsurf", ".augment",
1767
+ ".clinerules", ".copilot", ".gemini", ".codex",
1768
+ ".aider", ".continue", ".roo", ".kilocode",
1769
+ )
1770
+ SCOPE_DETECT_AI_FILES = (
1771
+ "CLAUDE.md", "AGENTS.md", "GEMINI.md",
1772
+ ".windsurfrules", ".aider.conf.yml",
1773
+ )
1774
+
1775
+
1776
+ def detect_scope(cwd: Path) -> tuple[str, str]:
1777
+ """Multi-signal scope detection per ADR-007 D2 / Phase 1.3.
1778
+
1779
+ Returns ``(scope, reason)`` where ``scope`` is one of:
1780
+
1781
+ * ``"project"`` — install into ``cwd`` (current behaviour preserved).
1782
+ Triggered by an existing ``.agent-settings.yml`` in ``cwd``.
1783
+ * ``"prompt"`` — caller MUST resolve the ambiguity (interactive
1784
+ prompt in 1.4 / ``--scope=<x>`` override for CI). Triggered by
1785
+ a manifest file (``package.json`` / ``composer.json`` / etc.)
1786
+ combined with at least one project-local AI-tool config marker.
1787
+ * ``"global"`` — install to user-scope paths. Default when no other
1788
+ signal fires (including ``cwd == ~``, empty dir, dotfile-git).
1789
+
1790
+ ``.git/`` is explicitly NOT a signal — monorepos, dotfile managers,
1791
+ and non-Git workspaces all break it. Pure function; no side effects.
1792
+ """
1793
+ if (cwd / SETTINGS_FILE).exists():
1794
+ return "project", f"existing {SETTINGS_FILE}"
1795
+
1796
+ has_manifest = next(
1797
+ (m for m in SCOPE_DETECT_MANIFESTS if (cwd / m).exists()),
1798
+ None,
1799
+ )
1800
+ has_ai_dir = next(
1801
+ (d for d in SCOPE_DETECT_AI_DIRS if (cwd / d).is_dir()),
1802
+ None,
1803
+ )
1804
+ has_ai_file = next(
1805
+ (f for f in SCOPE_DETECT_AI_FILES if (cwd / f).exists()),
1806
+ None,
1807
+ )
1808
+
1809
+ if has_manifest and (has_ai_dir or has_ai_file):
1810
+ marker = has_ai_dir or has_ai_file
1811
+ return "prompt", f"manifest ({has_manifest}) + AI-tool config ({marker})"
1812
+
1813
+ return "global", "no project-scope signals"
1814
+
1815
+
1816
+ # --- Interactive prompts (Phase 1.4) ---
1817
+
1818
+ # Sentinel returned by `prompt_scope_choice` for the "Custom path" branch.
1819
+ # The caller must follow up by reading a path (CLI: `--custom-path`; TTY:
1820
+ # a second prompt line). Kept as a constant so call sites can match on
1821
+ # identity rather than the string literal.
1822
+ SCOPE_CUSTOM = "custom"
1823
+
1824
+
1825
+ def _read_line(prompt_text: str) -> str:
1826
+ """Thin wrapper over `input()` so tests can monkey-patch a single point.
1827
+
1828
+ Returns the user's stripped reply. Raises ``EOFError`` on Ctrl-D so
1829
+ callers can fail-fast rather than loop on closed stdin.
1830
+ """
1831
+ return input(prompt_text).strip()
1832
+
1833
+
1834
+ def prompt_scope_choice(reason: str) -> str:
1835
+ """Interactive 3-option scope chooser per ADR-007 D2 / Phase 1.4.
1836
+
1837
+ Returns one of ``"project"``, ``"global"``, ``SCOPE_CUSTOM``. The
1838
+ caller is responsible for resolving ``SCOPE_CUSTOM`` to an actual
1839
+ path (e.g. by reading ``--custom-path`` or a follow-up prompt).
1840
+
1841
+ Loops on invalid input; aborts the install via ``fail()`` on EOF
1842
+ (Ctrl-D) or three consecutive invalid replies so a stuck CI run
1843
+ cannot hang the parent process.
1844
+ """
1845
+ print()
1846
+ info(f"Ambiguous install scope: {reason}.")
1847
+ info("Choose where to install:")
1848
+ print(" 1) Project — install into the current directory")
1849
+ print(" 2) User — install into ~/ (recommended; one install per machine)")
1850
+ print(" 3) Custom — specify an explicit destination path")
1851
+ print()
1852
+ attempts = 0
1853
+ while attempts < 3:
1854
+ try:
1855
+ reply = _read_line("Choose [1/2/3]: ")
1856
+ except EOFError:
1857
+ fail("Scope prompt aborted (EOF on stdin); pass --scope=project|global to override")
1858
+ if reply in ("1", "project", "p"):
1859
+ return "project"
1860
+ if reply in ("2", "global", "user", "u", "g"):
1861
+ return "global"
1862
+ if reply in ("3", "custom", "c"):
1863
+ return SCOPE_CUSTOM
1864
+ attempts += 1
1865
+ warn(f"Invalid choice '{reply}'. Enter 1, 2, or 3.")
1866
+ fail("Scope prompt aborted (3 invalid replies); pass --scope=project|global to override")
1867
+ return "project" # unreachable; fail() exits
1868
+
1869
+
1870
+ def prompt_collision_choice(path: Path) -> str:
1871
+ """Hard-Floor 3-option prompt for an existing user-scope config file.
1872
+
1873
+ Returns one of ``"merge"``, ``"backup"``, ``"abort"``. Used by future
1874
+ write code paths (1.5 export, 1.6 lockfile) before clobbering an
1875
+ existing ``~/.claude/CLAUDE.md`` / ``~/.codex/AGENTS.md`` / etc. The
1876
+ helper itself does not touch the filesystem; the caller owns the
1877
+ merge / rename-to-`.bak.<ts>` / exit-1 action.
1878
+ """
1879
+ print()
1880
+ warn(f"Existing file at {path}")
1881
+ info("Choose how to handle the collision:")
1882
+ print(" 1) Merge — append our content, preserve theirs")
1883
+ print(" 2) Backup and replace — rename existing to .bak.<ts>, write fresh")
1884
+ print(" 3) Abort — leave the file untouched, exit non-zero")
1885
+ print()
1886
+ attempts = 0
1887
+ while attempts < 3:
1888
+ try:
1889
+ reply = _read_line("Choose [1/2/3]: ")
1890
+ except EOFError:
1891
+ fail(f"Collision prompt aborted (EOF on stdin) for {path}")
1892
+ if reply in ("1", "merge", "m"):
1893
+ return "merge"
1894
+ if reply in ("2", "backup", "b"):
1895
+ return "backup"
1896
+ if reply in ("3", "abort", "a"):
1897
+ return "abort"
1898
+ attempts += 1
1899
+ warn(f"Invalid choice '{reply}'. Enter 1, 2, or 3.")
1900
+ fail(f"Collision prompt aborted (3 invalid replies) for {path}")
1901
+ return "abort" # unreachable
1902
+
1903
+
1904
+ def _load_installed_lock_module():
1905
+ """Lazy-import ``scripts._lib.installed_lock`` regardless of load mode.
1906
+
1907
+ ``install.py`` runs both as a top-level script (``python3 scripts/install.py``)
1908
+ and as ``scripts.install`` (via ``from scripts.install import …``). The
1909
+ repo root has to be on ``sys.path`` for the package-qualified import to
1910
+ resolve in the script case.
1911
+ """
1912
+ pkg_root = str(Path(__file__).resolve().parents[1])
1913
+ if pkg_root not in sys.path:
1914
+ sys.path.insert(0, pkg_root)
1915
+ from scripts._lib import installed_lock # noqa: WPS433 — lazy by design
1916
+ return installed_lock
1917
+
1918
+
1919
+ def _load_installed_tools_module():
1920
+ """Lazy-import ``scripts._lib.installed_tools`` (ADR-008 manifest)."""
1921
+ pkg_root = str(Path(__file__).resolve().parents[1])
1922
+ if pkg_root not in sys.path:
1923
+ sys.path.insert(0, pkg_root)
1924
+ from scripts._lib import installed_tools # noqa: WPS433 — lazy by design
1925
+ return installed_tools
1926
+
1927
+
1928
+ def _update_installed_tools_manifest(
1929
+ project_root: Path,
1930
+ tools: set[str],
1931
+ scope: str,
1932
+ force: bool,
1933
+ ) -> int:
1934
+ """Append / refresh project-scope manifest entries (ADR-008 Phase 3.2).
1935
+
1936
+ Called after the bridge-write phase succeeds. The manifest lives at
1937
+ ``<project_root>/agents/installed-tools.lock`` and tracks which AI tools
1938
+ this project expects, separate from ``.agent-project-settings.yml``
1939
+ (behaviour). Idempotent on (name, scope) match; refuses scope changes
1940
+ without ``--force`` per ADR-008 § Lifecycle.
1941
+
1942
+ Returns 0 on success, 1 on refusal (scope mismatch without ``--force``).
1943
+ """
1944
+ tools_mod = _load_installed_tools_module()
1945
+ target = tools_mod.manifest_path(project_root)
1946
+ existing = tools_mod.read_manifest(target) or {}
1947
+ entries = list(existing.get("tools", []))
1948
+
1949
+ lock_mod = _load_installed_lock_module()
1950
+ version = lock_mod.current_package_version()
1951
+
1952
+ for tool_id in sorted(tools):
1953
+ marker = _bridge_marker(tool_id, scope)
1954
+ if not marker:
1955
+ # Substrate (vscode) or unknown — not tracked in the manifest.
1956
+ continue
1957
+ try:
1958
+ entries = tools_mod.upsert_tool(
1959
+ entries,
1960
+ name=tool_id,
1961
+ scope=scope,
1962
+ bridge_marker=marker,
1963
+ force=force,
1964
+ )
1965
+ except tools_mod.ScopeMismatchError as exc:
1966
+ if not QUIET:
1967
+ warn(str(exc))
1968
+ info(f" Manifest: {target}")
1969
+ info(" Override: re-run with `--force` to rewrite the entry")
1970
+ return 1
1971
+
1972
+ tools_mod.write_manifest(target, version, entries)
1973
+ if not QUIET:
1974
+ info(f"Manifest updated: {target.relative_to(project_root) if target.is_relative_to(project_root) else target}")
1975
+ return 0
1976
+
1977
+
1978
+ def install_global(
1979
+ tools: set[str],
1980
+ force: bool,
1981
+ project_root: Path | None = None,
1982
+ ) -> int:
1983
+ """User-scope install path (ADR-007 + Phase 1.6 lockfile lifecycle).
1984
+
1985
+ Reads ``~/.config/agent-config/installed.lock`` first. A recorded
1986
+ version that does not match the running package version refuses the
1987
+ install with a remediation hint unless ``--force`` is passed. On
1988
+ success the lockfile is rewritten atomically with the current
1989
+ package version + the union of previously-recorded and now-installed
1990
+ tool IDs. Concrete per-tool file writes still belong to later tasks
1991
+ (export remains the documented escape for project-local content).
1992
+
1993
+ When ``project_root`` points at a project tree (detected by the
1994
+ presence of ``.agent-settings.yml``), the project-scope manifest at
1995
+ ``agents/installed-tools.lock`` is also refreshed with ``scope=global``
1996
+ entries per ADR-008 Phase 3.2.
1997
+ """
1998
+ lock_mod = _load_installed_lock_module()
1999
+ installed_version = lock_mod.current_package_version()
2000
+ lock_path = lock_mod.lockfile_path()
2001
+ ok, recorded = lock_mod.check_version(installed_version, path=lock_path)
2002
+
2003
+ if not ok and not force:
2004
+ if not QUIET:
2005
+ print()
2006
+ warn("Refusing global install: lockfile version mismatch.")
2007
+ info(f" Lockfile: {lock_path}")
2008
+ info(f" Recorded version: {recorded}")
2009
+ info(f" Current package: {installed_version}")
2010
+ info(" Fix: run `agent-config update`")
2011
+ info(" Override: re-run with `--force` (replaces the lockfile)")
2012
+ print()
2013
+ return 1
2014
+
2015
+ if not QUIET:
2016
+ print()
2017
+ info("Agent Config — Global (user-scope) install [ADR-007]")
2018
+ info("Planned per-tool anchor paths:")
2019
+ for tool_id in sorted(tools):
2020
+ anchor = USER_SCOPE_PATHS.get(tool_id)
2021
+ if anchor is None:
2022
+ continue
2023
+ print(f" {tool_id:<15} → {anchor}")
2024
+
2025
+ existing = lock_mod.read_lockfile(path=lock_path) or {}
2026
+ existing_tools = list(existing.get("tools", []))
2027
+ merged_tools = sorted(set(existing_tools) | set(tools))
2028
+ written = lock_mod.write_lockfile(installed_version, merged_tools, path=lock_path)
2029
+
2030
+ if not QUIET:
2031
+ print()
2032
+ info(f"Lockfile written: {written}")
2033
+ info(f" schema_version=1, agent_config_version={installed_version}")
2034
+ info(f" tools={','.join(merged_tools)}")
2035
+
2036
+ # Refresh the project-scope manifest when running inside a project tree
2037
+ # (ADR-008 Phase 3.2). Outside a project (e.g. plain `~/`) there is no
2038
+ # manifest to write and the global lockfile alone is the source of truth.
2039
+ if project_root is not None and (project_root / SETTINGS_FILE).exists():
2040
+ rc = _update_installed_tools_manifest(project_root, tools, "global", force)
2041
+ if rc != 0:
2042
+ return rc
2043
+
2044
+ if not QUIET:
2045
+ print()
2046
+ success("Global install recorded.")
2047
+ print()
2048
+ return 0
1217
2049
 
1218
2050
  # --- Argument parsing ---
1219
2051
 
2052
+ def _merge_tools_aliases(tools: str | None, ai: str | None) -> str:
2053
+ """Merge --tools and --ai into a single comma-separated value.
2054
+
2055
+ `--ai` is an alias for `--tools` (Phase 2.4 of the global-first
2056
+ roadmap). Both accepted; when both are passed the comma-separated
2057
+ values are unioned (order-preserving, deduplicated). When neither
2058
+ is passed the default `all` keeps the backward-compatible behaviour.
2059
+ """
2060
+ items: list[str] = []
2061
+ for raw in (tools, ai):
2062
+ if not raw:
2063
+ continue
2064
+ for piece in raw.split(","):
2065
+ stripped = piece.strip()
2066
+ if stripped and stripped not in items:
2067
+ items.append(stripped)
2068
+ return ",".join(items) if items else "all"
2069
+
2070
+
1220
2071
  def parse_options(argv: list[str]) -> argparse.Namespace:
1221
2072
  parser = argparse.ArgumentParser(
1222
2073
  prog="install.py",
@@ -1260,19 +2111,64 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
1260
2111
  parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
1261
2112
  parser.add_argument(
1262
2113
  "--tools",
1263
- default="all",
2114
+ default=None,
1264
2115
  help=(
1265
2116
  "comma-separated tool IDs to install bridges for "
1266
- "(claude-code,cursor,windsurf,cline,gemini-cli,copilot,augment,aider,codex,all). "
2117
+ "(claude-code,claude-desktop,cursor,windsurf,cline,gemini-cli,"
2118
+ "copilot,augment,aider,codex,roocode,continue,kilocode,zed,"
2119
+ "jetbrains,kiro,all). "
1267
2120
  "Default: all (backward-compatible)."
1268
2121
  ),
1269
2122
  )
2123
+ parser.add_argument(
2124
+ "--ai",
2125
+ default=None,
2126
+ help=(
2127
+ "alias for --tools (same IDs accepted). When both are passed "
2128
+ "the comma-separated values are unioned. Default: all."
2129
+ ),
2130
+ )
1270
2131
  parser.add_argument(
1271
2132
  "--no-smoke",
1272
2133
  action="store_true",
1273
2134
  help="skip the post-install hook smoke test (default: dry-fire dispatch:hook against every installed bridge)",
1274
2135
  )
1275
- return parser.parse_args(argv)
2136
+ parser.add_argument(
2137
+ "--global",
2138
+ dest="global_install",
2139
+ action="store_true",
2140
+ help="install to user-scope paths (~/.claude/, ~/.cursor/, …) per ADR-007 instead of project-locally",
2141
+ )
2142
+ parser.add_argument(
2143
+ "--scope",
2144
+ choices=("project", "global", "prompt", "auto"),
2145
+ default=None,
2146
+ help=(
2147
+ "force install scope (overrides --global and detection): "
2148
+ "project = install into cwd; global = install into user-scope paths; "
2149
+ "prompt = force the interactive 3-option chooser; "
2150
+ "auto = honor detect_scope() output. Default: legacy "
2151
+ "(project unless --global, with auto-prompt on collision detection)."
2152
+ ),
2153
+ )
2154
+ parser.add_argument(
2155
+ "--custom-path",
2156
+ default=None,
2157
+ help=(
2158
+ "destination root for --scope=project when not the cwd "
2159
+ "(used by the 'Custom' branch of the scope chooser; ignored "
2160
+ "for --scope=global)."
2161
+ ),
2162
+ )
2163
+ opts = parser.parse_args(argv)
2164
+ opts.tools = _merge_tools_aliases(opts.tools, opts.ai)
2165
+ if opts.scope == "global" and opts.custom_path:
2166
+ fail("--custom-path is incompatible with --scope=global")
2167
+ if opts.global_install and opts.custom_path:
2168
+ fail("--custom-path is incompatible with --global")
2169
+ if opts.scope is not None and opts.global_install and opts.scope != "global":
2170
+ fail(f"--scope={opts.scope} conflicts with --global; pick one")
2171
+ return opts
1276
2172
 
1277
2173
 
1278
2174
  # Mapping of --tools IDs accepted by install.py. Mirrors VALID_TOOLS in
@@ -1280,7 +2176,8 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
1280
2176
  # bridges (vscode, augment) always run.
1281
2177
  _VALID_TOOLS = {
1282
2178
  "claude-code", "claude-desktop", "cursor", "windsurf", "cline",
1283
- "gemini-cli", "copilot", "augment", "aider", "codex", "all",
2179
+ "gemini-cli", "copilot", "augment", "aider", "codex", "roocode",
2180
+ "continue", "kilocode", "zed", "jetbrains", "kiro", "all",
1284
2181
  }
1285
2182
 
1286
2183
 
@@ -1303,6 +2200,18 @@ def _parse_tools(raw: str) -> set[str]:
1303
2200
  return set(items)
1304
2201
 
1305
2202
 
2203
+ def _tools_was_all(raw: str) -> bool:
2204
+ """True when the raw --tools value is the implicit/explicit `all` set.
2205
+
2206
+ Used by _validate_scope() to decide between silent-filter (default
2207
+ install backward-compatible) and hard-reject (explicit list).
2208
+ """
2209
+ if not raw or not raw.strip():
2210
+ return False
2211
+ items = [s.strip() for s in raw.split(",") if s.strip()]
2212
+ return "all" in items
2213
+
2214
+
1306
2215
  def _is_tool_enabled(tools: set[str], tool_id: str) -> bool:
1307
2216
  return tool_id in tools
1308
2217
 
@@ -1318,7 +2227,30 @@ def main(argv: list[str]) -> int:
1318
2227
  if opts.profile not in SUPPORTED_PROFILES:
1319
2228
  fail(f"Unsupported profile: {opts.profile}. Supported: {', '.join(SUPPORTED_PROFILES)}")
1320
2229
 
1321
- project_root = Path(opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()).resolve()
2230
+ # Multi-signal scope detection (Phase 1.3) + scope resolution
2231
+ # (Phase 1.4). Order of precedence (highest first):
2232
+ # 1. --scope=<x> — explicit user override (CI-friendly; auto = honor detection)
2233
+ # 2. --global — legacy alias for --scope=global
2234
+ # 3. detect_scope() == "prompt" → interactive 3-option chooser (TTY only)
2235
+ # 4. Legacy default → project (preserved for backward compatibility)
2236
+ detect_root = Path(opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()).resolve()
2237
+ detected, detect_reason = detect_scope(detect_root)
2238
+ custom_path: Path | None = Path(opts.custom_path).resolve() if opts.custom_path else None
2239
+ scope = _resolve_scope(opts, detected, detect_reason, custom_path)
2240
+
2241
+ # Scope validation runs before filesystem / package detection so
2242
+ # --tools=X / --scope conflicts fail fast with a directive error
2243
+ # instead of partial-state side effects (Phase 2.3).
2244
+ parsed_tools = _parse_tools(opts.tools)
2245
+ tools_was_all = _tools_was_all(opts.tools)
2246
+ parsed_tools = _validate_scope(parsed_tools, scope, tools_was_all)
2247
+
2248
+ if scope == "global":
2249
+ # Pass detect_root so the manifest refresh runs when --global is
2250
+ # invoked from within a project tree (ADR-008 Phase 3.2).
2251
+ return install_global(parsed_tools, opts.force, project_root=detect_root)
2252
+
2253
+ project_root = custom_path or Path(opts.project or os.environ.get("PROJECT_ROOT") or os.getcwd()).resolve()
1322
2254
  is_first_run = not (project_root / SETTINGS_FILE).exists()
1323
2255
 
1324
2256
  if opts.package:
@@ -1341,7 +2273,7 @@ def main(argv: list[str]) -> int:
1341
2273
 
1342
2274
  ensure_agent_settings(project_root, package_root, opts.profile, opts.force)
1343
2275
 
1344
- tools = _parse_tools(opts.tools)
2276
+ tools = parsed_tools
1345
2277
 
1346
2278
  if not opts.skip_bridges:
1347
2279
  # Substrate bridges (always written; other tools symlink/depend on them).
@@ -1360,6 +2292,24 @@ def main(argv: list[str]) -> int:
1360
2292
  ensure_gemini_bridge(project_root, opts.force)
1361
2293
  if _is_tool_enabled(tools, "copilot"):
1362
2294
  ensure_copilot_bridge(project_root, opts.force)
2295
+ if _is_tool_enabled(tools, "roocode"):
2296
+ ensure_roocode_bridge(project_root, opts.force)
2297
+ if _is_tool_enabled(tools, "claude-desktop"):
2298
+ ensure_claude_desktop_bridge(project_root, opts.force)
2299
+ if _is_tool_enabled(tools, "aider"):
2300
+ ensure_aider_bridge(project_root, opts.force)
2301
+ if _is_tool_enabled(tools, "codex"):
2302
+ ensure_codex_bridge(project_root, opts.force)
2303
+ if _is_tool_enabled(tools, "continue"):
2304
+ ensure_continue_bridge(project_root, opts.force)
2305
+ if _is_tool_enabled(tools, "kilocode"):
2306
+ ensure_kilocode_bridge(project_root, opts.force)
2307
+ if _is_tool_enabled(tools, "zed"):
2308
+ ensure_zed_bridge(project_root, opts.force)
2309
+ if _is_tool_enabled(tools, "jetbrains"):
2310
+ ensure_jetbrains_bridge(project_root, opts.force)
2311
+ if _is_tool_enabled(tools, "kiro"):
2312
+ ensure_kiro_bridge(project_root, opts.force)
1363
2313
 
1364
2314
  if opts.augment_user_hooks:
1365
2315
  ensure_augment_user_hooks(package_root, opts.force)
@@ -1382,6 +2332,17 @@ def main(argv: list[str]) -> int:
1382
2332
  info("Smoke-testing installed hook bridges (dry-run)")
1383
2333
  _smoke_test_hooks(project_root, package_root)
1384
2334
 
2335
+ # Refresh the project-scope installed-tools manifest (ADR-008 Phase 3.2).
2336
+ # Runs after bridges are on disk so the manifest only lists tools whose
2337
+ # markers actually exist. Skipped when `--skip-bridges` was used (the
2338
+ # caller is exercising the install plan, not committing to it).
2339
+ if not opts.skip_bridges:
2340
+ rc = _update_installed_tools_manifest(
2341
+ project_root, parsed_tools, "project", opts.force,
2342
+ )
2343
+ if rc != 0:
2344
+ return rc
2345
+
1385
2346
  if not QUIET:
1386
2347
  print()
1387
2348
  success("Done.")
@@ -1394,7 +2355,7 @@ def main(argv: list[str]) -> int:
1394
2355
  print()
1395
2356
  print(" Next steps:")
1396
2357
  print(" • Commit .agent-settings.yml and bridge files to your repo")
1397
- print(" • New team members run `npx @event4u/create-agent-config init` — done")
2358
+ print(" • New team members run `npx @event4u/agent-config init` — done")
1398
2359
  print(" • Inspect hook coverage: ./agent-config hooks:status")
1399
2360
  print(" • Full walkthrough: https://github.com/event4u-app/agent-config/blob/main/docs/getting-started.md")
1400
2361
  print()