@event4u/agent-config 2.1.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.
@@ -0,0 +1,160 @@
1
+ ---
2
+ adr: 008
3
+ status: proposed
4
+ date: 2026-05-12
5
+ decision: committed-installed-tools-manifest-separate-from-settings
6
+ supersedes: —
7
+ superseded_by: —
8
+ phase: v2.x · post-global-first-install
9
+ ---
10
+
11
+ # ADR-008 — Installed-Tools Manifest
12
+
13
+ ## Status
14
+
15
+ **Proposed** · 2026-05-12 · pending implementation in Phase 3 of
16
+ [`road-to-global-first-install`](../../agents/roadmaps/road-to-global-first-install.md).
17
+
18
+ Originates from user ask (Matze, 2026-05-12): "Sollten wir auf
19
+ Projektebene festhalten, welche Agents wir initialisiert haben, damit
20
+ bei jedem Sync das Verzeichnis aktualisiert werden kann?" Validated
21
+ through AI Council Round 1 (claude-sonnet-4-5 + gpt-4o, $0.0298 actual,
22
+ both converged on "yes, separate file"). Council session:
23
+ [`agents/council-sessions/2026-05-12-project-settings-and-v1-v2/`](../../agents/council-sessions/2026-05-12-project-settings-and-v1-v2/). <!-- council-ref-allowed: ADR decision trace -->
24
+
25
+ ## Context
26
+
27
+ After ADR-007 (global-first install), each developer's AI tooling
28
+ lives in user-scope paths (`~/.claude/`, `~/.augment/`, …). A project
29
+ no longer carries the AI config in its tree — except for tools with
30
+ `workspace > global` precedence (Windsurf, Cline, Gemini-when-project-
31
+ wins) that **must** keep a project-local bridge.
32
+
33
+ **Resulting gap:** a project has no committed record of which AI
34
+ tools it expects. A new team member cloning the repo cannot tell
35
+ whether the codebase was built with Claude Code, Windsurf, both, or
36
+ five others. Onboarding is "ask the team lead, hope they remember".
37
+
38
+ **Two related but orthogonal problems:**
39
+
40
+ 1. **Bill of materials** — "Which AIs does this project use?"
41
+ 2. **Settings hierarchy** — "How do agents behave in this project?"
42
+
43
+ Today, `.agent-project-settings.yml` (committed) answers #2 (personas,
44
+ quality tools, locked keys). #1 is unanswered.
45
+
46
+ ### What we considered
47
+
48
+ | Option | Verdict |
49
+ |---|---|
50
+ | **A.** Add `installed_tools` block to `.agent-project-settings.yml` | **Rejected** — mixes behaviour with bill-of-materials, creates a "god file" that every sync command must parse and partially ignore. Settings ≠ manifest. |
51
+ | **B.** Put manifest at root: `.agent-installed-tools.lock` | Rejected — root is already crowded with 10+ AI dotfiles; adding another worsens it. |
52
+ | **C.** Separate manifest at `agents/installed-tools.lock` | **Accepted** — co-located with project-shared agent docs; clear name; clear job. |
53
+ | **D.** Skip — let team docs / README describe the tool set | Rejected — README drifts, no machine-readable contract, no drift detection. |
54
+
55
+ Council (Sonnet): _"Settings (user prefs, locked keys, override paths)
56
+ ≠ Manifest (which tools exist). Mixing them creates a god file."_ Both
57
+ members converged on a separate file; the location split (Sonnet
58
+ favoured `installed-tools.lock`, GPT-4o favoured
59
+ `.project-settings.yml`) resolved in favour of Sonnet on the
60
+ separation-of-concerns argument.
61
+
62
+ ## Decision
63
+
64
+ **Adopt option C.** Ship `agents/installed-tools.lock` as the
65
+ committed, schema-versioned bill-of-materials for AI tooling.
66
+
67
+ ### Schema (v1)
68
+
69
+ ```yaml
70
+ schema_version: 1
71
+ agent_config_version: "2.x.y" # version that wrote the file last
72
+ tools:
73
+ - name: claude-code # matches scripts/install.py _VALID_TOOLS
74
+ scope: global # one of: global, project
75
+ bridge_marker: ~/.claude/PROJECT_MANAGED_BY_AGENT_CONFIG
76
+ installed_at: "2026-05-12"
77
+ - name: windsurf
78
+ scope: project # workspace > global → must live in repo
79
+ bridge_marker: .windsurf/PROJECT_MANAGED_BY_AGENT_CONFIG
80
+ installed_at: "2026-05-12"
81
+ ```
82
+
83
+ **Fields:**
84
+
85
+ - `schema_version` — integer; bump on breaking schema changes.
86
+ - `agent_config_version` — last package version that wrote the file.
87
+ - `tools[]` — append-on-init order; not alphabetised (preserves
88
+ installation history for forensics).
89
+ - `tools[].name` — must match `_VALID_TOOLS` in `scripts/install.py`.
90
+ - `tools[].scope` — `global` (user-home install) or `project`
91
+ (workspace-wins tools that need a local bridge).
92
+ - `tools[].bridge_marker` — path to the marker file the installer
93
+ drops to claim ownership. `validate` checks this file exists.
94
+ - `tools[].installed_at` — ISO date; informational only.
95
+
96
+ ### Lifecycle
97
+
98
+ 1. `init --ai <name>` — adds an entry (idempotent). Existing entry
99
+ for same tool with **same scope** is a no-op. Existing entry with
100
+ **different scope** refuses without `--force` (loud warning:
101
+ "tool X is committed as scope=global; you are about to change it
102
+ to project").
103
+ 2. `sync` — reads the lock file, replays every listed tool's install
104
+ (skip if marker present, install if missing). Used by new team
105
+ members.
106
+ 3. `validate` — read-only drift check. Exit 1 if any listed marker
107
+ is missing or scope mismatches the file system. **No auto-fix.**
108
+ 4. Manual edit — discouraged. Lock file is machine-managed; humans
109
+ edit via CLI subcommands.
110
+
111
+ ### Relationship to `.agent-project-settings.yml`
112
+
113
+ | File | Owner | Scope | Example keys |
114
+ |---|---|---|---|
115
+ | `agents/installed-tools.lock` | this ADR | bill of materials | `tools[]`, `scope`, `bridge_marker` |
116
+ | `.agent-project-settings.yml` | layered-settings system | behaviour | `personas.default`, `quality.php.tools`, `locked_keys` |
117
+
118
+ Both committed, both have a single job, never overlap.
119
+
120
+ ## Consequences
121
+
122
+ ### Positive
123
+
124
+ - Onboarding: `git clone` + `npx @event4u/agent-config sync` brings
125
+ every team member's AI tooling to parity.
126
+ - Drift detection: `validate` catches "team lead added Windsurf but
127
+ forgot to commit the lock-file update".
128
+ - Forensics: install order preserved in `tools[]` order; `installed_at`
129
+ pins approximate timestamps.
130
+ - Separation of concerns: behaviour settings stay clean; manifest
131
+ stays focused.
132
+
133
+ ### Negative
134
+
135
+ - New committed file = one more thing to keep in sync with reality.
136
+ Mitigated by **machine-written-only** rule and `validate` CI hook.
137
+ - Scope migration (tool moves between `global` and `project`) needs
138
+ documented playbook in `installed-tools-manifest.md` (Phase 3.5).
139
+ - Single-developer projects gain little — the manifest is overhead
140
+ until a second developer joins. Mitigation: file is optional;
141
+ commands work without it (empty manifest = empty install).
142
+
143
+ ### Neutral
144
+
145
+ - File lives under `agents/`, not at repo root — consistent with
146
+ Matze's preference and council Sonnet's argument that root is
147
+ already crowded.
148
+
149
+ ## Implementation Plan
150
+
151
+ Tracked as Phase 3 of `road-to-global-first-install` (steps 3.1–3.5).
152
+ Ships in a v2.x minor release **after** Phase 2 lands. Out of scope
153
+ for this ADR.
154
+
155
+ ## References
156
+
157
+ - [`ADR-007`](ADR-007-agent-discovery-scopes.md) — global-first install (this ADR depends on it).
158
+ - [`agents/roadmaps/road-to-global-first-install.md`](../../agents/roadmaps/road-to-global-first-install.md) Phase 3.
159
+ - [`agents/council-sessions/2026-05-12-project-settings-and-v1-v2/`](../../agents/council-sessions/2026-05-12-project-settings-and-v1-v2/) — full council transcripts. <!-- council-ref-allowed: ADR decision trace -->
160
+ - [`docs/guidelines/agent-infra/layered-settings.md`](../guidelines/agent-infra/layered-settings.md) — the existing 4-layer settings precedence; this ADR adds a parallel file outside that hierarchy.
@@ -10,6 +10,8 @@ _Auto-generated by `scripts/adr/regenerate_index.py`. Do not edit._
10
10
  | [ADR-004](ADR-004-rule-governance-pruning.md) | Rule Governance Pruning | accepted | 2026-05-08 | — |
11
11
  | [ADR-005](ADR-005-subagent-worktrees.md) | Subagent Worktrees No Auto Merge | accepted | 2026-05-09 | — |
12
12
  | [ADR-006](ADR-006-skill-tools-python-pilot.md) | Skill Tools Python Pilot Pass | accepted | 2026-05-09 | — |
13
+ | [ADR-007](ADR-007-agent-discovery-scopes.md) | Global Default Install With Export Subcommand | accepted | 2026-05-12 | — |
14
+ | [ADR-008](ADR-008-installed-tools-manifest.md) | Committed Installed Tools Manifest Separate From Settings | proposed | 2026-05-12 | — |
13
15
 
14
16
  ## Unnumbered (legacy)
15
17
 
@@ -106,3 +106,35 @@ correction pattern.
106
106
 
107
107
  Acknowledge once, in the user's language, switch behavior, no
108
108
  excuses (mirrors `language-and-tone` § slip handling).
109
+
110
+
111
+ ## No Cheap Questions — Iron Law 3 detail (paternalistic state options)
112
+
113
+ Companion to `no-cheap-questions` § Iron Law 3. The rule states the
114
+ prohibition; this file lists the patterns and the carve-outs.
115
+
116
+ **Forbidden patterns** (non-exhaustive):
117
+
118
+ - "Stop hier — du hast genug für heute"
119
+ - "Take a break and come back fresh"
120
+ - "Weitermachen wenn frisch"
121
+ - "Du wirkst genervt, sollen wir pausieren?"
122
+ - "Sleep on it"
123
+ - "That's a good stopping point" as a numbered option
124
+ - Any option whose recommendation rests on inferred fatigue,
125
+ frustration, or end-of-day mood.
126
+
127
+ **Carve-outs** — allowed because they cite **observable, in-message**
128
+ evidence, not inferred state:
129
+
130
+ - User said "ich bin müde / done for today / let's stop" **this turn**
131
+ → ack and stop (instruction, not option).
132
+ - Hard Floor confirmation per `non-destructive-by-default` → "confirm
133
+ or abort" is the option, not "rest".
134
+ - Context-window / freshness threshold tripped per `context-hygiene` →
135
+ cite the threshold ("fresh chat at 75%"), do not infer mood.
136
+
137
+ **The rule of thumb**: every numbered option must be a technical /
138
+ scope / sequencing choice with a real trade-off, not a mood-management
139
+ nudge. If the only remaining differentiator is "you might be tired" →
140
+ drop the option, recommend a concrete next step instead.
@@ -0,0 +1,135 @@
1
+ # Installed-Tools Manifest
2
+
3
+ Project-committed bill of materials for AI tooling. Answers the
4
+ question "which AIs does this project use, where do their bridges live,
5
+ and is everyone on the team on the same set?". Canonical schema is
6
+ ADR-008 ([`docs/decisions/ADR-008-installed-tools-manifest.md`](../../decisions/ADR-008-installed-tools-manifest.md)).
7
+ Phase 3 of [`road-to-global-first-install`](../../../agents/roadmaps/road-to-global-first-install.md).
8
+
9
+ This file lives at **`agents/installed-tools.lock`** — committed,
10
+ machine-managed, and orthogonal to `.agent-project-settings.yml`
11
+ (which owns *behaviour*, not *bill of materials*).
12
+
13
+ ## Schema (v1)
14
+
15
+ ```yaml
16
+ schema_version: 1
17
+ agent_config_version: "2.x.y" # last package version that wrote the file
18
+ tools:
19
+ - name: claude-code # must match _VALID_TOOLS in scripts/install.py
20
+ scope: global # one of: global, project
21
+ bridge_marker: ~/.claude/ # validate checks this path exists
22
+ installed_at: "2026-05-12"
23
+ - name: roocode
24
+ scope: project # workspace-wins → must live in repo
25
+ bridge_marker: .roo/rules/agent-config.md
26
+ installed_at: "2026-05-12"
27
+ ```
28
+
29
+ | Field | Owner | Notes |
30
+ |---|---|---|
31
+ | `schema_version` | machine | bumps on breaking schema changes |
32
+ | `agent_config_version` | machine | last writer's package version; `validate` flags drift |
33
+ | `tools[]` | machine | append-on-init order preserved (not alphabetised) |
34
+ | `tools[].name` | machine | one of the 17 valid IDs in `scripts/install.py` |
35
+ | `tools[].scope` | machine | `global` (user-home) or `project` (workspace bridge) |
36
+ | `tools[].bridge_marker` | machine | absolute / `~`-prefixed for global, repo-relative for project |
37
+ | `tools[].installed_at` | machine | ISO date; informational only |
38
+
39
+ The file is **machine-managed**. Hand-editing is discouraged — every
40
+ mutation goes through `init`, `sync`, or `init --force`.
41
+
42
+ ## Workflow
43
+
44
+ ### Team onboarding (clone → sync → done)
45
+
46
+ ```bash
47
+ git clone <repo>
48
+ cd <repo>
49
+ npx @event4u/agent-config sync
50
+ ```
51
+
52
+ `sync` reads `agents/installed-tools.lock`, checks every listed tool's
53
+ bridge marker, and replays `install.py --tools=<id>` for each missing
54
+ one. Tools whose marker is already present are skipped — `sync` is
55
+ idempotent and safe to re-run.
56
+
57
+ ### Adding a tool
58
+
59
+ ```bash
60
+ npx @event4u/agent-config init --tools=<id>
61
+ # or --tools=<id1>,<id2>
62
+ ```
63
+
64
+ `init` writes an entry per tool. Existing entry with the same
65
+ `(name, scope)` → no-op. Entry with **different scope** → loud warning
66
+ and refusal until you pass `--force` (see scope migration below).
67
+
68
+ ### Drift detection (CI gate)
69
+
70
+ ```bash
71
+ npx @event4u/agent-config validate
72
+ ```
73
+
74
+ Read-only. Exit code 1 if any drift is found. Surfaces three drift
75
+ kinds; no auto-fix.
76
+
77
+ | Kind | Trigger | Fix |
78
+ |---|---|---|
79
+ | `marker_missing` | recorded `bridge_marker` does not exist | `agent-config sync` |
80
+ | `scope_divergence` | marker only exists at the *other* scope | `agent-config init --tools=<id> --force` |
81
+ | `version_drift` | manifest's `agent_config_version` ≠ installed package | `agent-config update` then `agent-config init --force` |
82
+
83
+ `--skip-version-check` suppresses the third kind for repositories that
84
+ intentionally pin an older version of the manifest.
85
+
86
+ ## Scope migration
87
+
88
+ Moving a tool between `project` and `global` is supported but loud:
89
+
90
+ 1. Run `init --tools=<id> --scope=<new> --force`. The installer detects
91
+ the conflict, warns, and rewrites the entry only when `--force` is
92
+ present.
93
+ 2. The old bridge is **not** removed automatically — clean up the
94
+ leftover marker yourself (`rm .windsurf/agent-config.md` etc.).
95
+ 3. `validate` afterwards confirms the new state.
96
+
97
+ Reasoning: scope is a project-wide decision; flipping it silently
98
+ would surprise other team members who never asked for the change. The
99
+ loud refusal forces an explicit `--force` so the diff is reviewable in
100
+ the next commit.
101
+
102
+ ## Relationship to other files
103
+
104
+ | File | What it answers | Layer |
105
+ |---|---|---|
106
+ | `agents/installed-tools.lock` | **which AIs?** (this guideline) | bill of materials |
107
+ | `.agent-project-settings.yml` | **how do agents behave?** | layered-settings (team file) |
108
+ | `~/.config/agent-config/installed.lock` | **which package version did I install globally?** | per-developer global lockfile (Phase 1) |
109
+ | `.agent-settings.yml` | **what are my personal preferences in this project?** | layered-settings (developer file) |
110
+
111
+ Each file has one job. They never overlap. The two `.lock` files look
112
+ similar by name but answer different questions: `installed.lock` is
113
+ per-developer / cross-project (the package itself), while
114
+ `installed-tools.lock` is per-project / team-shared (which tools are
115
+ expected in *this* repo).
116
+
117
+ ## CI integration
118
+
119
+ Recommended gate (GitHub Actions / GitLab CI):
120
+
121
+ ```yaml
122
+ - name: Validate installed-tools manifest
123
+ run: npx @event4u/agent-config validate
124
+ ```
125
+
126
+ Pair it with `agent-config sync` in your dev-setup script so new
127
+ contributors get a working environment without reading the manifest by
128
+ hand.
129
+
130
+ ## References
131
+
132
+ - [`ADR-008`](../../decisions/ADR-008-installed-tools-manifest.md) — manifest decision and schema.
133
+ - [`ADR-007`](../../decisions/ADR-007-agent-discovery-scopes.md) — global-first install (prerequisite).
134
+ - [`docs/installation.md`](../../installation.md) — team-onboarding flow.
135
+ - [`layered-settings.md`](layered-settings.md) — parallel settings hierarchy (orthogonal to this manifest).
@@ -1,7 +1,32 @@
1
1
  # Installation
2
2
 
3
- **Principle:** Project-installed by default, plugin-enhanced when available.
4
- No Task, no Make, no build tools required for installation.
3
+ **Principle:** Global-first install (cross-project, in `~/.claude/`,
4
+ `~/.cursor/`, …), opt-in project export when a team wants the config
5
+ committed to a repo. No Task, no Make, no build tools required.
6
+
7
+ > **v2.1+** — the installer detects intent. Running `npx
8
+ > @event4u/create-agent-config init` in `~/` or any directory without a
9
+ > project manifest defaults to **global**. Running it inside a project
10
+ > (`package.json` / `composer.json` / `pyproject.toml` / etc.) defaults
11
+ > to **project**. Pass `--scope=global` or `--scope=project` to override
12
+ > detection. See `--scope` in the CLI help for the full matrix.
13
+
14
+ A global install records itself in `~/.config/agent-config/installed.lock`
15
+ (schema_version, agent_config_version, installed_at, tools[]). `npx
16
+ @event4u/create-agent-config update` keeps that manifest in lockstep
17
+ with the project pin in `.agent-settings.yml`. A version-mismatched
18
+ re-run of `init --scope=global` is refused with exit code 1 until you
19
+ `update` or pass `--force`.
20
+
21
+ To commit a specific tool's config into a project repo, use:
22
+
23
+ ```bash
24
+ agent-config export --tool=<id> --output=<path>
25
+ ```
26
+
27
+ (Idempotent; `--force` overrides drift. `--list` enumerates supported
28
+ tool ids. See [`docs/contracts/command-clusters.md`](contracts/command-clusters.md)
29
+ for the export contract.)
5
30
 
6
31
  ## Per-IDE setup — quick index
7
32
 
@@ -248,6 +273,7 @@ After initial setup, commit these files:
248
273
 
249
274
  ```
250
275
  .agent-settings.yml ← shared profile (e.g., cost_profile: minimal)
276
+ agents/installed-tools.lock ← AI bill of materials (ADR-008, Phase 3)
251
277
  .augment/ ← rules, skills, commands (symlinks)
252
278
  .cursor/rules/ ← Cursor rules (symlinks)
253
279
  .claude/ ← Claude rules, skills (symlinks)
@@ -255,7 +281,37 @@ AGENTS.md ← Copilot/Gemini instructions
255
281
  .github/copilot-instructions.md ← GitHub Copilot instructions
256
282
  ```
257
283
 
258
- New team members: run `composer install` (or `npm install`) open editor → done.
284
+ `agents/installed-tools.lock` lists every AI tool the project expects,
285
+ its scope (`global` or `project`), and its bridge marker path. Written
286
+ by `init`, replayed by `sync`, checked by `validate`. Schema and
287
+ workflow: [`docs/guidelines/agent-infra/installed-tools-manifest.md`](guidelines/agent-infra/installed-tools-manifest.md).
288
+
289
+ ### Team onboarding — clone → sync → done
290
+
291
+ New team members get every AI bridge online with a single command:
292
+
293
+ ```bash
294
+ git clone <repo>
295
+ cd <repo>
296
+ npx @event4u/agent-config sync
297
+ ```
298
+
299
+ `sync` reads `agents/installed-tools.lock` and re-runs the installer
300
+ for every tool whose bridge marker is missing locally. Idempotent —
301
+ re-running after every clone is safe. Tools with markers already in
302
+ place are skipped.
303
+
304
+ Pair it with a CI gate to catch drift in PRs:
305
+
306
+ ```bash
307
+ npx @event4u/agent-config validate
308
+ ```
309
+
310
+ `validate` is read-only. Exit 1 on any of: marker missing, scope
311
+ divergence (manifest says `project` but marker only exists at the
312
+ global anchor, or vice versa), version drift (manifest's
313
+ `agent_config_version` ≠ installed package). Full drift catalog and
314
+ fix table: [`installed-tools-manifest.md § Drift detection`](guidelines/agent-infra/installed-tools-manifest.md#drift-detection-ci-gate).
259
315
 
260
316
  ---
261
317
 
@@ -3,10 +3,14 @@
3
3
  The fastest path to running our skills, rules, and (optionally) the MCP
4
4
  server inside Claude Desktop. macOS / Windows / Linux. ~5 minutes.
5
5
 
6
- > **TL;DR** — run `npx @event4u/agent-config init --tools=claude-code`
7
- > inside each project that should expose the skills/rules to Claude
8
- > Desktop. The package now ships as an npx-resolved runtime; the
9
- > retired `--global` symlink scheme has been removed.
6
+ > **TL;DR** — Claude Desktop reads from `~/.claude/` (global only, no
7
+ > project-local discovery on macOS). Run `npx @event4u/agent-config
8
+ > global --tools=claude-desktop` once per user, or
9
+ > `npx @event4u/agent-config init --tools=claude-code` per project
10
+ > (Claude Code's project install also covers Desktop on macOS via the
11
+ > shared `~/.claude/` location seeded during `init`). The v1 npm /
12
+ > composer install scheme is retired; the new global-first scheme is
13
+ > ADR-007 and writes through `~/.config/agent-config/installed.lock`.
10
14
 
11
15
  ## Prerequisites
12
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -0,0 +1,157 @@
1
+ """``agent-config export`` — eject a tool's canonical content into the project.
2
+
3
+ Phase 1.5 of road-to-global-first-install.md (ADR-007 D3). Replaces the
4
+ rejected symlink-bridge subcommand: writes a real file with the resolved
5
+ content for a named tool into a user-chosen path so it can be committed,
6
+ shared with the team, or customized in place. Idempotent by default;
7
+ ``--force`` overrides content drift. No canonical-path defaults.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import hashlib
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Callable, Optional
16
+
17
+ from scripts.install import (
18
+ AIDER_MARKER,
19
+ CLAUDE_DESKTOP_MARKER,
20
+ CODEX_MARKER,
21
+ CONTINUE_MARKER,
22
+ JETBRAINS_MARKER,
23
+ KILOCODE_MARKER,
24
+ KIRO_MARKER,
25
+ ROOCODE_MARKER,
26
+ ZED_MARKER,
27
+ )
28
+
29
+ PACKAGE_ROOT = Path(__file__).resolve().parents[2]
30
+ TEMPLATES_DIR = PACKAGE_ROOT / ".agent-src" / "templates"
31
+
32
+
33
+ def _from_template(rel: str) -> Callable[[], str]:
34
+ def _read() -> str:
35
+ path = TEMPLATES_DIR / rel
36
+ if not path.is_file():
37
+ raise FileNotFoundError(
38
+ f"template missing from package: {path} "
39
+ f"(reinstall @event4u/agent-config or report a bug)"
40
+ )
41
+ return path.read_text(encoding="utf-8")
42
+ return _read
43
+
44
+
45
+ def _from_constant(value: str) -> Callable[[], str]:
46
+ def _read() -> str:
47
+ return value
48
+ return _read
49
+
50
+
51
+ # tool_id → (description, content_provider).
52
+ EXPORT_REGISTRY: "dict[str, tuple[str, Callable[[], str]]]" = {
53
+ "roocode": ("Roo Code marker (.roo/rules/agent-config.md body)",
54
+ _from_constant(ROOCODE_MARKER)),
55
+ "claude-desktop": ("Claude Desktop marker (informational, global-scope tool)",
56
+ _from_constant(CLAUDE_DESKTOP_MARKER)),
57
+ "aider": ("Aider marker (manual `read:` wiring documented inline)",
58
+ _from_constant(AIDER_MARKER)),
59
+ "codex": ("Codex CLI marker (informational — AGENTS.md is canonical)",
60
+ _from_constant(CODEX_MARKER)),
61
+ "continue": ("Continue.dev marker (.continue/rules/agent-config.md body)",
62
+ _from_constant(CONTINUE_MARKER)),
63
+ "kilocode": ("Kilo Code marker (.kilocode/rules/agent-config.md body)",
64
+ _from_constant(KILOCODE_MARKER)),
65
+ "zed": ("Zed marker (informational — .rules at repo root is canonical)",
66
+ _from_constant(ZED_MARKER)),
67
+ "jetbrains": ("JetBrains AI Assistant marker (.jetbrains/agent-config.md body)",
68
+ _from_constant(JETBRAINS_MARKER)),
69
+ "kiro": ("Kiro marker (.kiro/steering/agent-config.md body)",
70
+ _from_constant(KIRO_MARKER)),
71
+ "agents-md": ("AGENTS.md template (Thin-Root entry point — consumer scaffold)",
72
+ _from_template("AGENTS.md")),
73
+ "copilot-instructions": ("GitHub Copilot Code Review instructions template",
74
+ _from_template("copilot-instructions.md")),
75
+ }
76
+
77
+
78
+ def _list_tools(out) -> int:
79
+ print("Available tools for `agent-config export --tool <id>`:", file=out)
80
+ width = max(len(t) for t in EXPORT_REGISTRY) + 2
81
+ for tool_id, (desc, _) in sorted(EXPORT_REGISTRY.items()):
82
+ print(f" {tool_id:<{width}}{desc}", file=out)
83
+ return 0
84
+
85
+
86
+ def _hash(content: str) -> str:
87
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
88
+
89
+
90
+ def _rel(path: Path) -> Path:
91
+ try:
92
+ return path.relative_to(Path.cwd())
93
+ except ValueError:
94
+ return path
95
+
96
+
97
+ def _write(output: Path, content: str, *, force: bool, out, err) -> int:
98
+ if output.exists():
99
+ existing = output.read_text(encoding="utf-8")
100
+ if _hash(existing) == _hash(content):
101
+ print(f"ℹ️ {_rel(output)} already exported (content matches).", file=out)
102
+ return 0
103
+ if not force:
104
+ print(
105
+ f"❌ refusing to overwrite {output} — content differs. "
106
+ f"Pass --force to replace.",
107
+ file=err,
108
+ )
109
+ return 1
110
+ output.parent.mkdir(parents=True, exist_ok=True)
111
+ output.write_text(content, encoding="utf-8")
112
+ print(f"✅ exported to {_rel(output)}", file=out)
113
+ return 0
114
+
115
+
116
+ def main(argv: Optional[list[str]] = None, *, out=sys.stdout, err=sys.stderr) -> int:
117
+ parser = argparse.ArgumentParser(
118
+ prog="agent-config export",
119
+ description="Eject a tool's resolved content into a user-chosen path.",
120
+ )
121
+ parser.add_argument("--tool", metavar="ID",
122
+ help="Tool to export (see --list for the catalog).")
123
+ parser.add_argument("--output", metavar="PATH",
124
+ help="Destination path (relative to CWD).")
125
+ parser.add_argument("--force", action="store_true",
126
+ help="Overwrite an existing file with non-matching content.")
127
+ parser.add_argument("--list", action="store_true",
128
+ help="Print supported tool IDs with descriptions and exit.")
129
+ args = parser.parse_args(argv)
130
+
131
+ if args.list:
132
+ return _list_tools(out)
133
+ if not args.tool:
134
+ print("❌ --tool is required (see --list for the catalog).", file=err)
135
+ return 2
136
+ if not args.output:
137
+ print("❌ --output is required (no canonical-path defaults).", file=err)
138
+ return 2
139
+
140
+ entry = EXPORT_REGISTRY.get(args.tool)
141
+ if entry is None:
142
+ print(f"❌ unknown tool: {args.tool} (see --list)", file=err)
143
+ return 2
144
+
145
+ _, provider = entry
146
+ try:
147
+ content = provider()
148
+ except FileNotFoundError as exc:
149
+ print(f"❌ {exc}", file=err)
150
+ return 1
151
+
152
+ output = Path(args.output).expanduser().resolve()
153
+ return _write(output, content, force=args.force, out=out, err=err)
154
+
155
+
156
+ if __name__ == "__main__": # pragma: no cover
157
+ sys.exit(main())