@event4u/agent-config 1.38.0 → 1.39.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 +131 -50
- package/.agent-src/templates/agents/agent-project-settings.example.yml +9 -2
- package/.agent-src/templates/scripts/work_engine/_lib/__init__.py +7 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +168 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +18 -19
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +34 -0
- package/README.md +14 -3
- package/docs/customization.md +45 -0
- package/docs/guidelines/agent-infra/layered-settings.md +54 -17
- package/docs/setup/mcp-client-config.md +152 -0
- package/docs/setup/mcp-cloud-endpoints.md +16 -0
- package/package.json +1 -1
- package/scripts/_lib/agent_settings.py +168 -0
|
@@ -12,35 +12,68 @@ suggestion:
|
|
|
12
12
|
|
|
13
13
|
# /onboard
|
|
14
14
|
|
|
15
|
-
Centralized first-run flow. Bundles
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`.agent-settings.yml`.
|
|
15
|
+
Centralized first-run flow. Bundles scattered "ask once" prompts (user_name,
|
|
16
|
+
IDE, rtk install, cost profile, learning loop) into one interactive setup.
|
|
17
|
+
Ends by setting `onboarding.onboarded: true` in `.agent-settings.yml`.
|
|
19
18
|
|
|
20
|
-
Triggered by
|
|
21
|
-
`onboarding.onboarded` is `false
|
|
19
|
+
Triggered by [`onboarding-gate`](../rules/onboarding-gate.md) when
|
|
20
|
+
`onboarding.onboarded` is `false`, or by explicit re-run.
|
|
22
21
|
|
|
23
22
|
## When NOT to use
|
|
24
23
|
|
|
25
24
|
- Change cost profile only → [`/set-cost-profile`](set-cost-profile.md).
|
|
26
|
-
- Single-value edit → ask
|
|
27
|
-
|
|
28
|
-
[`layered-settings`](../docs/guidelines/agent-infra/layered-settings.md).
|
|
25
|
+
- Single-value edit → ask agent to change it, or edit `.agent-settings.yml`
|
|
26
|
+
directly per [`layered-settings`](../docs/guidelines/agent-infra/layered-settings.md).
|
|
29
27
|
|
|
30
28
|
## Preconditions
|
|
31
29
|
|
|
32
|
-
`.agent-settings.yml` exists. If missing, tell
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
`.agent-settings.yml` exists. If missing, tell user to run `scripts/install`
|
|
31
|
+
(or `python3 scripts/install.py`) first and stop — command assumes file +
|
|
32
|
+
template defaults are in place.
|
|
35
33
|
|
|
36
34
|
## Steps
|
|
37
35
|
|
|
38
36
|
### 1. Greet and set expectations
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
One line: one-time setup, six questions, one at a time (iron law from
|
|
39
|
+
`user-interaction`).
|
|
42
40
|
|
|
43
|
-
### 2.
|
|
41
|
+
### 2. Offer user-global cross-project defaults
|
|
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`.
|
|
46
|
+
|
|
47
|
+
- **File exists** → skip step entirely. Re-onboarding never overwrites
|
|
48
|
+
user-global file silently.
|
|
49
|
+
- **File missing AND first-time setup heuristic** — heuristic for "first
|
|
50
|
+
machine setup": no other `.agent-settings.yml` in any sibling project on
|
|
51
|
+
disk. Conservative shell probe:
|
|
52
|
+
`find $(dirname "$PWD") -maxdepth 3 -name .agent-settings.yml 2>/dev/null | grep -v "^$PWD/" | head -1`
|
|
53
|
+
→ non-empty → developer done this before, **skip**.
|
|
54
|
+
→ empty → first-time setup, ask:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
> A user-global config at ~/.config/agent-config/agent-settings.yml lets
|
|
58
|
+
> you carry your DX-comfort defaults (name, IDE, autonomy, cost profile,
|
|
59
|
+
> communication style) across every project that uses event4u/agent-config.
|
|
60
|
+
>
|
|
61
|
+
> Project-local .agent-settings.yml always wins. Only six keys are
|
|
62
|
+
> mergeable from the user-global file:
|
|
63
|
+
> name · ide · cost_profile · personal.bot_icon · personal.autonomy · caveman.speak_scope
|
|
64
|
+
>
|
|
65
|
+
> 1. Yes — create it after this onboarding finishes
|
|
66
|
+
> 2. No — keep settings project-local only
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If user picks `1`, **defer write** to tail step (see step 9). Capture choice
|
|
70
|
+
in working memory only; do **not** create file here. File gets written
|
|
71
|
+
**after** project-local values are confirmed, so initial values mirror
|
|
72
|
+
what developer just chose for this project.
|
|
73
|
+
|
|
74
|
+
If user picks `2`, set working-memory flag to skip step 9.
|
|
75
|
+
|
|
76
|
+
### 3. Capture `personal.user_name`
|
|
44
77
|
|
|
45
78
|
Skip if already set (non-empty). Otherwise:
|
|
46
79
|
|
|
@@ -51,11 +84,11 @@ Skip if already set (non-empty). Otherwise:
|
|
|
51
84
|
> 2. Skip — stay anonymous
|
|
52
85
|
```
|
|
53
86
|
|
|
54
|
-
Free-text
|
|
87
|
+
Free-text → write to `personal.user_name`. `2` → leave empty.
|
|
55
88
|
|
|
56
|
-
###
|
|
89
|
+
### 4. Capture `personal.ide` (with auto-detect)
|
|
57
90
|
|
|
58
|
-
Skip if
|
|
91
|
+
Skip if set. Otherwise auto-detect first:
|
|
59
92
|
|
|
60
93
|
```bash
|
|
61
94
|
ps aux | grep -iE '(Visual Studio Code|Code Helper|phpstorm|cursor)' | grep -v grep
|
|
@@ -73,13 +106,13 @@ ps aux | grep -iE '(Visual Studio Code|Code Helper|phpstorm|cursor)' | grep -v g
|
|
|
73
106
|
> 4. Skip — I'll configure it later
|
|
74
107
|
```
|
|
75
108
|
|
|
76
|
-
If IDE
|
|
109
|
+
If IDE set, also ask `personal.open_edited_files` (`true`/`false`).
|
|
77
110
|
|
|
78
|
-
###
|
|
111
|
+
### 5. Capture `personal.pr_comment_bot_icon`
|
|
79
112
|
|
|
80
|
-
Personal preference — each developer decides how
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
Personal preference — each developer decides how own PR replies look. Skip
|
|
114
|
+
only if user already set non-default deliberately (agent can't tell, so
|
|
115
|
+
always ask on first run):
|
|
83
116
|
|
|
84
117
|
```
|
|
85
118
|
> When I reply to PR review comments on your behalf, should I prefix each
|
|
@@ -91,7 +124,7 @@ deliberately (agent can't tell, so always ask on first run):
|
|
|
91
124
|
|
|
92
125
|
`1` → write `personal.pr_comment_bot_icon: true`. `2` → leave `false`.
|
|
93
126
|
|
|
94
|
-
###
|
|
127
|
+
### 6. Detect `personal.rtk_installed`
|
|
95
128
|
|
|
96
129
|
Silent `which rtk`.
|
|
97
130
|
|
|
@@ -108,16 +141,17 @@ Silent `which rtk`.
|
|
|
108
141
|
```
|
|
109
142
|
|
|
110
143
|
`1` or `2` → run install, on success set `rtk_installed: true` and apply
|
|
111
|
-
rtk post-install steps (telemetry off, init --global) per
|
|
112
|
-
[`rtk-output-filtering`](../skills/rtk-output-filtering/SKILL.md)
|
|
113
|
-
`3` → leave `rtk_installed: false
|
|
114
|
-
|
|
144
|
+
rtk post-install steps (telemetry off, init --global) per
|
|
145
|
+
[`rtk-output-filtering`](../skills/rtk-output-filtering/SKILL.md).
|
|
146
|
+
`3` → leave `rtk_installed: false`, move on. No "ask again tomorrow" —
|
|
147
|
+
`/onboard` is one-shot.
|
|
148
|
+
|
|
115
149
|
|
|
116
|
-
###
|
|
150
|
+
### 7. Confirm `cost_profile` and learning loop
|
|
117
151
|
|
|
118
152
|
Read current `cost_profile` and `pipelines.skill_improvement` values.
|
|
119
|
-
Present
|
|
120
|
-
|
|
153
|
+
Present plainly (sensible defaults from template — `minimal` +
|
|
154
|
+
`skill_improvement: true`):
|
|
121
155
|
|
|
122
156
|
```
|
|
123
157
|
> Cost profile: {current} (minimal by default — includes the learning loop)
|
|
@@ -128,16 +162,54 @@ template — `minimal` + `skill_improvement: true`):
|
|
|
128
162
|
> 3. Disable learning loop — sets pipelines.skill_improvement=false
|
|
129
163
|
```
|
|
130
164
|
|
|
131
|
-
`2` → defer to `/set-cost-profile` and return here. `3` → flip
|
|
165
|
+
`2` → defer to `/set-cost-profile` and return here. `3` → flip toggle.
|
|
132
166
|
|
|
133
|
-
###
|
|
167
|
+
### 8. Mark onboarded
|
|
134
168
|
|
|
135
|
-
Write `onboarding.onboarded: true` to `.agent-settings.yml` using
|
|
169
|
+
Write `onboarding.onboarded: true` to `.agent-settings.yml` using
|
|
136
170
|
section-aware merge rules from
|
|
137
171
|
[`layered-settings`](../docs/guidelines/agent-infra/layered-settings.md#section-aware-merge-rules)
|
|
138
|
-
(preserve comments, key order, touch only
|
|
172
|
+
(preserve comments, key order, touch only changed fields).
|
|
173
|
+
|
|
174
|
+
### 9. Write user-global file (only if opted in at step 2)
|
|
175
|
+
|
|
176
|
+
Skip unless step 2 captured explicit "yes". Re-confirm intent in one line —
|
|
177
|
+
never silent-write a file outside project tree:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
> Writing ~/.config/agent-config/agent-settings.yml with the six
|
|
181
|
+
> mergeable keys mirrored from this project's choices:
|
|
182
|
+
>
|
|
183
|
+
> name: {personal.user_name or ""}
|
|
184
|
+
> ide: {personal.ide or ""}
|
|
185
|
+
> cost_profile: {cost_profile}
|
|
186
|
+
> personal.bot_icon: {personal.pr_comment_bot_icon}
|
|
187
|
+
> personal.autonomy: {personal.autonomy or "ask"}
|
|
188
|
+
> caveman.speak_scope: {caveman.speak_scope or "prose_only"}
|
|
189
|
+
>
|
|
190
|
+
> 1. Yes, write it
|
|
191
|
+
> 2. Cancel — keep settings project-local only
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`1` → ensure `~/.config/agent-config/` exists (`mkdir -p`, mode `0700`),
|
|
195
|
+
then write file with mode `0600`. Schema is **flat-or-nested YAML keyed on
|
|
196
|
+
dotted paths** in whitelist documented in
|
|
197
|
+
[`scripts/_lib/agent_settings.py`](../scripts/_lib/agent_settings.py).
|
|
198
|
+
Use same section-aware merge rules from
|
|
199
|
+
[`layered-settings`](../docs/guidelines/agent-infra/layered-settings.md#section-aware-merge-rules)
|
|
200
|
+
**only if file unexpectedly already exists** between step 2 and this step
|
|
201
|
+
(race condition); otherwise create from scratch with exact six keys above
|
|
202
|
+
and a one-line file header comment:
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
# event4u/agent-config — user-global DX-comfort defaults
|
|
206
|
+
# Whitelist: name · ide · cost_profile · personal.bot_icon · personal.autonomy · caveman.speak_scope
|
|
207
|
+
# Project-local .agent-settings.yml always wins. See docs/customization.md.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`2` → no write, no error, no second ask. Move on.
|
|
139
211
|
|
|
140
|
-
###
|
|
212
|
+
### 10. Summary
|
|
141
213
|
|
|
142
214
|
Echo what was captured, in one block:
|
|
143
215
|
|
|
@@ -152,18 +224,19 @@ Echo what was captured, in one block:
|
|
|
152
224
|
cost_profile: {value}
|
|
153
225
|
pipelines.skill_improvement: {value}
|
|
154
226
|
onboarding.onboarded: true
|
|
227
|
+
user-global: {"written" if step 9 wrote · "—" otherwise}
|
|
155
228
|
|
|
156
229
|
You can re-run this with /onboard anytime, or edit .agent-settings.yml
|
|
157
230
|
directly — the agent follows the merge rules in `layered-settings` when
|
|
158
231
|
you ask it to change a value.
|
|
159
232
|
```
|
|
160
233
|
|
|
161
|
-
###
|
|
234
|
+
### 11. Maintainer-only feature pointer
|
|
162
235
|
|
|
163
|
-
Print
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
236
|
+
Print one-screen hint after summary — no question, no prompt, just pointer
|
|
237
|
+
for maintainers who want to opt into artefact-engagement telemetry layer.
|
|
238
|
+
Consumers can ignore; feature is **default-off** and stays off unless
|
|
239
|
+
explicitly enabled.
|
|
167
240
|
|
|
168
241
|
```
|
|
169
242
|
ℹ️ Maintainer telemetry (opt-in)
|
|
@@ -181,20 +254,27 @@ Skip this block in cloud surfaces (no settings file, no log path).
|
|
|
181
254
|
|
|
182
255
|
## Gotchas
|
|
183
256
|
|
|
184
|
-
- `.agent-settings.yml` is git-ignored.
|
|
185
|
-
- One question per turn.
|
|
186
|
-
|
|
257
|
+
- `.agent-settings.yml` is git-ignored. Command never commits.
|
|
258
|
+
- One question per turn. Iron law from `ask-when-uncertain` applies; do
|
|
259
|
+
not stack questions 2–9 into single prompt.
|
|
187
260
|
- Re-running `/onboard` when `onboarded: true` is allowed — walk through
|
|
188
|
-
all steps again and rewrite
|
|
189
|
-
- Never overwrite
|
|
190
|
-
|
|
261
|
+
all steps again and rewrite values user confirms.
|
|
262
|
+
- Never overwrite non-empty value without asking (applies to `user_name`,
|
|
263
|
+
`ide`).
|
|
264
|
+
- **User-global file is opt-in, one-shot, never silent.** Step 2 captures
|
|
265
|
+
intent, step 9 re-confirms before actual write. If
|
|
266
|
+
`~/.config/agent-config/agent-settings.yml` already exists when
|
|
267
|
+
`/onboard` starts, step 2 is skipped entirely — re-onboarding never
|
|
268
|
+
silently rewrites developer's cross-project defaults. Use
|
|
269
|
+
`/sync-agent-settings` (project-scoped only) or edit file manually for
|
|
270
|
+
mid-life changes.
|
|
191
271
|
|
|
192
272
|
## Cloud Behavior
|
|
193
273
|
|
|
194
274
|
On cloud surfaces (Claude.ai Web, Skills API) this command is **fully inert** —
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
275
|
+
no `.agent-settings.yml` to write, no `onboarding.onboarded` key to flip,
|
|
276
|
+
no local IDE/rtk env to capture. First-run setup is local-agent concern;
|
|
277
|
+
cloud agent should proceed without invoking it.
|
|
198
278
|
|
|
199
279
|
## See also
|
|
200
280
|
|
|
@@ -202,3 +282,4 @@ local-agent concern; the cloud agent should proceed without invoking it.
|
|
|
202
282
|
- [`set-cost-profile`](set-cost-profile.md) — isolated profile change
|
|
203
283
|
- [`layered-settings`](../docs/guidelines/agent-infra/layered-settings.md) — merge rules for mid-life edits
|
|
204
284
|
- [`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
|
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Precedence (lowest → highest):
|
|
8
8
|
# 1. Package defaults (shipped by event4u/agent-config)
|
|
9
|
-
# 2.
|
|
10
|
-
#
|
|
9
|
+
# 2. ~/.config/agent-config/agent-settings.yml — user-global DX-comfort
|
|
10
|
+
# defaults (whitelist: name, ide, cost_profile, personal.bot_icon,
|
|
11
|
+
# personal.autonomy, caveman.speak_scope). Created on opt-in via
|
|
12
|
+
# /onboard; project-local files always win.
|
|
13
|
+
# 3. This file (.agent-project-settings.yml) — team defaults
|
|
14
|
+
# 4. .agent-settings.yml — developer overrides (gitignored)
|
|
11
15
|
#
|
|
12
16
|
# Any key marked `locked: true` in this file CANNOT be overridden by
|
|
13
17
|
# .agent-settings.yml. Use sparingly — locked keys reduce developer
|
|
14
18
|
# autonomy. Reserve for compliance or correctness concerns (e.g.
|
|
15
19
|
# forcing a test framework, pinning a coding style).
|
|
16
20
|
#
|
|
21
|
+
# Full precedence model + user-global whitelist contract:
|
|
22
|
+
# docs/guidelines/agent-infra/layered-settings.md
|
|
23
|
+
#
|
|
17
24
|
# Copy this file to `.agent-project-settings.yml` (drop the `.example`)
|
|
18
25
|
# and commit it.
|
|
19
26
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Internal helpers shared across work_engine submodules.
|
|
2
|
+
|
|
3
|
+
Currently houses :mod:`agent_settings` — the byte-identical mirror of
|
|
4
|
+
``scripts/_lib/agent_settings.py`` from the agent-config package. The
|
|
5
|
+
parity test in ``tests/test_template_agent_settings_parity.py`` guards
|
|
6
|
+
the two files against drift.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Centralized loader for ``.agent-settings.yml`` with user-global fallback.
|
|
2
|
+
|
|
3
|
+
Phase 1 of road-to-portable-dev-preferences. Single source of truth for
|
|
4
|
+
how scripts read agent settings — replaces ~15 ad-hoc loaders in P3.
|
|
5
|
+
|
|
6
|
+
Resolution order (project wins, user-global fills gaps for whitelisted
|
|
7
|
+
keys only):
|
|
8
|
+
|
|
9
|
+
1. Project ``./.agent-settings.yml`` (full file, all keys)
|
|
10
|
+
2. ``~/.config/agent-config/agent-settings.yml`` (whitelist only)
|
|
11
|
+
3. Built-in defaults (currently empty)
|
|
12
|
+
|
|
13
|
+
Whitelisted keys (``MERGEABLE_KEYS``) are exact dotted paths. A
|
|
14
|
+
non-whitelisted key in the user-global file is silently ignored — the
|
|
15
|
+
``verbose=True`` flag surfaces ignored paths via ``logging.info`` for
|
|
16
|
+
debugging.
|
|
17
|
+
|
|
18
|
+
Contract — pure, read-only, tolerant:
|
|
19
|
+
|
|
20
|
+
* Lazy PyYAML import; no yaml installed → defaults returned.
|
|
21
|
+
* Missing project file → user-global + defaults.
|
|
22
|
+
* Missing user-global file → project + defaults.
|
|
23
|
+
* Both missing → defaults.
|
|
24
|
+
* Malformed YAML / unreadable file → defaults, logged at WARNING.
|
|
25
|
+
* No file is ever created or written by this module.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
DEFAULT_PROJECT_FILE = ".agent-settings.yml"
|
|
36
|
+
DEFAULT_USER_GLOBAL_FILE = (
|
|
37
|
+
Path.home() / ".config" / "agent-config" / "agent-settings.yml"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
#: Exact dotted paths allowed to cascade from user-global into the merged
|
|
41
|
+
#: settings. Anything not listed here is silently ignored when present in
|
|
42
|
+
#: the user-global file. Adding a key requires an ADR — see
|
|
43
|
+
#: ``agents/roadmaps/road-to-portable-dev-preferences.md``.
|
|
44
|
+
MERGEABLE_KEYS: tuple[str, ...] = (
|
|
45
|
+
"name",
|
|
46
|
+
"ide",
|
|
47
|
+
"cost_profile",
|
|
48
|
+
"personal.bot_icon",
|
|
49
|
+
"personal.autonomy",
|
|
50
|
+
"caveman.speak_scope",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_DEFAULTS: dict[str, Any] = {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_agent_settings(
|
|
57
|
+
project_path: Path | str | None = None,
|
|
58
|
+
user_global_path: Path | str | None = None,
|
|
59
|
+
verbose: bool = False,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Return the merged settings dict.
|
|
62
|
+
|
|
63
|
+
``project_path`` defaults to ``./.agent-settings.yml`` (CWD-relative).
|
|
64
|
+
``user_global_path`` defaults to
|
|
65
|
+
``~/.config/agent-config/agent-settings.yml``. Both arguments accept
|
|
66
|
+
``Path`` or ``str``. Pass ``verbose=True`` to log keys present in
|
|
67
|
+
user-global that are not on the whitelist.
|
|
68
|
+
"""
|
|
69
|
+
project = _read_yaml(
|
|
70
|
+
Path(project_path) if project_path else Path(DEFAULT_PROJECT_FILE),
|
|
71
|
+
) or {}
|
|
72
|
+
user_global_raw = _read_yaml(
|
|
73
|
+
Path(user_global_path) if user_global_path else DEFAULT_USER_GLOBAL_FILE,
|
|
74
|
+
) or {}
|
|
75
|
+
|
|
76
|
+
user_global_filtered, ignored = _filter_whitelist(
|
|
77
|
+
user_global_raw, MERGEABLE_KEYS,
|
|
78
|
+
)
|
|
79
|
+
if verbose and ignored:
|
|
80
|
+
logger.info(
|
|
81
|
+
"agent_settings: ignored non-whitelisted user-global keys: %s",
|
|
82
|
+
sorted(ignored),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
merged: dict[str, Any] = _deep_copy_defaults(_DEFAULTS)
|
|
86
|
+
_deep_merge(merged, user_global_filtered)
|
|
87
|
+
_deep_merge(merged, project)
|
|
88
|
+
return merged
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _read_yaml(path: Path) -> dict[str, Any] | None:
|
|
92
|
+
"""Best-effort YAML read; never raises. Returns ``None`` on any failure."""
|
|
93
|
+
if not path.is_file():
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
import yaml # type: ignore[import-untyped]
|
|
97
|
+
except ImportError:
|
|
98
|
+
return None
|
|
99
|
+
try:
|
|
100
|
+
with path.open(encoding="utf-8") as fh:
|
|
101
|
+
data = yaml.safe_load(fh) or {}
|
|
102
|
+
except (OSError, yaml.YAMLError):
|
|
103
|
+
logger.warning("agent_settings: unreadable or malformed YAML at %s", path)
|
|
104
|
+
return None
|
|
105
|
+
return data if isinstance(data, dict) else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _filter_whitelist(
|
|
109
|
+
raw: dict[str, Any], allowed: tuple[str, ...],
|
|
110
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
111
|
+
"""Return ``(filtered_dict, ignored_paths)`` from a user-global blob."""
|
|
112
|
+
filtered: dict[str, Any] = {}
|
|
113
|
+
for dotted in allowed:
|
|
114
|
+
value = _get_dotted(raw, dotted)
|
|
115
|
+
if value is not None:
|
|
116
|
+
_set_dotted(filtered, dotted, value)
|
|
117
|
+
ignored = [p for p in _leaf_paths(raw) if p not in allowed]
|
|
118
|
+
return filtered, ignored
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_dotted(data: dict[str, Any], dotted: str) -> Any:
|
|
122
|
+
cursor: Any = data
|
|
123
|
+
for part in dotted.split("."):
|
|
124
|
+
if not isinstance(cursor, dict) or part not in cursor:
|
|
125
|
+
return None
|
|
126
|
+
cursor = cursor[part]
|
|
127
|
+
return cursor
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _set_dotted(target: dict[str, Any], dotted: str, value: Any) -> None:
|
|
131
|
+
parts = dotted.split(".")
|
|
132
|
+
cursor = target
|
|
133
|
+
for part in parts[:-1]:
|
|
134
|
+
nxt = cursor.setdefault(part, {})
|
|
135
|
+
if not isinstance(nxt, dict):
|
|
136
|
+
nxt = {}
|
|
137
|
+
cursor[part] = nxt
|
|
138
|
+
cursor = nxt
|
|
139
|
+
cursor[parts[-1]] = value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _leaf_paths(data: dict[str, Any], prefix: str = "") -> list[str]:
|
|
143
|
+
paths: list[str] = []
|
|
144
|
+
for key, value in data.items():
|
|
145
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
146
|
+
if isinstance(value, dict) and value:
|
|
147
|
+
paths.extend(_leaf_paths(value, path))
|
|
148
|
+
else:
|
|
149
|
+
paths.append(path)
|
|
150
|
+
return paths
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _deep_merge(dst: dict[str, Any], src: dict[str, Any]) -> None:
|
|
154
|
+
"""Merge ``src`` into ``dst`` in-place; nested dicts are merged recursively."""
|
|
155
|
+
for key, value in src.items():
|
|
156
|
+
if (
|
|
157
|
+
isinstance(value, dict)
|
|
158
|
+
and isinstance(dst.get(key), dict)
|
|
159
|
+
):
|
|
160
|
+
_deep_merge(dst[key], value)
|
|
161
|
+
else:
|
|
162
|
+
dst[key] = value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _deep_copy_defaults(src: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
out: dict[str, Any] = {}
|
|
167
|
+
_deep_merge(out, src)
|
|
168
|
+
return out
|
|
@@ -13,6 +13,12 @@ settings.py``):
|
|
|
13
13
|
* Chat-history hooks gate on **two** switches: ``hooks.chat_history.
|
|
14
14
|
enabled`` AND the global ``chat_history.enabled``. Either off → no
|
|
15
15
|
chat-history hook registers.
|
|
16
|
+
|
|
17
|
+
Per road-to-portable-dev-preferences P3, the YAML read goes through
|
|
18
|
+
:func:`work_engine._lib.agent_settings.load_agent_settings`, which
|
|
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.
|
|
16
22
|
"""
|
|
17
23
|
from __future__ import annotations
|
|
18
24
|
|
|
@@ -20,6 +26,8 @@ from dataclasses import dataclass
|
|
|
20
26
|
from pathlib import Path
|
|
21
27
|
from typing import Any
|
|
22
28
|
|
|
29
|
+
from work_engine._lib.agent_settings import load_agent_settings
|
|
30
|
+
|
|
23
31
|
DEFAULT_SETTINGS_FILE = ".agent-settings.yml"
|
|
24
32
|
DEFAULT_CHAT_HISTORY_SCRIPT = "scripts/chat_history.py"
|
|
25
33
|
|
|
@@ -52,36 +60,27 @@ _DEFAULT = HookSettings()
|
|
|
52
60
|
|
|
53
61
|
def load_hook_settings(
|
|
54
62
|
settings_path: Path | str | None = None,
|
|
63
|
+
user_global_path: Path | str | None = None,
|
|
55
64
|
) -> HookSettings:
|
|
56
65
|
"""Return :class:`HookSettings` hydrated from ``.agent-settings.yml``.
|
|
57
66
|
|
|
58
67
|
``settings_path`` defaults to ``./.agent-settings.yml`` relative to
|
|
59
68
|
the current working directory — same convention as chat-history.
|
|
69
|
+
``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.
|
|
60
73
|
"""
|
|
61
74
|
path = Path(settings_path) if settings_path else Path(DEFAULT_SETTINGS_FILE)
|
|
62
|
-
raw =
|
|
63
|
-
|
|
75
|
+
raw = load_agent_settings(
|
|
76
|
+
project_path=path,
|
|
77
|
+
user_global_path=user_global_path,
|
|
78
|
+
)
|
|
79
|
+
if not raw:
|
|
64
80
|
return _DEFAULT
|
|
65
81
|
return _settings_from_raw(raw)
|
|
66
82
|
|
|
67
83
|
|
|
68
|
-
def _read_yaml(path: Path) -> dict[str, Any] | None:
|
|
69
|
-
if not path.is_file():
|
|
70
|
-
return None
|
|
71
|
-
try:
|
|
72
|
-
import yaml # type: ignore[import-untyped]
|
|
73
|
-
except ImportError:
|
|
74
|
-
return None
|
|
75
|
-
try:
|
|
76
|
-
with path.open(encoding="utf-8") as fh:
|
|
77
|
-
data = yaml.safe_load(fh) or {}
|
|
78
|
-
except (OSError, yaml.YAMLError):
|
|
79
|
-
return None
|
|
80
|
-
if not isinstance(data, dict):
|
|
81
|
-
return None
|
|
82
|
-
return data
|
|
83
|
-
|
|
84
|
-
|
|
85
84
|
def _settings_from_raw(data: dict[str, Any]) -> HookSettings:
|
|
86
85
|
hooks = data.get("hooks")
|
|
87
86
|
if not isinstance(hooks, dict):
|
package/AGENTS.md
CHANGED
|
@@ -16,12 +16,12 @@ task ci # full pipeline — green before PR
|
|
|
16
16
|
|
|
17
17
|
## Pointers
|
|
18
18
|
|
|
19
|
-
- **Package self-orientation** — identity, four-wing cognition map, repo layout, tech stack, key-rules table, telemetry, command-suggester: [`docs/contracts/package-self-orientation.md`](docs/contracts/package-self-orientation.md).
|
|
20
|
-
- **Kernel + Router** — 9 always-loaded Iron-Law rules, tier-1 / tier-2 routing, cost profiles, per-rule char caps enforced by `task lint-rule-budget`: [`kernel-membership`](docs/contracts/kernel-membership.md) + [`rule-router`](docs/contracts/rule-router.md).
|
|
19
|
+
- **Package self-orientation** (beta) — identity, four-wing cognition map, repo layout, tech stack, key-rules table, telemetry, command-suggester: [`docs/contracts/package-self-orientation.md`](docs/contracts/package-self-orientation.md).
|
|
20
|
+
- **Kernel + Router** (beta) — 9 always-loaded Iron-Law rules, tier-1 / tier-2 routing, cost profiles, per-rule char caps enforced by `task lint-rule-budget`: [`kernel-membership`](docs/contracts/kernel-membership.md) + [`rule-router`](docs/contracts/rule-router.md).
|
|
21
21
|
- **Multi-tool projection** — Augment, Claude Code, Cursor, Cline, Windsurf, Gemini CLI, Claude.ai bundle pipeline that ships from `.agent-src/` to consumer surfaces: [`docs/architecture.md`](docs/architecture.md#cloud-bundle-pipeline).
|
|
22
22
|
- **Editing this repo** — Iron-Law rules (portability, source-of-truth, skill-quality) and the Thin-Root contract (caps · pointer-ratio · triage block) governing AGENTS.md: [`augment-portability`](.agent-src/rules/augment-portability.md), [`augment-source-of-truth`](.agent-src/rules/augment-source-of-truth.md), [`skill-quality`](.agent-src/rules/skill-quality.md), [`agents-md-thin-root`](.agent-src/skills/agents-md-thin-root/SKILL.md).
|
|
23
|
-
- **Consumer story + architecture deep-dive** —
|
|
24
|
-
- **Personas** — 11 review-lens cast (6 core · 5 specialist), `personas:` vs `/mode` axes, citation map, override pattern: [`docs/personas.md`](docs/personas.md), schema [`docs/contracts/persona-schema.md`](docs/contracts/persona-schema.md).
|
|
23
|
+
- **Consumer story + architecture deep-dive** — install story + ship pipeline: [`README.md`](README.md), [`docs/architecture.md`](docs/architecture.md).
|
|
24
|
+
- **Personas** — 11 review-lens cast (6 core · 5 specialist), `personas:` vs `/mode` axes, citation map, override pattern: [`docs/personas.md`](docs/personas.md), schema [`docs/contracts/persona-schema.md`](docs/contracts/persona-schema.md) (beta).
|
|
25
25
|
|
|
26
26
|
## Emergency triage — read this when nothing else is reachable
|
|
27
27
|
|
package/CHANGELOG.md
CHANGED
|
@@ -318,6 +318,40 @@ our recommendation order, not its support status.
|
|
|
318
318
|
users" tension without removing any path that an existing user
|
|
319
319
|
might rely on.
|
|
320
320
|
|
|
321
|
+
## [1.39.0](https://github.com/event4u-app/agent-config/compare/1.38.0...1.39.0) (2026-05-11)
|
|
322
|
+
|
|
323
|
+
### Features
|
|
324
|
+
|
|
325
|
+
* **onboard,docs:** wire user-global DX defaults into onboarding + docs ([761a969](https://github.com/event4u-app/agent-config/commit/761a96979c42b880d83f72ccd2ac2d752ae47f0b))
|
|
326
|
+
* **settings:** add centralized agent-settings loader ([d5699be](https://github.com/event4u-app/agent-config/commit/d5699bee9ebb47dd2f1d78716e1ff84a7139b5da))
|
|
327
|
+
|
|
328
|
+
### Bug Fixes
|
|
329
|
+
|
|
330
|
+
* **ci:** grant contents:write to deploy-mcp-worker for release comment ([fb5c895](https://github.com/event4u-app/agent-config/commit/fb5c89537daef55e5dde12bfe85cc703bfa181ed))
|
|
331
|
+
|
|
332
|
+
### Documentation
|
|
333
|
+
|
|
334
|
+
* **roadmap:** add road-to-simplicity-and-everywhere (highest prio) ([417a8fa](https://github.com/event4u-app/agent-config/commit/417a8fa9b7059dac783d3bc9423c64a994a9421a))
|
|
335
|
+
* **roadmap:** add road-to-mcp-full-coverage (Discovery-First) ([fabf897](https://github.com/event4u-app/agent-config/commit/fabf89761322a437b39178366e45f68efbdd7924))
|
|
336
|
+
* **mcp-cloud:** clarify Lite-vs-Full scope at endpoint surface ([97c4684](https://github.com/event4u-app/agent-config/commit/97c4684d07be4fe49c248a1ee0a60cd36b82e071))
|
|
337
|
+
* **stability:** align public surface with stability markers ([d9afe4a](https://github.com/event4u-app/agent-config/commit/d9afe4ad67aad80e2bb12b1c707189f3ee8f47c2))
|
|
338
|
+
* **mcp:** clarify .agent-settings.yml vs. MCP client config ([27a77b2](https://github.com/event4u-app/agent-config/commit/27a77b2459cad1faff12cd8a237e59091ec687de))
|
|
339
|
+
* **mcp:** add per-client setup guide for hosted Remote MCP ([179da38](https://github.com/event4u-app/agent-config/commit/179da38631ea9693a94f259fb1edd2746b89425d))
|
|
340
|
+
|
|
341
|
+
### Refactoring
|
|
342
|
+
|
|
343
|
+
* **work-engine:** centralize agent-settings loading in shared _lib ([3c548bb](https://github.com/event4u-app/agent-config/commit/3c548bb72d3c2022362050a4c88ebe412bf7a897))
|
|
344
|
+
|
|
345
|
+
### Chores
|
|
346
|
+
|
|
347
|
+
* **compress:** regenerate commands/onboard.md after onboard,docs source edit ([e676915](https://github.com/event4u-app/agent-config/commit/e67691564405f05fba033f29b0df696e25788dd0))
|
|
348
|
+
|
|
349
|
+
### Other
|
|
350
|
+
|
|
351
|
+
* add portable-dev-preferences (3 phases, refined via AI council) ([7468a4c](https://github.com/event4u-app/agent-config/commit/7468a4cd2d352d6bfcd7797a8dd8b6ed8ec8f27a))
|
|
352
|
+
|
|
353
|
+
Tests: 2699 (+20 since 1.38.0)
|
|
354
|
+
|
|
321
355
|
## [1.38.0](https://github.com/event4u-app/agent-config/compare/1.37.0...1.38.0) (2026-05-11)
|
|
322
356
|
|
|
323
357
|
### Features
|
package/README.md
CHANGED
|
@@ -108,11 +108,22 @@ curl https://agent-config-mcp.event4u.workers.dev
|
|
|
108
108
|
# → { "ok": true, "name": "agent-config-mcp", "release_key": "v…", … }
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Read-only, identity-stable per release.
|
|
112
|
-
|
|
111
|
+
Read-only, identity-stable per release. Per-client setup snippets
|
|
112
|
+
(Claude Desktop, Claude Code, Cursor, Zed, Continue) —
|
|
113
|
+
[`docs/setup/mcp-client-config.md`](docs/setup/mcp-client-config.md).
|
|
114
|
+
URL shapes (latest vs. pinned `/v<X.Y.Z>`) —
|
|
113
115
|
[`docs/setup/mcp-cloud-endpoints.md`](docs/setup/mcp-cloud-endpoints.md).
|
|
114
116
|
Operator setup (account, R2, secrets) — [`docs/setup/mcp-cloud-setup.md`](docs/setup/mcp-cloud-setup.md).
|
|
115
|
-
Experimental — A0-cloud contract
|
|
117
|
+
Experimental — A0-cloud contract lives at `docs/contracts/mcp-cloud-scope.md` (internal reference only per `STABILITY.md`).
|
|
118
|
+
|
|
119
|
+
> **Scope — Lite, not Full.** The hosted MCP endpoint serves the
|
|
120
|
+
> read-only governance surface (skills · commands · rules · guidelines
|
|
121
|
+
> · contexts) as MCP prompts and resources. It does **not** execute any
|
|
122
|
+
> of the ~112 Python scripts that ship with the package (linters,
|
|
123
|
+
> audits, `task ci`, work-engine hooks, …). Those require the full
|
|
124
|
+
> local install per [Quickstart](#quickstart). See
|
|
125
|
+
> [`docs/contracts/mcp-cloud-scope.md`](docs/contracts/mcp-cloud-scope.md)
|
|
126
|
+
> for the execution-safety boundary.
|
|
116
127
|
|
|
117
128
|
### Optional: persistent agent memory
|
|
118
129
|
|
package/docs/customization.md
CHANGED
|
@@ -42,6 +42,51 @@ The `.agent-settings.yml` file in the consumer project configures agent behavior
|
|
|
42
42
|
It is written as YAML with section-level grouping; dotted keys below reference
|
|
43
43
|
those sections.
|
|
44
44
|
|
|
45
|
+
### User-global DX-comfort defaults (cross-project)
|
|
46
|
+
|
|
47
|
+
Six **DX-comfort** keys can be carried across every project that uses
|
|
48
|
+
`event4u/agent-config` by storing them once in a user-global file at:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
~/.config/agent-config/agent-settings.yml
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The path is XDG-style and matches the existing
|
|
55
|
+
`~/.config/agent-config/` directory used for `anthropic.key`,
|
|
56
|
+
`openai.key`, and `council-spend.jsonl`.
|
|
57
|
+
|
|
58
|
+
**Whitelist (locked, exact dotted paths)** — only these six keys are
|
|
59
|
+
mergeable from the user-global file; every other key is silently ignored:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
name
|
|
63
|
+
ide
|
|
64
|
+
cost_profile
|
|
65
|
+
personal.bot_icon
|
|
66
|
+
personal.autonomy
|
|
67
|
+
caveman.speak_scope
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Merge order** (lowest → highest precedence):
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
1. Package defaults (shipped by event4u/agent-config)
|
|
74
|
+
2. ~/.config/agent-config/agent-settings.yml (user-global · whitelist-filtered)
|
|
75
|
+
3. <project>/.agent-settings.yml (project-local · always wins)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Project-local values **always win**. The user-global file is a
|
|
79
|
+
fallback, never a lock. Non-whitelisted keys in the user-global file
|
|
80
|
+
are dropped without error — adding `personal.theme` there has no
|
|
81
|
+
effect.
|
|
82
|
+
|
|
83
|
+
The file is created **only on explicit opt-in via `/onboard`**. The
|
|
84
|
+
loader at [`scripts/_lib/agent_settings.py`](../scripts/_lib/agent_settings.py)
|
|
85
|
+
is **read-only** — no script can create or mutate it without an
|
|
86
|
+
explicit `/onboard` confirmation. Edit the file by hand for mid-life
|
|
87
|
+
changes; `/sync-agent-settings` stays project-scoped and never touches
|
|
88
|
+
user-global state.
|
|
89
|
+
|
|
45
90
|
### Available settings
|
|
46
91
|
|
|
47
92
|
| Setting | Default | Description |
|
|
@@ -1,37 +1,74 @@
|
|
|
1
1
|
# Layered Settings
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**
|
|
5
|
-
|
|
3
|
+
Three-file settings model: **team defaults** (committed),
|
|
4
|
+
**user-global DX-comfort defaults** (per-developer, cross-project), and
|
|
5
|
+
**developer overrides** (per-project, git-ignored). Lets a project pin
|
|
6
|
+
decisions, a developer carry DX preferences across every project, and
|
|
7
|
+
project-local choices always win.
|
|
6
8
|
|
|
7
|
-
Referenced by `road-to-project-memory.md` Phase 0
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
Referenced by `road-to-project-memory.md` Phase 0 and
|
|
10
|
+
`road-to-portable-dev-preferences.md`. Consumed by the centralized
|
|
11
|
+
settings loader at
|
|
12
|
+
[`scripts/_lib/agent_settings.py`](../../../scripts/_lib/agent_settings.py),
|
|
13
|
+
the `/onboard` command, and any agent that edits `.agent-settings.yml`
|
|
14
|
+
on user request.
|
|
10
15
|
|
|
11
|
-
## The
|
|
16
|
+
## The three files
|
|
12
17
|
|
|
13
18
|
| File | Git | Scope | Owner | Example values |
|
|
14
19
|
|---|---|---|---|---|
|
|
15
20
|
| `.agent-project-settings.yml` | **committed** | team / repo | lead maintainer | `project.stack`, `quality.php.tools`, `memory.dogfood` |
|
|
16
|
-
|
|
|
21
|
+
| `~/.config/agent-config/agent-settings.yml` | **n/a** (outside repo) | individual developer · cross-project | individual | `name`, `ide`, `cost_profile`, `personal.bot_icon`, `personal.autonomy`, `caveman.speak_scope` |
|
|
22
|
+
| `.agent-settings.yml` | **gitignored** | individual developer · this project | individual | `personal.ide`, `personal.user_name`, `subagents.max_parallel`, `onboarding.onboarded` |
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
(
|
|
24
|
+
All three are YAML. Schemas:
|
|
25
|
+
|
|
26
|
+
- Developer (project-local): [`agent-settings.md`](../../templates/agent-settings.md).
|
|
27
|
+
- Team: [`agent-project-settings.example.yml`](../../templates/agents/agent-project-settings.example.yml).
|
|
28
|
+
- User-global: six exact dotted paths — whitelist in
|
|
29
|
+
[`scripts/_lib/agent_settings.py`](../../../scripts/_lib/agent_settings.py).
|
|
22
30
|
|
|
23
31
|
## Merge order
|
|
24
32
|
|
|
25
33
|
Lowest priority → highest priority:
|
|
26
34
|
|
|
27
35
|
```
|
|
28
|
-
1. Package defaults
|
|
29
|
-
2.
|
|
30
|
-
3. .agent-settings.yml
|
|
36
|
+
1. Package defaults (shipped by event4u/agent-config)
|
|
37
|
+
2. ~/.config/agent-config/agent-settings.yml (user-global · whitelist-filtered)
|
|
38
|
+
3. .agent-project-settings.yml (team file, committed)
|
|
39
|
+
4. .agent-settings.yml (developer file, gitignored)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Keys from higher layers win unless a lower layer marks them
|
|
43
|
+
`locked` (team file only). The user-global file does **not** support
|
|
44
|
+
`locked` — its purpose is cross-project comfort, never policy.
|
|
45
|
+
|
|
46
|
+
## User-global whitelist
|
|
47
|
+
|
|
48
|
+
Only these exact dotted paths are mergeable from the user-global file;
|
|
49
|
+
every other key is silently dropped by the loader. The whitelist is
|
|
50
|
+
intentionally tiny — adding a key requires an ADR.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
name
|
|
54
|
+
ide
|
|
55
|
+
cost_profile
|
|
56
|
+
personal.bot_icon
|
|
57
|
+
personal.autonomy
|
|
58
|
+
caveman.speak_scope
|
|
31
59
|
```
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
Loader contract:
|
|
62
|
+
|
|
63
|
+
- **Read-only.** The loader never creates, modifies, or deletes the
|
|
64
|
+
user-global file. Writes are the exclusive responsibility of the
|
|
65
|
+
`/onboard` command on explicit user opt-in.
|
|
66
|
+
- **Tolerant.** Missing file, malformed YAML, empty file — all fall
|
|
67
|
+
back to the next tier without raising.
|
|
68
|
+
- **Silent on out-of-whitelist keys.** `verbose=True` logs which keys
|
|
69
|
+
were dropped for debugging; default mode is silent.
|
|
70
|
+
- **Never auto-creates `~/.config/agent-config/`.** That directory
|
|
71
|
+
pre-exists from key installation; `/onboard` `mkdir -p`s on opt-in.
|
|
35
72
|
|
|
36
73
|
## Lock semantics
|
|
37
74
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# MCP Client Config — Remote agent-config
|
|
2
|
+
|
|
3
|
+
Connect any MCP-capable client to the hosted `agent-config-mcp` Worker
|
|
4
|
+
at `https://agent-config-mcp.event4u.workers.dev`. Read-only,
|
|
5
|
+
identity-stable per release, no auth.
|
|
6
|
+
|
|
7
|
+
For URL shapes (latest vs. pinned `/v<X.Y.Z>`) see
|
|
8
|
+
[`mcp-cloud-endpoints.md`](mcp-cloud-endpoints.md). For operator
|
|
9
|
+
setup of your own deployment see [`mcp-cloud-setup.md`](mcp-cloud-setup.md).
|
|
10
|
+
|
|
11
|
+
## Transport note
|
|
12
|
+
|
|
13
|
+
The Worker speaks JSON-RPC over HTTP POST. Clients that support
|
|
14
|
+
HTTP transport natively (Claude Code, Cursor) talk to it directly.
|
|
15
|
+
Clients that only support stdio (Claude Desktop, Zed) need the
|
|
16
|
+
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge from
|
|
17
|
+
npm — invoked via `npx`, no install required.
|
|
18
|
+
|
|
19
|
+
## Where settings live — `.agent-settings.yml` vs. MCP config
|
|
20
|
+
|
|
21
|
+
These are **two different files** for two different layers. Don't
|
|
22
|
+
look for MCP server config inside `.agent-settings.yml`.
|
|
23
|
+
|
|
24
|
+
| File | Where | Who reads it | Purpose |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| MCP client config (this page) | client-specific path per section above | the MCP client at startup | which MCP servers to talk to (name + URL / command) |
|
|
27
|
+
| `.agent-settings.yml` | consumer project root (`<repo>/.agent-settings.yml`) | the agent at runtime (Claude / Cursor / …) | per-developer preferences: `name`, `ide`, `cost_profile`, `personal.autonomy`, `pipelines.skill_improvement`, `caveman.speak_scope`, … |
|
|
28
|
+
|
|
29
|
+
The hosted MCP is **stateless** and **project-agnostic** — it serves
|
|
30
|
+
the same skill / rule / command catalog to every client. Personalization
|
|
31
|
+
happens agent-side after the MCP delivers its content blob; the Worker
|
|
32
|
+
itself does not read `.agent-settings.yml`.
|
|
33
|
+
|
|
34
|
+
First-time setup of `.agent-settings.yml` runs through `/onboard`;
|
|
35
|
+
template drift is handled by `/sync-agent-settings`.
|
|
36
|
+
|
|
37
|
+
## Claude Desktop
|
|
38
|
+
|
|
39
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
40
|
+
(macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"agent-config": {
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "mcp-remote", "https://agent-config-mcp.event4u.workers.dev"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Restart Claude Desktop. The connector appears in the dropdown.
|
|
54
|
+
|
|
55
|
+
Newer builds also support **Settings → Connectors → Add Custom
|
|
56
|
+
Connector** with the URL directly — no `mcp-remote` wrapper needed.
|
|
57
|
+
|
|
58
|
+
## Claude Code
|
|
59
|
+
|
|
60
|
+
Native HTTP transport — one command:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
claude mcp add --transport http agent-config https://agent-config-mcp.event4u.workers.dev
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Verify:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp list
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Cursor
|
|
73
|
+
|
|
74
|
+
`~/.cursor/mcp.json` (global) or `<repo>/.cursor/mcp.json`
|
|
75
|
+
(project-local):
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"agent-config": {
|
|
81
|
+
"url": "https://agent-config-mcp.event4u.workers.dev"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Reload Cursor (`Cmd+Shift+P → Reload Window`).
|
|
88
|
+
|
|
89
|
+
## Zed
|
|
90
|
+
|
|
91
|
+
`~/.config/zed/settings.json`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"context_servers": {
|
|
96
|
+
"agent-config": {
|
|
97
|
+
"source": "custom",
|
|
98
|
+
"command": "npx",
|
|
99
|
+
"args": ["-y", "mcp-remote", "https://agent-config-mcp.event4u.workers.dev"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Zed has no native remote-MCP transport yet, so the `mcp-remote`
|
|
106
|
+
bridge is required.
|
|
107
|
+
|
|
108
|
+
## Continue
|
|
109
|
+
|
|
110
|
+
`~/.continue/config.yaml` (or `<repo>/.continue/config.yaml`):
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
mcpServers:
|
|
114
|
+
- name: agent-config
|
|
115
|
+
command: npx
|
|
116
|
+
args: ["-y", "mcp-remote", "https://agent-config-mcp.event4u.workers.dev"]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Verify
|
|
120
|
+
|
|
121
|
+
After reload, every client should:
|
|
122
|
+
|
|
123
|
+
1. List `agent-config` under connectors / tools with a release-key
|
|
124
|
+
matching the live deploy. Probe the endpoint to see the current
|
|
125
|
+
release:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
curl https://agent-config-mcp.event4u.workers.dev
|
|
129
|
+
# → { "ok": true, "release_key": "v…", "signature": "…", … }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
2. Expose skill + command prompts under URIs like `skill://…` and
|
|
133
|
+
`command://…`.
|
|
134
|
+
3. Expose rule + guideline + context resources under `rule://…`,
|
|
135
|
+
`guideline://…`, `context://…`.
|
|
136
|
+
|
|
137
|
+
If the client shows the connector but no prompts / resources,
|
|
138
|
+
re-probe the URL — a 5xx from the Worker indicates the deploy is
|
|
139
|
+
mid-roll, retry after a minute.
|
|
140
|
+
|
|
141
|
+
## Local stdio alternative
|
|
142
|
+
|
|
143
|
+
If you have the repo cloned and prefer running the MCP server
|
|
144
|
+
locally (faster startup, no network), the stdio kernel under
|
|
145
|
+
`scripts/mcp_server/` is the same wire surface. Setup:
|
|
146
|
+
`task mcp:setup`, run details in [`../mcp-server.md`](../mcp-server.md).
|
|
147
|
+
|
|
148
|
+
## See also
|
|
149
|
+
|
|
150
|
+
- URL shapes & DNS — [`mcp-cloud-endpoints.md`](mcp-cloud-endpoints.md)
|
|
151
|
+
- Operator setup (your own deploy) — [`mcp-cloud-setup.md`](mcp-cloud-setup.md)
|
|
152
|
+
- A0-cloud contract — [`../contracts/mcp-cloud-scope.md`](../contracts/mcp-cloud-scope.md)
|
|
@@ -10,6 +10,21 @@ by `docs/contracts/mcp-cloud-scope.md` (A0-cloud) and Phase 5.2 of
|
|
|
10
10
|
shapes below are pinned for the lifetime of the *experimental* window;
|
|
11
11
|
breaking changes require a stability-label bump.
|
|
12
12
|
|
|
13
|
+
## Scope — Lite surface
|
|
14
|
+
|
|
15
|
+
The hosted endpoint exposes the **read-only governance surface**
|
|
16
|
+
(skills, commands, rules, guidelines, contexts) as MCP prompts +
|
|
17
|
+
resources. `tools/list` returns two **deprecated stubs**
|
|
18
|
+
(`lint_skills`, `chat_history_append`) that point at their local-stdio
|
|
19
|
+
successors; `tools/call` against either returns `isError=true`. No
|
|
20
|
+
script execution, no FS access, no shell.
|
|
21
|
+
|
|
22
|
+
Full power — the ~112 Python scripts (linters, audits, `task ci`,
|
|
23
|
+
work-engine hooks) — requires the local install. See
|
|
24
|
+
[`../contracts/mcp-cloud-scope.md`](../contracts/mcp-cloud-scope.md)
|
|
25
|
+
for the execution-safety boundary and the Phase-7-DEFERRED gate that
|
|
26
|
+
governs any future tool restoration.
|
|
27
|
+
|
|
13
28
|
## URL shapes (pinned)
|
|
14
29
|
|
|
15
30
|
Two surfaces, both serve identical wire contracts (JSON-RPC over POST,
|
|
@@ -88,6 +103,7 @@ serving on `/latest/`.
|
|
|
88
103
|
|
|
89
104
|
## See also
|
|
90
105
|
|
|
106
|
+
- Per-client config snippets: [`mcp-client-config.md`](mcp-client-config.md)
|
|
91
107
|
- A0-cloud contract: `docs/contracts/mcp-cloud-scope.md`
|
|
92
108
|
- R2 bootstrap: `docs/setup/mcp-r2-bootstrap.md`
|
|
93
109
|
- Local stdio fallback: `scripts/mcp_server/` (unchanged)
|
package/package.json
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Centralized loader for ``.agent-settings.yml`` with user-global fallback.
|
|
2
|
+
|
|
3
|
+
Phase 1 of road-to-portable-dev-preferences. Single source of truth for
|
|
4
|
+
how scripts read agent settings — replaces ~15 ad-hoc loaders in P3.
|
|
5
|
+
|
|
6
|
+
Resolution order (project wins, user-global fills gaps for whitelisted
|
|
7
|
+
keys only):
|
|
8
|
+
|
|
9
|
+
1. Project ``./.agent-settings.yml`` (full file, all keys)
|
|
10
|
+
2. ``~/.config/agent-config/agent-settings.yml`` (whitelist only)
|
|
11
|
+
3. Built-in defaults (currently empty)
|
|
12
|
+
|
|
13
|
+
Whitelisted keys (``MERGEABLE_KEYS``) are exact dotted paths. A
|
|
14
|
+
non-whitelisted key in the user-global file is silently ignored — the
|
|
15
|
+
``verbose=True`` flag surfaces ignored paths via ``logging.info`` for
|
|
16
|
+
debugging.
|
|
17
|
+
|
|
18
|
+
Contract — pure, read-only, tolerant:
|
|
19
|
+
|
|
20
|
+
* Lazy PyYAML import; no yaml installed → defaults returned.
|
|
21
|
+
* Missing project file → user-global + defaults.
|
|
22
|
+
* Missing user-global file → project + defaults.
|
|
23
|
+
* Both missing → defaults.
|
|
24
|
+
* Malformed YAML / unreadable file → defaults, logged at WARNING.
|
|
25
|
+
* No file is ever created or written by this module.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
DEFAULT_PROJECT_FILE = ".agent-settings.yml"
|
|
36
|
+
DEFAULT_USER_GLOBAL_FILE = (
|
|
37
|
+
Path.home() / ".config" / "agent-config" / "agent-settings.yml"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
#: Exact dotted paths allowed to cascade from user-global into the merged
|
|
41
|
+
#: settings. Anything not listed here is silently ignored when present in
|
|
42
|
+
#: the user-global file. Adding a key requires an ADR — see
|
|
43
|
+
#: ``agents/roadmaps/road-to-portable-dev-preferences.md``.
|
|
44
|
+
MERGEABLE_KEYS: tuple[str, ...] = (
|
|
45
|
+
"name",
|
|
46
|
+
"ide",
|
|
47
|
+
"cost_profile",
|
|
48
|
+
"personal.bot_icon",
|
|
49
|
+
"personal.autonomy",
|
|
50
|
+
"caveman.speak_scope",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_DEFAULTS: dict[str, Any] = {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_agent_settings(
|
|
57
|
+
project_path: Path | str | None = None,
|
|
58
|
+
user_global_path: Path | str | None = None,
|
|
59
|
+
verbose: bool = False,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Return the merged settings dict.
|
|
62
|
+
|
|
63
|
+
``project_path`` defaults to ``./.agent-settings.yml`` (CWD-relative).
|
|
64
|
+
``user_global_path`` defaults to
|
|
65
|
+
``~/.config/agent-config/agent-settings.yml``. Both arguments accept
|
|
66
|
+
``Path`` or ``str``. Pass ``verbose=True`` to log keys present in
|
|
67
|
+
user-global that are not on the whitelist.
|
|
68
|
+
"""
|
|
69
|
+
project = _read_yaml(
|
|
70
|
+
Path(project_path) if project_path else Path(DEFAULT_PROJECT_FILE),
|
|
71
|
+
) or {}
|
|
72
|
+
user_global_raw = _read_yaml(
|
|
73
|
+
Path(user_global_path) if user_global_path else DEFAULT_USER_GLOBAL_FILE,
|
|
74
|
+
) or {}
|
|
75
|
+
|
|
76
|
+
user_global_filtered, ignored = _filter_whitelist(
|
|
77
|
+
user_global_raw, MERGEABLE_KEYS,
|
|
78
|
+
)
|
|
79
|
+
if verbose and ignored:
|
|
80
|
+
logger.info(
|
|
81
|
+
"agent_settings: ignored non-whitelisted user-global keys: %s",
|
|
82
|
+
sorted(ignored),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
merged: dict[str, Any] = _deep_copy_defaults(_DEFAULTS)
|
|
86
|
+
_deep_merge(merged, user_global_filtered)
|
|
87
|
+
_deep_merge(merged, project)
|
|
88
|
+
return merged
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _read_yaml(path: Path) -> dict[str, Any] | None:
|
|
92
|
+
"""Best-effort YAML read; never raises. Returns ``None`` on any failure."""
|
|
93
|
+
if not path.is_file():
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
import yaml # type: ignore[import-untyped]
|
|
97
|
+
except ImportError:
|
|
98
|
+
return None
|
|
99
|
+
try:
|
|
100
|
+
with path.open(encoding="utf-8") as fh:
|
|
101
|
+
data = yaml.safe_load(fh) or {}
|
|
102
|
+
except (OSError, yaml.YAMLError):
|
|
103
|
+
logger.warning("agent_settings: unreadable or malformed YAML at %s", path)
|
|
104
|
+
return None
|
|
105
|
+
return data if isinstance(data, dict) else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _filter_whitelist(
|
|
109
|
+
raw: dict[str, Any], allowed: tuple[str, ...],
|
|
110
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
111
|
+
"""Return ``(filtered_dict, ignored_paths)`` from a user-global blob."""
|
|
112
|
+
filtered: dict[str, Any] = {}
|
|
113
|
+
for dotted in allowed:
|
|
114
|
+
value = _get_dotted(raw, dotted)
|
|
115
|
+
if value is not None:
|
|
116
|
+
_set_dotted(filtered, dotted, value)
|
|
117
|
+
ignored = [p for p in _leaf_paths(raw) if p not in allowed]
|
|
118
|
+
return filtered, ignored
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_dotted(data: dict[str, Any], dotted: str) -> Any:
|
|
122
|
+
cursor: Any = data
|
|
123
|
+
for part in dotted.split("."):
|
|
124
|
+
if not isinstance(cursor, dict) or part not in cursor:
|
|
125
|
+
return None
|
|
126
|
+
cursor = cursor[part]
|
|
127
|
+
return cursor
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _set_dotted(target: dict[str, Any], dotted: str, value: Any) -> None:
|
|
131
|
+
parts = dotted.split(".")
|
|
132
|
+
cursor = target
|
|
133
|
+
for part in parts[:-1]:
|
|
134
|
+
nxt = cursor.setdefault(part, {})
|
|
135
|
+
if not isinstance(nxt, dict):
|
|
136
|
+
nxt = {}
|
|
137
|
+
cursor[part] = nxt
|
|
138
|
+
cursor = nxt
|
|
139
|
+
cursor[parts[-1]] = value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _leaf_paths(data: dict[str, Any], prefix: str = "") -> list[str]:
|
|
143
|
+
paths: list[str] = []
|
|
144
|
+
for key, value in data.items():
|
|
145
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
146
|
+
if isinstance(value, dict) and value:
|
|
147
|
+
paths.extend(_leaf_paths(value, path))
|
|
148
|
+
else:
|
|
149
|
+
paths.append(path)
|
|
150
|
+
return paths
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _deep_merge(dst: dict[str, Any], src: dict[str, Any]) -> None:
|
|
154
|
+
"""Merge ``src`` into ``dst`` in-place; nested dicts are merged recursively."""
|
|
155
|
+
for key, value in src.items():
|
|
156
|
+
if (
|
|
157
|
+
isinstance(value, dict)
|
|
158
|
+
and isinstance(dst.get(key), dict)
|
|
159
|
+
):
|
|
160
|
+
_deep_merge(dst[key], value)
|
|
161
|
+
else:
|
|
162
|
+
dst[key] = value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _deep_copy_defaults(src: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
out: dict[str, Any] = {}
|
|
167
|
+
_deep_merge(out, src)
|
|
168
|
+
return out
|