@event4u/agent-config 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent-src/commands/fix/{pr-bots.md → pr-bot-comments.md} +3 -3
- package/.agent-src/commands/fix/{pr.md → pr-comments.md} +6 -6
- package/.agent-src/commands/fix/{pr-developers.md → pr-developer-comments.md} +3 -3
- package/.agent-src/commands/fix.md +6 -6
- package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +2 -2
- package/.agent-src/rules/no-cheap-questions.md +11 -2
- package/.agent-src/skills/readme-writing-package/SKILL.md +24 -0
- package/.claude-plugin/marketplace.json +4 -4
- package/CHANGELOG.md +79 -0
- package/README.md +76 -12
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +3 -3
- package/docs/contracts/command-clusters.md +3 -3
- package/docs/contracts/file-ownership-matrix.json +9 -9
- package/docs/contracts/tier-3-contrib-plugin.md +129 -0
- package/docs/decisions/ADR-007-agent-discovery-scopes.md +278 -0
- package/docs/decisions/ADR-008-installed-tools-manifest.md +160 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/getting-started.md +16 -25
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +32 -0
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +135 -0
- package/docs/installation.md +116 -49
- package/docs/migrations/commands-1.15.0.md +3 -3
- package/docs/setup/per-ide/claude-desktop.md +8 -4
- package/docs/skills-catalog.md +23 -2
- package/docs/troubleshooting.md +20 -32
- package/llms.txt +22 -1
- package/package.json +1 -1
- package/scripts/_cli/cmd_export.py +157 -0
- package/scripts/_cli/cmd_sync.py +162 -0
- package/scripts/_cli/cmd_update.py +23 -1
- package/scripts/_cli/cmd_validate.py +164 -0
- package/scripts/_lib/installed_lock.py +160 -0
- package/scripts/_lib/installed_tools.py +237 -0
- package/scripts/agent-config +62 -0
- package/scripts/install +68 -13
- package/scripts/install.py +984 -33
- package/scripts/install.sh +6 -11
- package/templates/agent-config-wrapper.sh +40 -25
- package/templates/consumer-settings/README.md +2 -2
- package/scripts/setup.sh +0 -230
package/scripts/install.py
CHANGED
|
@@ -97,43 +97,34 @@ def fail(msg: str) -> "None":
|
|
|
97
97
|
# --- Package detection ---
|
|
98
98
|
|
|
99
99
|
def detect_package_root(project_root: Path) -> Path:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
]
|
|
104
|
-
for path in candidates:
|
|
105
|
-
if path.is_dir():
|
|
106
|
-
return path.resolve()
|
|
100
|
+
npm_path = project_root / "node_modules" / "@event4u" / "agent-config"
|
|
101
|
+
if npm_path.is_dir():
|
|
102
|
+
return npm_path.resolve()
|
|
107
103
|
|
|
108
104
|
# Running from within the package itself (development mode)
|
|
109
105
|
if (project_root / "config" / "profiles" / "minimal.ini").exists():
|
|
110
106
|
return project_root
|
|
111
107
|
|
|
112
|
-
fail(
|
|
108
|
+
fail(
|
|
109
|
+
"Could not find agent-config package. Install via "
|
|
110
|
+
"`npx @event4u/create-agent-config init` or `npm install --save-dev @event4u/agent-config`."
|
|
111
|
+
)
|
|
113
112
|
return project_root # unreachable
|
|
114
113
|
|
|
115
114
|
|
|
116
115
|
def detect_package_type(package_root: Path) -> str:
|
|
117
|
-
|
|
118
|
-
if "vendor" in parts:
|
|
119
|
-
return "composer"
|
|
120
|
-
if "node_modules" in parts:
|
|
116
|
+
if "node_modules" in package_root.parts:
|
|
121
117
|
return "npm"
|
|
122
118
|
return "local"
|
|
123
119
|
|
|
124
120
|
|
|
125
121
|
def detect_package_type_for_project(project_root: Path, package_root: Path) -> str:
|
|
126
122
|
"""Determine package type based on where the package lives relative to the project."""
|
|
127
|
-
composer_path = (project_root / "vendor" / "event4u" / "agent-config").resolve()
|
|
128
123
|
npm_path = (project_root / "node_modules" / "@event4u" / "agent-config").resolve()
|
|
129
124
|
package_resolved = package_root.resolve()
|
|
130
125
|
|
|
131
|
-
if package_resolved ==
|
|
132
|
-
|
|
133
|
-
return "composer"
|
|
134
|
-
if package_resolved == npm_path or npm_path.exists():
|
|
135
|
-
if package_resolved == npm_path:
|
|
136
|
-
return "npm"
|
|
126
|
+
if package_resolved == npm_path:
|
|
127
|
+
return "npm"
|
|
137
128
|
return detect_package_type(package_root)
|
|
138
129
|
|
|
139
130
|
|
|
@@ -446,7 +437,6 @@ def ensure_agent_settings(project_root: Path, package_root: Path, profile: str,
|
|
|
446
437
|
|
|
447
438
|
def ensure_vscode_bridge(project_root: Path, package_type: str, force: bool) -> None:
|
|
448
439
|
plugin_paths = {
|
|
449
|
-
"composer": "./vendor/event4u/agent-config/plugin/agent-config",
|
|
450
440
|
"npm": "./node_modules/@event4u/agent-config/plugin/agent-config",
|
|
451
441
|
}
|
|
452
442
|
plugin_path = plugin_paths.get(package_type, "./plugin/agent-config")
|
|
@@ -1116,6 +1106,326 @@ def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
|
|
|
1116
1106
|
success(".github/plugin/marketplace.json created")
|
|
1117
1107
|
|
|
1118
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
|
+
|
|
1119
1429
|
# --- Post-install smoke test ---
|
|
1120
1430
|
|
|
1121
1431
|
# (platform, native event used for the dry-fire). Probe events are
|
|
@@ -1217,16 +1527,547 @@ def _smoke_test_hooks(project_root: Path, package_root: Path) -> int:
|
|
|
1217
1527
|
return 1 if failed else 0
|
|
1218
1528
|
|
|
1219
1529
|
|
|
1220
|
-
# --- Global user-level install —
|
|
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.
|
|
1221
1567
|
#
|
|
1222
|
-
#
|
|
1223
|
-
#
|
|
1224
|
-
#
|
|
1225
|
-
#
|
|
1226
|
-
#
|
|
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
|
|
1227
2049
|
|
|
1228
2050
|
# --- Argument parsing ---
|
|
1229
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
|
+
|
|
1230
2071
|
def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
1231
2072
|
parser = argparse.ArgumentParser(
|
|
1232
2073
|
prog="install.py",
|
|
@@ -1270,19 +2111,64 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
1270
2111
|
parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
|
|
1271
2112
|
parser.add_argument(
|
|
1272
2113
|
"--tools",
|
|
1273
|
-
default=
|
|
2114
|
+
default=None,
|
|
1274
2115
|
help=(
|
|
1275
2116
|
"comma-separated tool IDs to install bridges for "
|
|
1276
|
-
"(claude-code,cursor,windsurf,cline,gemini-cli,
|
|
2117
|
+
"(claude-code,claude-desktop,cursor,windsurf,cline,gemini-cli,"
|
|
2118
|
+
"copilot,augment,aider,codex,roocode,continue,kilocode,zed,"
|
|
2119
|
+
"jetbrains,kiro,all). "
|
|
1277
2120
|
"Default: all (backward-compatible)."
|
|
1278
2121
|
),
|
|
1279
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
|
+
)
|
|
1280
2131
|
parser.add_argument(
|
|
1281
2132
|
"--no-smoke",
|
|
1282
2133
|
action="store_true",
|
|
1283
2134
|
help="skip the post-install hook smoke test (default: dry-fire dispatch:hook against every installed bridge)",
|
|
1284
2135
|
)
|
|
1285
|
-
|
|
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
|
|
1286
2172
|
|
|
1287
2173
|
|
|
1288
2174
|
# Mapping of --tools IDs accepted by install.py. Mirrors VALID_TOOLS in
|
|
@@ -1290,7 +2176,8 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
1290
2176
|
# bridges (vscode, augment) always run.
|
|
1291
2177
|
_VALID_TOOLS = {
|
|
1292
2178
|
"claude-code", "claude-desktop", "cursor", "windsurf", "cline",
|
|
1293
|
-
"gemini-cli", "copilot", "augment", "aider", "codex", "
|
|
2179
|
+
"gemini-cli", "copilot", "augment", "aider", "codex", "roocode",
|
|
2180
|
+
"continue", "kilocode", "zed", "jetbrains", "kiro", "all",
|
|
1294
2181
|
}
|
|
1295
2182
|
|
|
1296
2183
|
|
|
@@ -1313,6 +2200,18 @@ def _parse_tools(raw: str) -> set[str]:
|
|
|
1313
2200
|
return set(items)
|
|
1314
2201
|
|
|
1315
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
|
+
|
|
1316
2215
|
def _is_tool_enabled(tools: set[str], tool_id: str) -> bool:
|
|
1317
2216
|
return tool_id in tools
|
|
1318
2217
|
|
|
@@ -1328,7 +2227,30 @@ def main(argv: list[str]) -> int:
|
|
|
1328
2227
|
if opts.profile not in SUPPORTED_PROFILES:
|
|
1329
2228
|
fail(f"Unsupported profile: {opts.profile}. Supported: {', '.join(SUPPORTED_PROFILES)}")
|
|
1330
2229
|
|
|
1331
|
-
|
|
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()
|
|
1332
2254
|
is_first_run = not (project_root / SETTINGS_FILE).exists()
|
|
1333
2255
|
|
|
1334
2256
|
if opts.package:
|
|
@@ -1351,7 +2273,7 @@ def main(argv: list[str]) -> int:
|
|
|
1351
2273
|
|
|
1352
2274
|
ensure_agent_settings(project_root, package_root, opts.profile, opts.force)
|
|
1353
2275
|
|
|
1354
|
-
tools =
|
|
2276
|
+
tools = parsed_tools
|
|
1355
2277
|
|
|
1356
2278
|
if not opts.skip_bridges:
|
|
1357
2279
|
# Substrate bridges (always written; other tools symlink/depend on them).
|
|
@@ -1370,6 +2292,24 @@ def main(argv: list[str]) -> int:
|
|
|
1370
2292
|
ensure_gemini_bridge(project_root, opts.force)
|
|
1371
2293
|
if _is_tool_enabled(tools, "copilot"):
|
|
1372
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)
|
|
1373
2313
|
|
|
1374
2314
|
if opts.augment_user_hooks:
|
|
1375
2315
|
ensure_augment_user_hooks(package_root, opts.force)
|
|
@@ -1392,6 +2332,17 @@ def main(argv: list[str]) -> int:
|
|
|
1392
2332
|
info("Smoke-testing installed hook bridges (dry-run)")
|
|
1393
2333
|
_smoke_test_hooks(project_root, package_root)
|
|
1394
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
|
+
|
|
1395
2346
|
if not QUIET:
|
|
1396
2347
|
print()
|
|
1397
2348
|
success("Done.")
|
|
@@ -1404,7 +2355,7 @@ def main(argv: list[str]) -> int:
|
|
|
1404
2355
|
print()
|
|
1405
2356
|
print(" Next steps:")
|
|
1406
2357
|
print(" • Commit .agent-settings.yml and bridge files to your repo")
|
|
1407
|
-
print(" • New team members
|
|
2358
|
+
print(" • New team members run `npx @event4u/create-agent-config init` — done")
|
|
1408
2359
|
print(" • Inspect hook coverage: ./agent-config hooks:status")
|
|
1409
2360
|
print(" • Full walkthrough: https://github.com/event4u-app/agent-config/blob/main/docs/getting-started.md")
|
|
1410
2361
|
print()
|