@event4u/agent-config 2.2.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.agent-src/commands/onboard.md +14 -9
  2. package/.agent-src/rules/external-reference-deep-dive.md +69 -0
  3. package/.agent-src/skills/ai-council/SKILL.md +5 -3
  4. package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
  5. package/.agent-src/templates/agents/agent-project-settings.example.yml +4 -3
  6. package/.agent-src/templates/copilot-instructions.md +7 -0
  7. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +29 -7
  8. package/.agent-src/templates/scripts/work_engine/_lib/user_global_paths.py +249 -0
  9. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +8 -5
  10. package/.claude-plugin/marketplace.json +27 -1
  11. package/CHANGELOG.md +79 -0
  12. package/README.md +1 -8
  13. package/config/agent-settings.template.yml +5 -3
  14. package/docs/architecture.md +1 -1
  15. package/docs/catalog.md +5 -3
  16. package/docs/contracts/installed-tools-lockfile.md +142 -0
  17. package/docs/customization.md +23 -17
  18. package/docs/decisions/ADR-007-agent-discovery-scopes.md +6 -0
  19. package/docs/decisions/ADR-009-event4u-namespace.md +188 -0
  20. package/docs/decisions/INDEX.md +1 -0
  21. package/docs/development.md +37 -0
  22. package/docs/getting-started.md +1 -1
  23. package/docs/guidelines/agent-infra/installed-tools-manifest.md +1 -1
  24. package/docs/guidelines/agent-infra/layered-settings.md +6 -4
  25. package/docs/installation.md +17 -2
  26. package/docs/migration/v1-to-v2.md +45 -0
  27. package/docs/setup/per-ide/antigravity.md +63 -0
  28. package/docs/setup/per-ide/augment.md +77 -0
  29. package/docs/setup/per-ide/claude-desktop.md +107 -65
  30. package/docs/setup/per-ide/codebuddy.md +63 -0
  31. package/docs/setup/per-ide/continue.md +68 -0
  32. package/docs/setup/per-ide/droid.md +65 -0
  33. package/docs/setup/per-ide/jetbrains.md +76 -0
  34. package/docs/setup/per-ide/kilocode.md +66 -0
  35. package/docs/setup/per-ide/kiro.md +72 -0
  36. package/docs/setup/per-ide/opencode.md +62 -0
  37. package/docs/setup/per-ide/qoder.md +63 -0
  38. package/docs/setup/per-ide/roocode.md +68 -0
  39. package/docs/setup/per-ide/trae.md +63 -0
  40. package/docs/setup/per-ide/warp.md +63 -0
  41. package/docs/setup/per-ide/zed.md +73 -0
  42. package/package.json +1 -1
  43. package/scripts/_cli/cmd_doctor.py +351 -0
  44. package/scripts/_cli/cmd_prune.py +317 -0
  45. package/scripts/_cli/cmd_uninstall.py +465 -0
  46. package/scripts/_cli/cmd_update.py +30 -4
  47. package/scripts/_cli/cmd_versions.py +147 -0
  48. package/scripts/_lib/agent_settings.py +29 -7
  49. package/scripts/_lib/agents_overlay.py +15 -4
  50. package/scripts/_lib/claude_desktop_bundler.py +150 -0
  51. package/scripts/_lib/fs_atomic.py +116 -0
  52. package/scripts/_lib/installed_lock.py +37 -4
  53. package/scripts/_lib/installed_tools.py +189 -45
  54. package/scripts/_lib/json_pointers.py +260 -0
  55. package/scripts/_lib/update_check.py +29 -5
  56. package/scripts/_lib/user_global_paths.py +249 -0
  57. package/scripts/agent-config +69 -0
  58. package/scripts/ai_council/__init__.py +4 -3
  59. package/scripts/ai_council/budget_guard.py +34 -4
  60. package/scripts/ai_council/bundler.py +2 -0
  61. package/scripts/ai_council/clients.py +28 -7
  62. package/scripts/compress.py +78 -15
  63. package/scripts/install +8 -0
  64. package/scripts/install-hooks.sh +54 -1
  65. package/scripts/install.py +1149 -53
  66. package/scripts/install_anthropic_key.sh +5 -3
  67. package/scripts/install_openai_key.sh +5 -3
  68. package/scripts/skill_trigger_eval.py +13 -2
@@ -40,9 +40,11 @@ One line: one-time setup, six questions, one at a time (iron law from
40
40
 
41
41
  ### 2. Offer user-global cross-project defaults
42
42
 
43
- Detect whether `~/.config/agent-config/agent-settings.yml` exists. Path is
44
- XDG-style, matches existing `~/.config/agent-config/` dir used for
45
- `anthropic.key`, `openai.key`, `council-spend.jsonl`.
43
+ Detect whether `~/.event4u/agent-config/agent-settings.yml` exists (or
44
+ legacy `~/.config/agent-config/agent-settings.yml`, read as fallback by
45
+ every loader). New path namespaces every package-owned user-global
46
+ artefact under one root — same place `anthropic.key`, `openai.key`,
47
+ `council-spend.jsonl` now live.
46
48
 
47
49
  - **File exists** → skip step entirely. Re-onboarding never overwrites
48
50
  user-global file silently.
@@ -54,7 +56,7 @@ XDG-style, matches existing `~/.config/agent-config/` dir used for
54
56
  → empty → first-time setup, ask:
55
57
 
56
58
  ```
57
- > A user-global config at ~/.config/agent-config/agent-settings.yml lets
59
+ > A user-global config at ~/.event4u/agent-config/agent-settings.yml lets
58
60
  > you carry your DX-comfort defaults (name, IDE, autonomy, cost profile,
59
61
  > communication style) across every project that uses event4u/agent-config.
60
62
  >
@@ -177,7 +179,7 @@ Skip unless step 2 captured explicit "yes". Re-confirm intent in one line —
177
179
  never silent-write a file outside project tree:
178
180
 
179
181
  ```
180
- > Writing ~/.config/agent-config/agent-settings.yml with the six
182
+ > Writing ~/.event4u/agent-config/agent-settings.yml with the six
181
183
  > mergeable keys mirrored from this project's choices:
182
184
  >
183
185
  > name: {personal.user_name or ""}
@@ -191,10 +193,12 @@ never silent-write a file outside project tree:
191
193
  > 2. Cancel — keep settings project-local only
192
194
  ```
193
195
 
194
- `1` → ensure `~/.config/agent-config/` exists (`mkdir -p`, mode `0700`),
196
+ `1` → ensure `~/.event4u/agent-config/` exists (`mkdir -p`, mode `0700`;
197
+ migration shim in `scripts/install.py` moves any legacy
198
+ `~/.config/agent-config/` files into new namespace on first run),
195
199
  then write file with mode `0600`. Schema is **flat-or-nested YAML keyed on
196
200
  dotted paths** in whitelist documented in
197
- [`scripts/_lib/agent_settings.py`](../scripts/_lib/agent_settings.py).
201
+ [`scripts/_lib/agent_settings.py`](../../scripts/_lib/agent_settings.py).
198
202
  Use same section-aware merge rules from
199
203
  [`layered-settings`](../docs/guidelines/agent-infra/layered-settings.md#section-aware-merge-rules)
200
204
  **only if file unexpectedly already exists** between step 2 and this step
@@ -263,7 +267,8 @@ Skip this block in cloud surfaces (no settings file, no log path).
263
267
  `ide`).
264
268
  - **User-global file is opt-in, one-shot, never silent.** Step 2 captures
265
269
  intent, step 9 re-confirms before actual write. If
266
- `~/.config/agent-config/agent-settings.yml` already exists when
270
+ `~/.event4u/agent-config/agent-settings.yml` (or legacy
271
+ `~/.config/agent-config/agent-settings.yml`) already exists when
267
272
  `/onboard` starts, step 2 is skipped entirely — re-onboarding never
268
273
  silently rewrites developer's cross-project defaults. Use
269
274
  `/sync-agent-settings` (project-scoped only) or edit file manually for
@@ -282,4 +287,4 @@ cloud agent should proceed without invoking it.
282
287
  - [`set-cost-profile`](set-cost-profile.md) — isolated profile change
283
288
  - [`layered-settings`](../docs/guidelines/agent-infra/layered-settings.md) — merge rules for mid-life edits
284
289
  - [`agent-settings` template](../templates/agent-settings.md) — settings reference
285
- - [`scripts/_lib/agent_settings.py`](../scripts/_lib/agent_settings.py) — centralized loader + whitelist that consumes the user-global file
290
+ - [`scripts/_lib/agent_settings.py`](../../scripts/_lib/agent_settings.py) — centralized loader + whitelist that consumes the user-global file
@@ -0,0 +1,69 @@
1
+ ---
2
+ type: "auto"
3
+ tier: "2b"
4
+ description: "When the user names an external repo, file, URL, or artifact as a reference — fetch the actual tree and inspect, never summarize from README or metadata"
5
+ alwaysApply: false
6
+ source: package
7
+ triggers:
8
+ - intent: "look at how X does it"
9
+ - intent: "compare with reference repo"
10
+ - intent: "use as template / vorlage"
11
+ - intent: "wie macht es X"
12
+ - intent: "vergleiche mit Y"
13
+ - intent: "schau dir Z an"
14
+ - intent: "study this competitor"
15
+ - keyword: "github.com/"
16
+ - keyword: "source of truth"
17
+ - phrase: "reference repo"
18
+ ---
19
+
20
+ # external-reference-deep-dive
21
+
22
+ ## The Iron Law
23
+
24
+ ```
25
+ EXTERNAL REFERENCE NAMED → DEEP-DIVE FIRST.
26
+ NO README-ONLY SUMMARIES. NO METADATA GUESSES.
27
+ INSPECT THE TREE, THE FRONTMATTER, THE CONFIGS, THE CODE.
28
+ ABSENCE-OF-EVIDENCE IS NOT EVIDENCE — FETCH BEFORE CLAIMING.
29
+ ```
30
+
31
+ Triggered when the user points to an external artifact (repo URL, `owner/repo`, file path, website, archive) **and** asks to analyze, compare, mirror, use as template, or "see how they do it". The user invested in naming the source — answer from the source, not from the cover.
32
+
33
+ ## Mandatory before drawing any conclusion
34
+
35
+ 1. **Use `/analyze-reference-repo`** (the canonical flow) when the artifact is a repo. Its Steps 2–5 are not optional shortcuts — they are the deep-dive contract: fetch listings (not just README), inspect key directories (`skills/`, `rules/`, `commands/`, `scripts/`, `.github/workflows/`, install configs, frontmatter samples), classify each axis adopt/adapt/reject/already.
36
+ 2. **For single files / websites** — fetch the actual content, not a summary. PDFs/DOCX/XLSX → `markitdown` first.
37
+ 3. **Cite verbatim** — every finding lands with a file path, line range, or URL fragment. "The README says X" is **not** a finding about implementation; it's a finding about marketing copy.
38
+ 4. **Surface what was inspected** — list the files / directories actually fetched, so the user can audit coverage.
39
+ 5. **"Not found" beats "probably not there"** — if a fetch budget was hit, say which subtrees are still un-inspected and ask before concluding absence.
40
+
41
+ ## Forbidden patterns
42
+
43
+ - Reading only the README / homepage / package description and reporting capabilities.
44
+ - Declaring "they don't support X" without listing the directories actually inspected.
45
+ - Repeating a surface-level claim across turns without re-fetching when the user pushes back.
46
+ - Treating `package.json` / `composer.json` keywords as evidence of behavior.
47
+ - Summarizing a competitor's installer story from prose instead of reading `scripts/install*`, `bin/*`, or the npm `bin` field.
48
+
49
+ ## Failure mode catalog
50
+
51
+ | Pattern | Why it fails | Fix |
52
+ |---|---|---|
53
+ | **README-summary-as-analysis** | Marketing prose ≠ implementation. Capabilities listed there may be aspirational, deprecated, or differently scoped. | Fetch the directory listing + 1–2 representative files per axis. |
54
+ | **"Nothing matches" without enumeration** | Claims absence without proof. The user has to re-prompt to force the real lookup. | Before saying "no X", list the directories scanned. If the budget caps, ask which to expand. |
55
+ | **Repeat-the-guess after pushback** | User says "really look" → agent re-paraphrases the same surface read. Burns trust. | On any pushback that names the source again, restart from a tree listing, not from prior notes. |
56
+ | **Cross-tool path inference from a single example** | One config file ≠ a convention. | Inspect ≥2 unrelated tool integrations before claiming a pattern is the project's universal anchor strategy. |
57
+
58
+ **Case-zero anchor.** May 2026 — `nextlevelbuilder/ui-ux-pro-max-skill` was named as the source of truth for 23 AI-tool anchors. Three rounds of "we have no content for these tools" were reported from README inference. The actual `tool-configs/*.json` files in the reference repo gave the exact directory layout per tool in one fetch. Cost: ~2 hours of user frustration. Lesson: when an external source is named, the first action is `GET /repos/{o}/{r}/contents/{interesting-subtree}` — not paraphrase.
59
+
60
+ ## Escape hatch
61
+
62
+ User explicitly fences the scope (*"quick scan only"*, *"just glance at the README"*, *"don't fetch the whole thing"*) → quick-scan path is allowed. Say so up front: *"Quick-scan mode per your scope — README + top-level layout only, not a full analysis."*
63
+
64
+ ## See also
65
+
66
+ - Command [`/analyze-reference-repo`](../commands/analyze-reference-repo.md) — canonical deep-dive flow.
67
+ - Rule [`think-before-action`](think-before-action.md) — sibling Iron Law for code paths in **this** repo; this rule is its mirror for **external** artifacts.
68
+ - Rule [`ask-when-uncertain`](ask-when-uncertain.md) — when a fetch budget caps, ask which subtree to expand instead of guessing.
69
+ - Skill [`markitdown`](../skills/markitdown/SKILL.md) — convert binary office formats before analysis.
@@ -89,7 +89,7 @@ travel changes.
89
89
 
90
90
  | Mode | Client | Billable | Transport | Status |
91
91
  |---|---|---|---|---|
92
- | `api` | `AnthropicClient` / `OpenAIClient` | yes | provider SDK + key from `~/.config/agent-config/<provider>.key` | shipped |
92
+ | `api` | `AnthropicClient` / `OpenAIClient` | yes | provider SDK + key from `~/.event4u/agent-config/<provider>.key` (legacy `~/.config/agent-config/<provider>.key` read as fallback) | shipped |
93
93
  | `manual` | `ManualClient` | no | `stdout` (prompt block) + `stdin` (user pastes the web-UI reply, terminated by a line containing only `END`) | shipped (Phase 2b) |
94
94
 
95
95
  Resolution lives in `scripts/ai_council/modes.py`:
@@ -338,7 +338,8 @@ Real failure modes seen in the wild:
338
338
 
339
339
  The bundler's redaction pass strips:
340
340
 
341
- - Paths matching `~/.config/agent-config/*.key`.
341
+ - Paths matching `~/.event4u/agent-config/*.key` and the legacy
342
+ `~/.config/agent-config/*.key`.
342
343
  - Lines starting with `Authorization:`.
343
344
  - `key = …`, `secret = …`, `token = …`, `password = …` assignments.
344
345
  - `sk-ant-…` and `sk-…` token-like strings.
@@ -358,7 +359,8 @@ per-invocation caps from `ai_council.cost_budget`:
358
359
  - `max_calls` — maximum number of council members per invocation.
359
360
  - `daily_limit_usd` — rolling 24h spend cap across all `/council`
360
361
  invocations. `0` disables. Persists in
361
- `~/.config/agent-config/council-spend.jsonl` (mode 0600). Breach
362
+ `~/.event4u/agent-config/council-spend.jsonl` (mode 0600; legacy
363
+ `~/.config/agent-config/council-spend.jsonl` read as fallback). Breach
362
364
  fires `on_overrun(event)` with `event.breach_kind == "daily"` and,
363
365
  if the callback returns False or is absent, tags the member
364
366
  `daily_budget_exceeded` instead of `cost_budget_exceeded`.
@@ -97,7 +97,7 @@ Ask format:
97
97
 
98
98
  > 1. `.worktrees/` — project-local, hidden
99
99
  > 2. `worktrees/` — project-local, visible
100
- > 3. `~/.config/agent-config/worktrees/<project>/` — global
100
+ > 3. `~/.event4u/agent-config/worktrees/<project>/` — global
101
101
 
102
102
  **Recommendation: 1 — `.worktrees/`** — project-local keeps the worktree next to the repo (easy cleanup), and the leading dot keeps it out of `ls`. Caveat: pick 3 if multiple repos must share a single worktree root.
103
103
 
@@ -6,10 +6,11 @@
6
6
  #
7
7
  # Precedence (lowest → highest):
8
8
  # 1. Package defaults (shipped by event4u/agent-config)
9
- # 2. ~/.config/agent-config/agent-settings.yml — user-global DX-comfort
9
+ # 2. ~/.event4u/agent-config/agent-settings.yml — user-global DX-comfort
10
10
  # defaults (whitelist: name, ide, cost_profile, personal.bot_icon,
11
11
  # personal.autonomy, caveman.speak_scope). Created on opt-in via
12
- # /onboard; project-local files always win.
12
+ # /onboard; project-local files always win. Legacy
13
+ # ~/.config/agent-config/agent-settings.yml is read as a fallback.
13
14
  # 3. This file (.agent-project-settings.yml) — team defaults
14
15
  # 4. .agent-settings.yml — developer overrides (gitignored)
15
16
  #
@@ -38,7 +39,7 @@ schema_version: 1
38
39
  # CI guard: a release bump of `package.json` must update this value
39
40
  # in lockstep — see scripts/check_template_pin_drift.py (road-to-
40
41
  # portable-runtime-and-update-check P3.3).
41
- agent_config_version: "1.41.2"
42
+ agent_config_version: "2.3.0"
42
43
 
43
44
  # --- Project identity ---
44
45
  project:
@@ -25,11 +25,18 @@ This repository contains {{project_description_oneline}}.
25
25
  > rules, guidelines) and `AGENTS.md`. The instructions below are
26
26
  > self-contained for Copilot Code Review.
27
27
  >
28
+ > For multi-step workflows (refactors, new features, bug investigations),
29
+ > switch Copilot Chat to **Agent mode** — it can read this file plus
30
+ > `.augment/` and orchestrate tools. **Ask mode** and inline **Edit mode**
31
+ > stay local to the current selection.
32
+ >
28
33
  > For most tickets — feature, bug fix, or refactor — start with
29
34
  > `/implement-ticket` (see `.augment/commands/implement-ticket.md`). It drives
30
35
  > the linear flow `refine → memory → analyze → plan → implement → test →
31
36
  > verify → report`, blocks on ambiguity instead of guessing, and never
32
37
  > commits, pushes, or opens PRs on its own.
38
+ >
39
+ > See `docs/setup/per-ide/copilot.md` for the full activation guide.
33
40
 
34
41
  ## ✅ Scope Control
35
42
 
@@ -5,11 +5,18 @@ how scripts read agent settings — replaces ~15 ad-hoc loaders in P3.
5
5
 
6
6
  Resolution order (deepest wins; user-global is whitelist-filtered only):
7
7
 
8
- N. ``~/.config/agent-config/agent-settings.yml`` (user-global; whitelist only)
8
+ N. ``~/.event4u/agent-config/agent-settings.yml`` (user-global; whitelist only)
9
9
  N-1. ``<repo-root>/.agent-settings.yml`` (project-wide; all keys)
10
10
  N-2. ``<intermediate-dir>/.agent-settings.yml`` (subsystem-scoped; all keys)
11
11
  1. ``<CWD>/.agent-settings.yml`` (deepest, wins; all keys)
12
12
 
13
+ The user-global path is resolved via the sibling
14
+ :mod:`work_engine._lib.user_global_paths` module (vendored from
15
+ ``scripts/_lib/user_global_paths.py`` so the engine stays self-contained
16
+ when shipped into consumer projects) with a read-fallback to the legacy
17
+ ``~/.config/agent-config/agent-settings.yml`` so pre-2.4 installs keep
18
+ working during the namespace migration.
19
+
13
20
  ``<repo-root>`` is the nearest ancestor that contains ``.git`` (directory
14
21
  **or** file — submodule support). The walk stops there — it never drifts
15
22
  into a parent repo or ``$HOME``. When ``cwd`` is ``None`` (default), the
@@ -37,12 +44,26 @@ import logging
37
44
  from pathlib import Path
38
45
  from typing import Any, Iterator
39
46
 
47
+ from . import user_global_paths
48
+
40
49
  logger = logging.getLogger(__name__)
41
50
 
42
51
  DEFAULT_PROJECT_FILE = ".agent-settings.yml"
43
- DEFAULT_USER_GLOBAL_FILE = (
44
- Path.home() / ".config" / "agent-config" / "agent-settings.yml"
45
- )
52
+ USER_GLOBAL_FILENAME = "agent-settings.yml"
53
+
54
+ #: Canonical write target under the new ``~/.event4u/agent-config/``
55
+ #: namespace. Reads route through :func:`_resolve_user_global_file` so
56
+ #: pre-2.4 installs are still picked up from ``~/.config/agent-config/``
57
+ #: until the migration shim copies them across.
58
+ DEFAULT_USER_GLOBAL_FILE = user_global_paths.write_target(USER_GLOBAL_FILENAME)
59
+
60
+
61
+ def _resolve_user_global_file() -> Path:
62
+ """Return the active user-global settings path with legacy fallback."""
63
+ found = user_global_paths.resolve_with_fallback(USER_GLOBAL_FILENAME)
64
+ if found is not None:
65
+ return found
66
+ return DEFAULT_USER_GLOBAL_FILE
46
67
 
47
68
  #: Exact dotted paths allowed to cascade from user-global into the merged
48
69
  #: settings. Anything not listed here is silently ignored when present in
@@ -129,7 +150,8 @@ def load_agent_settings(
129
150
 
130
151
  ``project_path`` defaults to ``./.agent-settings.yml`` (CWD-relative).
131
152
  ``user_global_path`` defaults to
132
- ``~/.config/agent-config/agent-settings.yml``. Both arguments accept
153
+ ``~/.event4u/agent-config/agent-settings.yml`` (with a read fallback
154
+ to the legacy ``~/.config/agent-config/agent-settings.yml``). Both arguments accept
133
155
  ``Path`` or ``str``. Pass ``verbose=True`` to log keys present in
134
156
  user-global that are not on the whitelist.
135
157
 
@@ -143,7 +165,7 @@ def load_agent_settings(
143
165
  with pre-cascade callers.
144
166
  """
145
167
  user_global_raw = _read_yaml(
146
- Path(user_global_path) if user_global_path else DEFAULT_USER_GLOBAL_FILE,
168
+ Path(user_global_path) if user_global_path else _resolve_user_global_file(),
147
169
  ) or {}
148
170
 
149
171
  user_global_filtered, ignored = _filter_whitelist(
@@ -181,7 +203,7 @@ def iter_setting_overrides(
181
203
  Never blocks, never raises on missing files.
182
204
  """
183
205
  user_global_path_resolved = (
184
- Path(user_global_path) if user_global_path else DEFAULT_USER_GLOBAL_FILE
206
+ Path(user_global_path) if user_global_path else _resolve_user_global_file()
185
207
  )
186
208
  user_global_raw = _read_yaml(user_global_path_resolved) or {}
187
209
  user_global_filtered, _ = _filter_whitelist(user_global_raw, MERGEABLE_KEYS)
@@ -0,0 +1,249 @@
1
+ """Vendor-namespaced user-global path resolution.
2
+
3
+ Phase 1 of road-to-event4u-namespace-and-claude-desktop.md. Single source
4
+ of truth for "where does this package keep user-global state on disk?".
5
+ Replaces hard-coded ``~/.config/agent-config/`` literals scattered across
6
+ ``scripts/_lib`` and ``scripts/ai_council``.
7
+
8
+ Resolution order:
9
+
10
+ 1. ``$EVENT4U_CONFIG_HOME`` — full path override (testing + power users).
11
+ 2. ``~/.event4u/agent-config/`` — vendor-namespaced source-of-truth.
12
+
13
+ For backward compatibility during the transition, ``legacy_xdg_root()``
14
+ exposes the old ``~/.config/agent-config/`` path so loaders can read
15
+ state written by pre-2.4 installs. Writers should never target the
16
+ legacy path; the auto-migration shim (Phase 3) copies state once into
17
+ the new namespace.
18
+
19
+ Contract — pure, read-only, never auto-creates directories.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import shutil
25
+ from pathlib import Path
26
+ from typing import Optional
27
+
28
+ #: Marker suffix for in-progress entry copies during migration. A copy
29
+ #: that crashes mid-flight leaves ``<name><suffix><pid>`` behind so the
30
+ #: next run can clean it up before retrying — instead of treating a
31
+ #: partial subdir as a completed copy.
32
+ _PARTIAL_SUFFIX = ".event4u-partial-"
33
+
34
+ #: Environment variable that overrides ``event4u_root()`` outright.
35
+ #: Accepts a full path (``~`` expanded). Primarily used by tests; power
36
+ #: users may also point this at a custom location.
37
+ EVENT4U_HOME_ENV = "EVENT4U_CONFIG_HOME"
38
+
39
+ #: Vendor-namespaced default. Relative to the user's home directory.
40
+ DEFAULT_EVENT4U_ROOT_RELATIVE = Path(".event4u") / "agent-config"
41
+
42
+ #: Legacy XDG-shaped default written by pre-2.4 installs. Read-only
43
+ #: fallback during the transition; never the target of a write.
44
+ LEGACY_XDG_ROOT_RELATIVE = Path(".config") / "agent-config"
45
+
46
+
47
+ def event4u_root(env: Optional[dict] = None) -> Path:
48
+ """Return the active user-global root directory.
49
+
50
+ Honours ``EVENT4U_CONFIG_HOME`` first, falls back to
51
+ ``~/.event4u/agent-config/``. Never creates the directory.
52
+ """
53
+ env_map = env if env is not None else os.environ
54
+ override = env_map.get(EVENT4U_HOME_ENV)
55
+ if override:
56
+ return Path(override).expanduser()
57
+ return Path.home() / DEFAULT_EVENT4U_ROOT_RELATIVE
58
+
59
+
60
+ def legacy_xdg_root() -> Path:
61
+ """Return the pre-2.4 user-global root at ``~/.config/agent-config/``.
62
+
63
+ Used by loaders during the transition to read settings, lockfiles,
64
+ and keys written before the namespace migration ran. Writers MUST
65
+ NOT target this path — only ``event4u_root()`` is a valid write
66
+ target. Never creates the directory.
67
+ """
68
+ return Path.home() / LEGACY_XDG_ROOT_RELATIVE
69
+
70
+
71
+ def resolve_with_fallback(
72
+ relative_name: str,
73
+ *,
74
+ env: Optional[dict] = None,
75
+ ) -> Optional[Path]:
76
+ """Resolve a named file/dir under the user-global root, with legacy fallback.
77
+
78
+ Returns the new-namespace path if it exists on disk, otherwise the
79
+ legacy XDG path if it exists, otherwise ``None``. Callers that need
80
+ the *write target* (regardless of existence) should use
81
+ ``event4u_root() / relative_name`` directly.
82
+
83
+ ``relative_name`` is a forward-slash separated string (e.g.
84
+ ``"installed.lock"`` or ``"agents/global"``). It is treated as a
85
+ path fragment relative to the chosen root; absolute paths are
86
+ rejected with ``ValueError``.
87
+ """
88
+ fragment = Path(relative_name)
89
+ if fragment.is_absolute():
90
+ raise ValueError(
91
+ f"resolve_with_fallback expects a relative path, got {relative_name!r}"
92
+ )
93
+ new_path = event4u_root(env=env) / fragment
94
+ if new_path.exists():
95
+ return new_path
96
+ legacy_path = legacy_xdg_root() / fragment
97
+ if legacy_path.exists():
98
+ return legacy_path
99
+ return None
100
+
101
+
102
+ def write_target(relative_name: str, *, env: Optional[dict] = None) -> Path:
103
+ """Return the canonical write target for a named user-global file/dir.
104
+
105
+ Always rooted at ``event4u_root()`` — writers never target the
106
+ legacy XDG path. Caller is responsible for ``mkdir(parents=True)``
107
+ on the parent before writing. Never creates the directory itself.
108
+ """
109
+ fragment = Path(relative_name)
110
+ if fragment.is_absolute():
111
+ raise ValueError(
112
+ f"write_target expects a relative path, got {relative_name!r}"
113
+ )
114
+ return event4u_root(env=env) / fragment
115
+
116
+
117
+ #: Breadcrumb dropped into the legacy root after a successful migration.
118
+ #: Tells the user where their state now lives and how to clean up. The
119
+ #: legacy tree itself is never auto-deleted — only the user does that.
120
+ MIGRATION_BREADCRUMB_NAME = "MIGRATED.md"
121
+
122
+ _BREADCRUMB_TEMPLATE = """# Migrated to `~/.event4u/agent-config/`
123
+
124
+ This directory (`~/.config/agent-config/`) is the **legacy** location
125
+ for `event4u/agent-config` user-global state. As of v2.4 the canonical
126
+ location is `~/.event4u/agent-config/`.
127
+
128
+ The migration shim has already copied your settings, keys, lockfiles,
129
+ and overrides into the new namespace. File modes (0600 on keys) were
130
+ preserved. Loaders prefer the new path but still read from this tree
131
+ as a fallback, so removing it is safe **once you've confirmed** the
132
+ new location is working.
133
+
134
+ ## To clean up
135
+
136
+ ```bash
137
+ rm -rf ~/.config/agent-config
138
+ ```
139
+
140
+ ## Why the move
141
+
142
+ `~/.config/` is a generic XDG-shaped directory shared by many tools.
143
+ `~/.event4u/agent-config/` is vendor-namespaced and avoids collisions
144
+ with unrelated CLIs. See
145
+ `agents/roadmaps/road-to-event4u-namespace-and-claude-desktop.md` for
146
+ the full rationale.
147
+ """
148
+
149
+
150
+ def migrate_legacy_namespace(
151
+ *,
152
+ env: Optional[dict] = None,
153
+ legacy_root_override: Optional[Path] = None,
154
+ ) -> bool:
155
+ """Copy pre-2.4 user-global state from legacy XDG root into the new namespace.
156
+
157
+ Idempotent and safe to call on every install / init. Returns ``True``
158
+ if a copy ran during this invocation, ``False`` when the migration
159
+ was already complete or there was nothing to migrate.
160
+
161
+ Contract:
162
+
163
+ - Never auto-deletes the legacy tree — that's the user's call (the
164
+ breadcrumb at ``~/.config/agent-config/MIGRATED.md`` documents it).
165
+ - Preserves file modes via ``shutil.copytree(..., copy_function=copy2)``
166
+ so 0600 key files stay 0600 after the copy.
167
+ - If the new root already exists with any content, the migration
168
+ treats it as already-done and only writes the breadcrumb (if
169
+ missing) — never overwrites new-namespace state.
170
+ - If the legacy root is missing or empty, the function is a no-op.
171
+ - Per-entry atomic write: each entry is copied to a sibling
172
+ ``<name>.event4u-partial-<pid>`` and then ``os.replace``'d into
173
+ the final name. If a previous run crashed mid-``copytree``, the
174
+ leftover ``*.event4u-partial-*`` siblings are cleaned up at the
175
+ top of the next run before retrying — a partial directory is
176
+ never mistaken for a completed copy.
177
+
178
+ ``legacy_root_override`` is for tests; production callers leave it ``None``.
179
+ """
180
+ legacy_root = (
181
+ legacy_root_override if legacy_root_override is not None else legacy_xdg_root()
182
+ )
183
+ new_root = event4u_root(env=env)
184
+
185
+ if not legacy_root.exists() or not legacy_root.is_dir():
186
+ return False
187
+
188
+ # Skip the migrated-breadcrumb itself when checking for content so a
189
+ # second invocation does not loop on its own marker.
190
+ legacy_entries = [
191
+ p for p in legacy_root.iterdir() if p.name != MIGRATION_BREADCRUMB_NAME
192
+ ]
193
+ if not legacy_entries:
194
+ return False
195
+
196
+ # Real content check ignores partial-copy debris from a prior
197
+ # interrupted run; otherwise the breadcrumb would be written for
198
+ # a half-finished migration and retry would never run.
199
+ new_has_content = new_root.exists() and any(
200
+ not _is_partial_entry(p) for p in new_root.iterdir()
201
+ )
202
+ if new_has_content:
203
+ _ensure_breadcrumb(legacy_root)
204
+ return False
205
+
206
+ new_root.mkdir(parents=True, exist_ok=True)
207
+ _purge_partial_entries(new_root)
208
+
209
+ for entry in legacy_entries:
210
+ target = new_root / entry.name
211
+ if target.exists():
212
+ continue
213
+ staging = new_root / f"{entry.name}{_PARTIAL_SUFFIX}{os.getpid()}"
214
+ if staging.exists():
215
+ _remove_path(staging)
216
+ if entry.is_dir():
217
+ shutil.copytree(entry, staging, copy_function=shutil.copy2)
218
+ else:
219
+ shutil.copy2(entry, staging)
220
+ os.replace(staging, target)
221
+
222
+ _ensure_breadcrumb(legacy_root)
223
+ return True
224
+
225
+
226
+ def _is_partial_entry(path: Path) -> bool:
227
+ return _PARTIAL_SUFFIX in path.name
228
+
229
+
230
+ def _purge_partial_entries(new_root: Path) -> None:
231
+ """Remove ``*.event4u-partial-*`` leftovers from a previous interrupted run."""
232
+ for entry in new_root.iterdir():
233
+ if _is_partial_entry(entry):
234
+ _remove_path(entry)
235
+
236
+
237
+ def _remove_path(path: Path) -> None:
238
+ if path.is_dir() and not path.is_symlink():
239
+ shutil.rmtree(path)
240
+ else:
241
+ path.unlink(missing_ok=True)
242
+
243
+
244
+ def _ensure_breadcrumb(legacy_root: Path) -> None:
245
+ """Write the ``MIGRATED.md`` breadcrumb into ``legacy_root`` if absent."""
246
+ breadcrumb = legacy_root / MIGRATION_BREADCRUMB_NAME
247
+ if breadcrumb.exists():
248
+ return
249
+ breadcrumb.write_text(_BREADCRUMB_TEMPLATE, encoding="utf-8")
@@ -17,8 +17,9 @@ settings.py``):
17
17
  Per road-to-portable-dev-preferences P3, the YAML read goes through
18
18
  :func:`work_engine._lib.agent_settings.load_agent_settings`, which
19
19
  cascades the whitelisted ``cost_profile`` (and other DX-comfort keys)
20
- from ``~/.config/agent-config/agent-settings.yml`` when the project
21
- file omits them. Project values always win.
20
+ from ``~/.event4u/agent-config/agent-settings.yml`` (legacy
21
+ ``~/.config/agent-config/agent-settings.yml`` read as fallback) when
22
+ the project file omits them. Project values always win.
22
23
  """
23
24
  from __future__ import annotations
24
25
 
@@ -67,9 +68,11 @@ def load_hook_settings(
67
68
  ``settings_path`` defaults to ``./.agent-settings.yml`` relative to
68
69
  the current working directory — same convention as chat-history.
69
70
  ``user_global_path`` defaults to
70
- ``~/.config/agent-config/agent-settings.yml`` and only cascades the
71
- whitelisted DX-comfort keys (currently ``cost_profile``) when the
72
- project file omits them. See road-to-portable-dev-preferences P3.
71
+ ``~/.event4u/agent-config/agent-settings.yml`` (with a read fallback
72
+ to the legacy ``~/.config/agent-config/agent-settings.yml``) and
73
+ only cascades the whitelisted DX-comfort keys (currently
74
+ ``cost_profile``) when the project file omits them. See
75
+ road-to-portable-dev-preferences P3.
73
76
  """
74
77
  path = Path(settings_path) if settings_path else Path(DEFAULT_SETTINGS_FILE)
75
78
  raw = load_agent_settings(
@@ -6,12 +6,38 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Shared agent configuration \u2014 skills for AI coding tools (Claude Code, Augment, Cursor, Cline, Windsurf, Gemini CLI).",
9
- "version": "2.2.2"
9
+ "version": "2.4.0",
10
+ "keywords": [
11
+ "agent-config",
12
+ "skills",
13
+ "rules",
14
+ "claude-code",
15
+ "augment",
16
+ "cursor",
17
+ "cline",
18
+ "windsurf",
19
+ "gemini",
20
+ "copilot",
21
+ "laravel",
22
+ "php",
23
+ "ai-coding"
24
+ ]
10
25
  },
11
26
  "plugins": [
12
27
  {
13
28
  "name": "agent-config",
14
29
  "description": "Full skill catalog \u2014 writing, reviewing, managing, and analyzing agent content, plus stack-specific skills for Laravel, PHP, Docker, AWS, Playwright, and more.",
30
+ "keywords": [
31
+ "skills",
32
+ "rules",
33
+ "commands",
34
+ "laravel",
35
+ "php",
36
+ "review",
37
+ "testing",
38
+ "infrastructure",
39
+ "documentation"
40
+ ],
15
41
  "source": "./",
16
42
  "strict": false,
17
43
  "skills": [