@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.
- package/.agent-src/commands/onboard.md +14 -9
- package/.agent-src/rules/external-reference-deep-dive.md +69 -0
- package/.agent-src/skills/ai-council/SKILL.md +5 -3
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- package/.agent-src/templates/agents/agent-project-settings.example.yml +4 -3
- package/.agent-src/templates/copilot-instructions.md +7 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +29 -7
- package/.agent-src/templates/scripts/work_engine/_lib/user_global_paths.py +249 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +8 -5
- package/.claude-plugin/marketplace.json +27 -1
- package/CHANGELOG.md +79 -0
- package/README.md +1 -8
- package/config/agent-settings.template.yml +5 -3
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +5 -3
- package/docs/contracts/installed-tools-lockfile.md +142 -0
- package/docs/customization.md +23 -17
- package/docs/decisions/ADR-007-agent-discovery-scopes.md +6 -0
- package/docs/decisions/ADR-009-event4u-namespace.md +188 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/development.md +37 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +6 -4
- package/docs/installation.md +17 -2
- package/docs/migration/v1-to-v2.md +45 -0
- package/docs/setup/per-ide/antigravity.md +63 -0
- package/docs/setup/per-ide/augment.md +77 -0
- package/docs/setup/per-ide/claude-desktop.md +107 -65
- package/docs/setup/per-ide/codebuddy.md +63 -0
- package/docs/setup/per-ide/continue.md +68 -0
- package/docs/setup/per-ide/droid.md +65 -0
- package/docs/setup/per-ide/jetbrains.md +76 -0
- package/docs/setup/per-ide/kilocode.md +66 -0
- package/docs/setup/per-ide/kiro.md +72 -0
- package/docs/setup/per-ide/opencode.md +62 -0
- package/docs/setup/per-ide/qoder.md +63 -0
- package/docs/setup/per-ide/roocode.md +68 -0
- package/docs/setup/per-ide/trae.md +63 -0
- package/docs/setup/per-ide/warp.md +63 -0
- package/docs/setup/per-ide/zed.md +73 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_doctor.py +351 -0
- package/scripts/_cli/cmd_prune.py +317 -0
- package/scripts/_cli/cmd_uninstall.py +465 -0
- package/scripts/_cli/cmd_update.py +30 -4
- package/scripts/_cli/cmd_versions.py +147 -0
- package/scripts/_lib/agent_settings.py +29 -7
- package/scripts/_lib/agents_overlay.py +15 -4
- package/scripts/_lib/claude_desktop_bundler.py +150 -0
- package/scripts/_lib/fs_atomic.py +116 -0
- package/scripts/_lib/installed_lock.py +37 -4
- package/scripts/_lib/installed_tools.py +189 -45
- package/scripts/_lib/json_pointers.py +260 -0
- package/scripts/_lib/update_check.py +29 -5
- package/scripts/_lib/user_global_paths.py +249 -0
- package/scripts/agent-config +69 -0
- package/scripts/ai_council/__init__.py +4 -3
- package/scripts/ai_council/budget_guard.py +34 -4
- package/scripts/ai_council/bundler.py +2 -0
- package/scripts/ai_council/clients.py +28 -7
- package/scripts/compress.py +78 -15
- package/scripts/install +8 -0
- package/scripts/install-hooks.sh +54 -1
- package/scripts/install.py +1149 -53
- package/scripts/install_anthropic_key.sh +5 -3
- package/scripts/install_openai_key.sh +5 -3
- 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 `~/.
|
|
44
|
-
|
|
45
|
-
|
|
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 ~/.
|
|
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 ~/.
|
|
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 `~/.
|
|
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`](
|
|
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
|
-
`~/.
|
|
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`](
|
|
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 `~/.
|
|
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
|
-
`~/.
|
|
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. `~/.
|
|
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. ~/.
|
|
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: "
|
|
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. ``~/.
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
``~/.
|
|
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
|
|
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
|
|
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 ``~/.
|
|
21
|
-
|
|
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
|
-
``~/.
|
|
71
|
-
|
|
72
|
-
|
|
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.
|
|
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": [
|