@event4u/agent-config 1.37.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.
@@ -12,35 +12,68 @@ suggestion:
12
12
 
13
13
  # /onboard
14
14
 
15
- Centralized first-run flow. Bundles what used to be scattered "ask once"
16
- prompts (user_name, IDE, rtk install, cost profile, learning loop) into a
17
- single interactive setup. Ends by setting `onboarding.onboarded: true` in
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 the [`onboarding-gate`](../rules/onboarding-gate.md) rule when
21
- `onboarding.onboarded` is `false` or by the user explicitly re-running it.
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 the agent to change it, or edit
27
- `.agent-settings.yml` directly. The agent follows the merge rules in
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 the user to run
33
- `scripts/install` (or `python3 scripts/install.py`) first and stop — this
34
- command assumes the file and its template-derived defaults are in place.
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
- Keep it short. One line explaining this is the one-time setup, six
41
- questions, one at a time, following the iron law (`user-interaction`).
38
+ One line: one-time setup, six questions, one at a time (iron law from
39
+ `user-interaction`).
42
40
 
43
- ### 2. Capture `personal.user_name`
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 answer → write to `personal.user_name`. `2` → leave empty.
87
+ Free-text → write to `personal.user_name`. `2` → leave empty.
55
88
 
56
- ### 3. Capture `personal.ide` (with auto-detect)
89
+ ### 4. Capture `personal.ide` (with auto-detect)
57
90
 
58
- Skip if already set. Otherwise auto-detect first:
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 is set, also ask about `personal.open_edited_files` (`true`/`false`).
109
+ If IDE set, also ask `personal.open_edited_files` (`true`/`false`).
77
110
 
78
- ### 4. Capture `personal.pr_comment_bot_icon`
111
+ ### 5. Capture `personal.pr_comment_bot_icon`
79
112
 
80
- Personal preference — each developer decides how their own PR replies
81
- should look. Skip only if the user has already set a non-default value
82
- deliberately (agent can't tell, so always ask on first run):
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
- ### 5. Detect `personal.rtk_installed`
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 the
112
- [`rtk-output-filtering`](../skills/rtk-output-filtering/SKILL.md) skill.
113
- `3` → leave `rtk_installed: false` and move on. No "ask again tomorrow"
114
- logic — `/onboard` is one-shot.
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
- ### 6. Confirm `cost_profile` and learning loop
150
+ ### 7. Confirm `cost_profile` and learning loop
117
151
 
118
152
  Read current `cost_profile` and `pipelines.skill_improvement` values.
119
- Present them plainly (they already have sensible defaults from the
120
- template — `minimal` + `skill_improvement: true`):
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 the toggle.
165
+ `2` → defer to `/set-cost-profile` and return here. `3` → flip toggle.
132
166
 
133
- ### 7. Mark onboarded
167
+ ### 8. Mark onboarded
134
168
 
135
- Write `onboarding.onboarded: true` to `.agent-settings.yml` using the
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 the changed fields).
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
- ### 8. Summary
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
- ### 9. Maintainer-only feature pointer
234
+ ### 11. Maintainer-only feature pointer
162
235
 
163
- Print a one-screen hint after the summary — no question, no prompt, just a
164
- pointer for maintainers who want to opt into the artefact-engagement
165
- telemetry layer. Consumers can ignore it; the feature is **default-off**
166
- and stays off unless explicitly enabled.
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. This command never commits.
185
- - One question per turn. The iron law from `ask-when-uncertain` applies;
186
- do not stack questions 2–6 into a single prompt.
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 the values the user confirms.
189
- - Never overwrite a non-empty value without asking (applies to `user_name`
190
- and `ide`).
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
- there is no `.agent-settings.yml` to write, no `onboarding.onboarded` key to
196
- flip, and no local IDE/rtk environment to capture. First-run setup is a
197
- local-agent concern; the cloud agent should proceed without invoking it.
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. This file (.agent-project-settings.yml)team defaults
10
- # 3. .agent-settings.yml developer overrides (gitignored)
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 = _read_yaml(path)
63
- if raw is None:
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):
@@ -6,7 +6,7 @@
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": "1.37.0"
9
+ "version": "1.39.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
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** — what the package does for installers and how it ships: [`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).
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,70 @@ 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
+
355
+ ## [1.38.0](https://github.com/event4u-app/agent-config/compare/1.37.0...1.38.0) (2026-05-11)
356
+
357
+ ### Features
358
+
359
+ * **mcp:** add cloud setup tasks + operator README ([c5aebba](https://github.com/event4u-app/agent-config/commit/c5aebba37a608d0a41b1586acacdb055996a961d))
360
+ * **scripts:** MCP content packer + cloud parity smoke ([e0d132a](https://github.com/event4u-app/agent-config/commit/e0d132afb487b720386bd54b68e10f1a342d6b0c))
361
+
362
+ ### Documentation
363
+
364
+ * **readme:** surface hosted Remote MCP as zero-install option ([de6161e](https://github.com/event4u-app/agent-config/commit/de6161e3660b81a460f057bc46c1b75e6fe8693c))
365
+ * **mcp:** add A0-cloud invariant 8 — ingress protection via edge cache + platform rate-limit ([c4b9371](https://github.com/event4u-app/agent-config/commit/c4b9371da2db55958bca29a7d10291626d13782f))
366
+ * **mcp:** drop archived-roadmap refs from stable contracts ([15418de](https://github.com/event4u-app/agent-config/commit/15418de5a65c3cd5968bbeecc20ed5ad5c4a4958))
367
+ * **mcp:** surface experimental hosted-MCP channel ([cecae08](https://github.com/event4u-app/agent-config/commit/cecae08ad647c7e28931e7328047fdef5ca3d3a3))
368
+ * **setup:** MCP cloud endpoints, R2 bootstrap, registry listing ([7cb3341](https://github.com/event4u-app/agent-config/commit/7cb334110ff940d3703ada013ef5163c899d96b2))
369
+ * **mcp:** add A0-cloud contract + cross-links (Phase 1 of cloudflare-mcp-hosting) ([2fc5084](https://github.com/event4u-app/agent-config/commit/2fc50845c3b194e861b54f82c852ba4bb9fc406a))
370
+ * **roadmap:** add Cloudflare-hosted MCP roadmap, archive distribution ([fd1c437](https://github.com/event4u-app/agent-config/commit/fd1c437ea8a918eebc8604ee7958d8c842c7aac8))
371
+
372
+ ### CI
373
+
374
+ * **deploy-mcp-worker:** release-tag triggered Worker deploy ([4297514](https://github.com/event4u-app/agent-config/commit/4297514b80457852511ad90674f4d633a75a92bf))
375
+
376
+ ### Chores
377
+
378
+ * **linter:** raise README overloaded threshold to 750 lines ([1e3fbb7](https://github.com/event4u-app/agent-config/commit/1e3fbb7c649398ba56bc32677c7b588131f7e949))
379
+ * **mcp:** mark dev content.json stub with explanatory _comment ([8f6c7ff](https://github.com/event4u-app/agent-config/commit/8f6c7ffe493b72ccb2d0ec66121a9d32d0b2fe8c))
380
+ * **roadmap:** archive road-to-cloudflare-mcp-hosting (100% complete) ([99e07f9](https://github.com/event4u-app/agent-config/commit/99e07f92293e62841ff87f315e41c5ceabd7b277))
381
+ * **workers/mcp:** scaffold TypeScript Cloudflare Worker ([447e071](https://github.com/event4u-app/agent-config/commit/447e071984f86fc084058d05f22d0e0a7c936a5e))
382
+
383
+ Tests: 2679 (+0 since 1.37.0)
384
+
321
385
  ## [1.37.0](https://github.com/event4u-app/agent-config/compare/1.36.1...1.37.0) (2026-05-10)
322
386
 
323
387
  ### Features