@event4u/agent-config 5.6.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.agent-src/commands/cost-report.md +12 -7
  2. package/.agent-src/commands/prediction-pool.md +215 -0
  3. package/.agent-src/commands/set-cost-profile.md +8 -8
  4. package/.agent-src/commands/sync-agent-settings.md +2 -2
  5. package/.agent-src/presets/README.md +1 -1
  6. package/.agent-src/profiles/README.md +1 -1
  7. package/.agent-src/rules/non-destructive-by-default.md +2 -1
  8. package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +196 -0
  9. package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +18 -0
  10. package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +80 -0
  11. package/.agent-src/templates/agent-settings.md +7 -7
  12. package/.agent-src/templates/agents/agent-project-settings.example.yml +2 -2
  13. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +2 -1
  14. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +1 -1
  15. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +9 -7
  16. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +9 -10
  17. package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +17 -4
  18. package/.claude-plugin/marketplace.json +3 -1
  19. package/CHANGELOG.md +48 -0
  20. package/README.md +2 -2
  21. package/config/agent-settings.template.yml +11 -2
  22. package/config/discovery/packs.yml +11 -0
  23. package/config/discovery/workspaces.yml +1 -1
  24. package/config/profiles/balanced.ini +1 -1
  25. package/config/profiles/full.ini +1 -1
  26. package/config/profiles/minimal.ini +1 -1
  27. package/dist/discovery/deprecation-report.md +1 -1
  28. package/dist/discovery/discovery-manifest.json +80 -14
  29. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  30. package/dist/discovery/discovery-manifest.summary.md +3 -2
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +34 -3
  33. package/dist/discovery/trust-report.md +2 -2
  34. package/dist/discovery/workspaces.json +13 -4
  35. package/dist/mcp/registry-manifest.json +2 -2
  36. package/dist/server/io/substituteTemplate.js +3 -3
  37. package/dist/server/io/substituteTemplate.js.map +1 -1
  38. package/dist/server/routes/settings.js +2 -2
  39. package/dist/server/routes/settings.js.map +1 -1
  40. package/dist/server/schemas/settings.js +4 -2
  41. package/dist/server/schemas/settings.js.map +1 -1
  42. package/dist/ui/assets/{index-DVsyUMZe.js → index-5lFqAKL0.js} +2 -2
  43. package/dist/ui/assets/index-5lFqAKL0.js.map +1 -0
  44. package/dist/ui/index.html +1 -1
  45. package/docs/architecture/current-onboard-baseline.md +3 -3
  46. package/docs/architecture.md +2 -2
  47. package/docs/catalog.md +7 -5
  48. package/docs/contracts/adr-level-6-productization.md +1 -1
  49. package/docs/contracts/config-presets.md +2 -2
  50. package/docs/contracts/cost-profile-defaults.md +5 -5
  51. package/docs/contracts/discovery-manifest.schema.json +1 -1
  52. package/docs/contracts/explain-trace.schema.json +3 -3
  53. package/docs/contracts/memory-visibility-v1.md +15 -7
  54. package/docs/contracts/profile-system.md +2 -2
  55. package/docs/contracts/settings-api.md +3 -3
  56. package/docs/contracts/value-report-schema.md +14 -1
  57. package/docs/customization.md +21 -5
  58. package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +11 -11
  59. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +16 -2
  60. package/docs/decisions/ADR-034-per-skill-model-recommendation-transport.md +1 -1
  61. package/docs/decisions/ADR-036-global-install-browser-wizard-handoff.md +106 -0
  62. package/docs/decisions/ADR-037-cost-profile-untangle.md +117 -0
  63. package/docs/decisions/ADR-rule-kernel-and-router.md +1 -1
  64. package/docs/decisions/INDEX.md +2 -0
  65. package/docs/getting-started.md +2 -2
  66. package/docs/guidelines/agent-infra/layered-settings.md +2 -2
  67. package/docs/installation.md +3 -3
  68. package/docs/setup/mcp-client-config.md +1 -1
  69. package/docs/value.md +9 -7
  70. package/docs/wizard.md +1 -1
  71. package/package.json +1 -1
  72. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  73. package/scripts/_cli/cmd_explain.py +1 -1
  74. package/scripts/_cli/explain_last/inputs.py +11 -8
  75. package/scripts/_cli/explain_last/sections/inputs.py +1 -1
  76. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  77. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  78. package/scripts/_lib/agent_settings.py +2 -1
  79. package/scripts/_lib/value_ladder.py +99 -2
  80. package/scripts/_lib/value_report.py +30 -16
  81. package/scripts/ai_council/modes.py +1 -1
  82. package/scripts/audit_initial_context.py +16 -0
  83. package/scripts/check_skill_requires.py +143 -0
  84. package/scripts/condense.py +13 -2
  85. package/scripts/first-run.sh +11 -11
  86. package/scripts/install +14 -1
  87. package/scripts/install.py +127 -428
  88. package/scripts/install_anthropic_key.sh +1 -1
  89. package/scripts/install_openai_key.sh +1 -1
  90. package/scripts/lint_discovery_vocabulary.py +5 -5
  91. package/scripts/lint_value_dashboard.py +1 -1
  92. package/scripts/prediction-pool/adapters/_schema.md +42 -0
  93. package/scripts/prediction-pool/adapters/kicktipp.yml +23 -0
  94. package/scripts/prediction-pool/poisson_sim.py +167 -0
  95. package/scripts/render_value_md.py +1 -0
  96. package/scripts/schemas/agent-settings.schema.json +77 -0
  97. package/scripts/schemas/skill.schema.json +7 -0
  98. package/scripts/smoke_quickstart.py +4 -4
  99. package/scripts/sync_agent_settings.py +4 -2
  100. package/scripts/validate_agent_settings.py +120 -0
  101. package/templates/minimal/.agent-settings.yml +1 -1
  102. package/dist/ui/assets/index-DVsyUMZe.js.map +0 -1
@@ -104,5 +104,5 @@ echo " Remove: rm ${TARGET_FILE}"
104
104
  echo
105
105
  echo "ℹ️ Key install ≠ enable. To use this provider in /council:"
106
106
  echo " set ai_council.enabled: true and ai_council.members.anthropic.enabled: true"
107
- echo " in .agent-settings.yml. Council is a 'full' cost_profile feature; under"
107
+ echo " in .agent-settings.yml. Council is a 'full' rule_loading_tier feature; under"
108
108
  echo " 'minimal' / 'balanced' the runtime hooks stay inactive."
@@ -104,5 +104,5 @@ echo " Remove: rm ${TARGET_FILE}"
104
104
  echo
105
105
  echo "ℹ️ Key install ≠ enable. To use this provider in /council:"
106
106
  echo " set ai_council.enabled: true and ai_council.members.openai.enabled: true"
107
- echo " in .agent-settings.yml. Council is a 'full' cost_profile feature; under"
107
+ echo " in .agent-settings.yml. Council is a 'full' rule_loading_tier feature; under"
108
108
  echo " 'minimal' / 'balanced' the runtime hooks stay inactive."
@@ -10,7 +10,7 @@ every pack listed as a workspace's default/optional MUST list that
10
10
  workspace in its own `workspaces:` array (and vice versa).
11
11
 
12
12
  Non-overlap (ADR-010 alignment): no pack id may collide with a
13
- `cost_profile` value (minimal/balanced/full/custom) or a `profile.id`
13
+ `rule_loading_tier` value (minimal/balanced/full/custom) or a `profile.id`
14
14
  value (founder/developer/content_creator/agency/finance/ops).
15
15
 
16
16
  Cap: ≤ 150 LOC, stdlib + PyYAML. Exit 0 clean, 1 on failure.
@@ -41,11 +41,11 @@ ADR_PACKS: frozenset[str] = frozenset({
41
41
  "typescript", "react", "nextjs", "python", "product-basic",
42
42
  "product-discovery", "finance-basic", "finance-advanced",
43
43
  "gtm-sales", "gtm-marketing", "ops-people", "founder-strategy", "small-business",
44
- "construction", "ai-video", "meta",
44
+ "construction", "ai-video", "fun", "meta",
45
45
  })
46
46
 
47
47
  # ADR-010 non-overlap reservations.
48
- COST_PROFILE_RESERVED: frozenset[str] = frozenset({
48
+ RULE_LOADING_TIER_RESERVED: frozenset[str] = frozenset({
49
49
  "minimal", "balanced", "full", "custom",
50
50
  })
51
51
  PROFILE_ID_RESERVED: frozenset[str] = frozenset({
@@ -136,9 +136,9 @@ def lint(quiet: bool) -> int:
136
136
  )
137
137
 
138
138
  # 5. Non-overlap (ADR-010).
139
- overlap_cost = pack_ids & COST_PROFILE_RESERVED
139
+ overlap_cost = pack_ids & RULE_LOADING_TIER_RESERVED
140
140
  if overlap_cost:
141
- errors.append(f"pack ids collide with cost_profile values: {sorted(overlap_cost)}")
141
+ errors.append(f"pack ids collide with rule_loading_tier values: {sorted(overlap_cost)}")
142
142
  overlap_profile = pack_ids & PROFILE_ID_RESERVED
143
143
  if overlap_profile:
144
144
  errors.append(f"pack ids collide with profile.id values: {sorted(overlap_profile)}")
@@ -48,7 +48,7 @@ REQUIRED_SECTIONS = (
48
48
  "**NETTO",
49
49
  )
50
50
 
51
- CANONICAL_RUNG_IDS = ("baseline", "load", "condense", "rtk", "terse")
51
+ CANONICAL_RUNG_IDS = ("baseline", "load", "thin", "condense", "rtk", "terse")
52
52
 
53
53
 
54
54
  def _log(msg: str, quiet: bool, *, err: bool = False) -> None:
@@ -0,0 +1,42 @@
1
+ # Tippspiel adapter contract — declarative selector maps
2
+
3
+ An adapter is **data, not code**: a YAML file mapping a prediction-pool
4
+ platform's tip-form fields to CSS selectors. A generic, trusted Playwright
5
+ driver reads it and fills the inputs. Because adapters carry no executable
6
+ code, contributing one via PR is safe — there is no supply-chain surface to
7
+ audit beyond the selectors themselves.
8
+
9
+ One file per platform: `scripts/prediction-pool/adapters/<platform>.yml`.
10
+
11
+ ## Required keys
12
+
13
+ | Key | Type | Meaning |
14
+ |---|---|---|
15
+ | `platform` | string | Stable id, matches the filename (`kicktipp`). |
16
+ | `match` | string | URL host (or substring) this adapter applies to. |
17
+ | `login_required` | bool | Always `true` — the user logs in; the driver never handles credentials. |
18
+ | `selectors.row` | string | CSS selector for one repeated **match row** on the tip page. |
19
+ | `selectors.home_input` | string | Within a row: the home-score input. |
20
+ | `selectors.away_input` | string | Within a row: the away-score input. |
21
+
22
+ ## Optional keys
23
+
24
+ | Key | Type | Meaning |
25
+ |---|---|---|
26
+ | `tip_page_hint` | string | URL path pattern of the tip page (e.g. `/<pool>/tippabgabe`). |
27
+ | `selectors.home_team` / `selectors.away_team` | string | Within a row: team-name nodes, to align the right tip to the right match. |
28
+ | `selectors.bonus_*` | string | Selectors for bonus-question inputs. |
29
+ | `selectors.submit` | string | The submit control. **The driver NEVER clicks this** unless the user authorized submit this turn. |
30
+ | `notes` | string | Drift warnings, quirks, last-verified date. |
31
+
32
+ ## Rules for adapters
33
+
34
+ - **No code.** YAML data only — no scripts, no JS, no URLs the driver
35
+ fetches. Selectors and hints, nothing else.
36
+ - **`submit` is never auto-clicked.** It exists so the driver can *locate*
37
+ (and, only on explicit authorization, click) the control — never by default.
38
+ - **Selectors drift.** If a row/input selector no longer matches at run
39
+ time, the driver falls back to the **vision-assisted synthesis** path
40
+ (screenshot → identify fields → user confirms) rather than guessing.
41
+ - **PR contributions** add exactly one `<platform>.yml` plus, ideally, a
42
+ `notes:` line with the date the selectors were verified.
@@ -0,0 +1,23 @@
1
+ # kicktipp — declarative selector map (see _schema.md)
2
+ #
3
+ # Selectors are a STARTING POINT and may drift if kicktipp changes its DOM.
4
+ # They have not been re-verified against the live site in this commit — if a
5
+ # selector no longer matches at run time, the driver falls back to the
6
+ # vision-assisted synthesis path. Update `notes.verified` when you confirm.
7
+ platform: kicktipp
8
+ match: kicktipp.de
9
+ login_required: true
10
+ tip_page_hint: "/<pool>/tippabgabe"
11
+ selectors:
12
+ row: "form#tippabgabeSpiele tbody tr"
13
+ home_team: "td.col1 .nfooball, td.col1"
14
+ away_team: "td.col3 .nfooball, td.col3"
15
+ home_input: "input.kicktipp-heimtipp"
16
+ away_input: "input.kicktipp-gasttipp"
17
+ # submit is located, never auto-clicked unless the user authorizes it this turn
18
+ submit: "input[type=submit][value*='Tipps speichern'], button[type=submit]"
19
+ notes: |
20
+ verified: unverified (DOM not inspected in this commit)
21
+ kicktipp renders one <tr> per match inside the tip form; each row carries a
22
+ home and away number input. Bonus questions live on separate pages per pool.
23
+ If the row/input selectors miss, use the vision path — do not guess.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ """Poisson tournament simulator for prediction-pool-optimizer.
3
+
4
+ Honest replacement for "I simulated 10,000 runs" — this actually runs them.
5
+ Goals per match are drawn from a Poisson whose rate comes from each team's
6
+ attack / defence strength; group stages are round-robin, then a single-
7
+ elimination bracket runs over the qualifiers. Aggregates advancement and
8
+ title probabilities over N runs.
9
+
10
+ It is an APPROXIMATION, stated as such: real tournament bracket pairings
11
+ (winner-of-A vs runner-up-of-B …) are format-specific. Provide an explicit
12
+ `bracket` (list of name pairs per round, or "auto" for a random seed) to
13
+ control this; the default "auto" randomly seeds qualifiers and is good
14
+ enough for outright/advancement estimates, not for exact-pairing bonus
15
+ questions.
16
+
17
+ Input JSON shape:
18
+ {
19
+ "base_goals": 1.35, # league-average goals per side
20
+ "teams": { "Germany": {"att": 1.3, "def": 0.8}, ... }, # att/def multipliers (1.0 = average)
21
+ "groups": [ ["Germany","Scotland","Hungary","Switzerland"], ... ],
22
+ "advance_per_group": 2,
23
+ "bracket": "auto" # or omit; "auto" = random seed of qualifiers
24
+ }
25
+
26
+ Usage:
27
+ python3 scripts/prediction-pool/poisson_sim.py teams.json --runs 20000 [--seed 1]
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import math
34
+ import random
35
+ import sys
36
+ from collections import defaultdict
37
+ from pathlib import Path
38
+
39
+
40
+ def _poisson(rate: float, rng: random.Random) -> int:
41
+ """Knuth's algorithm — stdlib only, no numpy."""
42
+ if rate <= 0:
43
+ return 0
44
+ L = math.exp(-rate)
45
+ k, p = 0, 1.0
46
+ while True:
47
+ k += 1
48
+ p *= rng.random()
49
+ if p <= L:
50
+ return k - 1
51
+
52
+
53
+ def _rates(home: str, away: str, teams: dict, base: float) -> tuple[float, float]:
54
+ h, a = teams.get(home, {}), teams.get(away, {})
55
+ lam_h = base * h.get("att", 1.0) * a.get("def", 1.0)
56
+ lam_a = base * a.get("att", 1.0) * h.get("def", 1.0)
57
+ return lam_h, lam_a
58
+
59
+
60
+ def _play(home: str, away: str, teams: dict, base: float, rng: random.Random,
61
+ allow_draw: bool = True) -> tuple[int, int]:
62
+ lam_h, lam_a = _rates(home, away, teams, base)
63
+ gh, ga = _poisson(lam_h, rng), _poisson(lam_a, rng)
64
+ if not allow_draw and gh == ga:
65
+ # extra-time / penalties proxy: edge to the stronger attack, else coin flip
66
+ if lam_h == lam_a:
67
+ return (gh + 1, ga) if rng.random() < 0.5 else (gh, ga + 1)
68
+ return (gh + 1, ga) if lam_h > lam_a else (gh, ga + 1)
69
+ return gh, ga
70
+
71
+
72
+ def _group_table(group: list[str], teams: dict, base: float, rng: random.Random) -> list[str]:
73
+ pts = defaultdict(int)
74
+ gd = defaultdict(int)
75
+ gf = defaultdict(int)
76
+ for i in range(len(group)):
77
+ for j in range(i + 1, len(group)):
78
+ gh, ga = _play(group[i], group[j], teams, base, rng)
79
+ gd[group[i]] += gh - ga
80
+ gd[group[j]] += ga - gh
81
+ gf[group[i]] += gh
82
+ gf[group[j]] += ga
83
+ if gh > ga:
84
+ pts[group[i]] += 3
85
+ elif ga > gh:
86
+ pts[group[j]] += 3
87
+ else:
88
+ pts[group[i]] += 1
89
+ pts[group[j]] += 1
90
+ # rank: points, then goal difference, then goals for, then random tiebreak
91
+ return sorted(group, key=lambda t: (pts[t], gd[t], gf[t], rng.random()), reverse=True)
92
+
93
+
94
+ def _knockout(qualifiers: list[str], teams: dict, base: float, rng: random.Random) -> str:
95
+ field = qualifiers[:]
96
+ rng.shuffle(field)
97
+ # pad to a power of two with byes
98
+ while (len(field) & (len(field) - 1)) != 0:
99
+ field.append(None)
100
+ while len(field) > 1:
101
+ nxt = []
102
+ for i in range(0, len(field), 2):
103
+ a, b = field[i], field[i + 1]
104
+ if a is None:
105
+ nxt.append(b)
106
+ elif b is None:
107
+ nxt.append(a)
108
+ else:
109
+ gh, ga = _play(a, b, teams, base, rng, allow_draw=False)
110
+ nxt.append(a if gh > ga else b)
111
+ field = nxt
112
+ return field[0]
113
+
114
+
115
+ def simulate(cfg: dict, runs: int, seed: int | None) -> dict:
116
+ rng = random.Random(seed)
117
+ base = float(cfg.get("base_goals", 1.35))
118
+ teams = cfg["teams"]
119
+ groups = cfg.get("groups", [])
120
+ adv = int(cfg.get("advance_per_group", 2))
121
+
122
+ advanced = defaultdict(int)
123
+ champ = defaultdict(int)
124
+ for _ in range(runs):
125
+ qualifiers: list[str] = []
126
+ if groups:
127
+ for g in groups:
128
+ ranked = _group_table(g, teams, base, rng)
129
+ top = ranked[:adv]
130
+ qualifiers.extend(top)
131
+ for t in top:
132
+ advanced[t] += 1
133
+ else:
134
+ qualifiers = list(teams.keys())
135
+ winner = _knockout(qualifiers, teams, base, rng) if len(qualifiers) > 1 else (qualifiers or [None])[0]
136
+ if winner is not None:
137
+ champ[winner] += 1
138
+
139
+ def pct(d):
140
+ return {t: round(100 * c / runs, 2) for t, c in sorted(d.items(), key=lambda kv: -kv[1])}
141
+
142
+ return {"runs": runs, "seed": seed, "advance_pct": pct(advanced), "title_pct": pct(champ)}
143
+
144
+
145
+ def main() -> int:
146
+ ap = argparse.ArgumentParser(description="Poisson tournament simulator (stdlib only).")
147
+ ap.add_argument("config", help="Path to teams/groups JSON.")
148
+ ap.add_argument("--runs", type=int, default=20000, help="Number of simulated tournaments.")
149
+ ap.add_argument("--seed", type=int, default=None, help="RNG seed for reproducibility.")
150
+ args = ap.parse_args()
151
+
152
+ cfg_path = Path(args.config)
153
+ if not cfg_path.is_file():
154
+ print(f"ERROR: config not found: {cfg_path}", file=sys.stderr)
155
+ return 2
156
+ cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
157
+ if "teams" not in cfg:
158
+ print("ERROR: config needs a 'teams' object.", file=sys.stderr)
159
+ return 2
160
+
161
+ result = simulate(cfg, args.runs, args.seed)
162
+ print(json.dumps(result, indent=2))
163
+ return 0
164
+
165
+
166
+ if __name__ == "__main__":
167
+ raise SystemExit(main())
@@ -67,6 +67,7 @@ def confidence_badge(level: str) -> str:
67
67
  "estimated": "≈ geschätzt",
68
68
  "vendor-claim": "⚠️ vendor-claim",
69
69
  "pending": "⏳ pending",
70
+ "available": "🔁 verfügbar (Default aus)",
70
71
  }
71
72
  return badges.get(level, level)
72
73
 
@@ -0,0 +1,77 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/event4u-app/agent-config/scripts/schemas/agent-settings.schema.json",
4
+ "title": "Agent settings (.agent-settings.yml)",
5
+ "$comment": "Collision-prevention schema added by the 2026-06-01 rule_loading_tier untangle. It enum-constrains the value-bearing keys that have historically been overloaded with a foreign vocabulary (the root cause of the rule_loading_tier/memory-cadence collision: the same key carrying two value sets). It is deliberately PERMISSIVE elsewhere (additionalProperties: true at every level) — exhaustive per-key validation is out of scope and would be brittle. The job of this schema is to make a value-vocabulary collision a hard CI failure, not to type every setting.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "properties": {
9
+ "rule_loading_tier": {
10
+ "type": "string",
11
+ "enum": ["minimal", "balanced", "full", "custom"],
12
+ "description": "Rule-tier loading footprint. See docs/contracts/cost-profile-defaults.md and docs/contracts/rule-router.md. NOT a memory or model lever."
13
+ },
14
+ "memory": {
15
+ "type": "object",
16
+ "additionalProperties": true,
17
+ "properties": {
18
+ "cadence": {
19
+ "type": "string",
20
+ "enum": ["auto", "always", "never"],
21
+ "description": "Cadence of the 🧠 memory-visibility line. See docs/contracts/memory-visibility-v1.md. Owns its own key since the 2026-06-01 untangle; previously collided with rule_loading_tier."
22
+ }
23
+ }
24
+ },
25
+ "model": {
26
+ "type": "object",
27
+ "additionalProperties": true,
28
+ "properties": {
29
+ "auto_switch": {
30
+ "type": "string",
31
+ "enum": ["suggest", "auto", "off"],
32
+ "description": "Per-skill model-tier routing (ADR-035). Distinct from rule_loading_tier — picks WHICH model runs, not how many rules load."
33
+ }
34
+ }
35
+ },
36
+ "lean_projection": {
37
+ "type": "object",
38
+ "additionalProperties": true,
39
+ "properties": {
40
+ "mode": {
41
+ "type": "string",
42
+ "enum": ["eager-all", "thin"]
43
+ }
44
+ }
45
+ },
46
+ "cost": {
47
+ "type": "object",
48
+ "additionalProperties": true,
49
+ "properties": {
50
+ "enforcement": {
51
+ "type": "string",
52
+ "enum": ["advisory", "hard-stop"]
53
+ }
54
+ }
55
+ },
56
+ "personal": {
57
+ "type": "object",
58
+ "additionalProperties": true,
59
+ "properties": {
60
+ "autonomy": {
61
+ "type": "string",
62
+ "enum": ["on", "off", "auto"]
63
+ }
64
+ }
65
+ },
66
+ "worktrees": {
67
+ "type": "object",
68
+ "additionalProperties": true,
69
+ "properties": {
70
+ "mode": {
71
+ "type": "string",
72
+ "enum": ["off", "on", "ask"]
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
@@ -154,6 +154,13 @@
154
154
  "default": "active",
155
155
  "description": "ADR-013 lifecycle state."
156
156
  },
157
+ "requires_skills": {
158
+ "type": "array",
159
+ "minItems": 1,
160
+ "uniqueItems": true,
161
+ "items": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"},
162
+ "description": "Skill-composition graph (roadmap 3.4): names of sub-skills this skill's body invokes/assumes. Distinct from ADR-015 `requires` (artefact→pack edges). scripts/check_skill_requires.py enforces (a) each target exists and (b) the required skill is co-available wherever this skill ships (same pack, a requires_hint-reachable pack, or always-on)."
163
+ },
157
164
  "trust": {
158
165
  "type": "object",
159
166
  "additionalProperties": false,
@@ -4,7 +4,7 @@
4
4
  Verifies the 3-step Quickstart from a fresh-project perspective:
5
5
 
6
6
  1. `scripts/install.py --project <tmpdir>` produces a usable
7
- `.agent-settings.yml` with the documented default `cost_profile`.
7
+ `.agent-settings.yml` with the documented default `rule_loading_tier`.
8
8
  2. The decision_engine block (P2.x of road-to-productization) parses
9
9
  cleanly through the same engine parser the runtime uses.
10
10
  3. The work-engine state-file format (`agents/runtime/state/<id>.json`) is
@@ -68,16 +68,16 @@ def _check_installer_runs(tmpdir: Path) -> tuple[int, Path | None]:
68
68
 
69
69
 
70
70
  def _check_default_profile(settings: Path) -> int:
71
- """Step 2 — assert default cost_profile matches the contract."""
71
+ """Step 2 — assert default rule_loading_tier matches the contract."""
72
72
  import yaml
73
73
 
74
74
  parsed = yaml.safe_load(settings.read_text(encoding="utf-8"))
75
75
  if not isinstance(parsed, dict):
76
76
  return _fail(f"{settings.name}: top-level is not a YAML mapping")
77
- profile = parsed.get("cost_profile")
77
+ profile = parsed.get("rule_loading_tier")
78
78
  if profile != EXPECTED_DEFAULT_PROFILE:
79
79
  return _fail(
80
- f"cost_profile drift: docs/contracts/cost-profile-defaults.md "
80
+ f"rule_loading_tier drift: docs/contracts/rule-loading-tier-defaults.md "
81
81
  f"declares '{EXPECTED_DEFAULT_PROFILE}', settings has '{profile!r}'"
82
82
  )
83
83
  return 0
@@ -90,7 +90,7 @@ def main(argv: list[str] | None = None) -> int:
90
90
  ap.add_argument("--template", default=str(DEFAULT_TEMPLATE),
91
91
  help="path to the settings template")
92
92
  ap.add_argument("--profile", default=None,
93
- help="cost_profile preset (minimal|balanced|full). "
93
+ help="rule_loading_tier preset (minimal|balanced|full). "
94
94
  "Default: inferred from target, else 'minimal'")
95
95
  ap.add_argument("--profile-dir", default=str(DEFAULT_PROFILE_DIR),
96
96
  help="directory containing profile .ini files")
@@ -108,7 +108,9 @@ def main(argv: list[str] | None = None) -> int:
108
108
 
109
109
  try:
110
110
  user_data = load_user(target)
111
- profile = args.profile or str(user_data.get("cost_profile") or "minimal")
111
+ profile = args.profile or str(
112
+ user_data.get("rule_loading_tier") or user_data.get("cost_profile") or "minimal"
113
+ )
112
114
  if profile not in _install.SUPPORTED_PROFILES:
113
115
  print(f"error: unsupported profile {profile!r}", file=sys.stderr)
114
116
  return 2
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """Agent-settings schema validator (rule_loading_tier untangle, 2026-06-01).
3
+
4
+ Validates ``config/agent-settings.template.yml`` and any local
5
+ ``.agent-settings.yml`` against
6
+ ``scripts/schemas/agent-settings.schema.json``. The schema is
7
+ deliberately permissive (``additionalProperties: true`` everywhere) and
8
+ only enum-constrains the value-bearing keys that have historically been
9
+ overloaded with a foreign vocabulary — the root cause of the
10
+ ``rule_loading_tier`` / memory-cadence collision. Its job is to make a
11
+ value-vocabulary collision a hard CI failure.
12
+
13
+ Template placeholders (``__RULE_LOADING_TIER__``, ``__USER_TYPE__``) are
14
+ substituted with their installer defaults before validation, mirroring
15
+ ``scripts/install.py``.
16
+
17
+ Exit codes:
18
+ - 0 — every checked file validates.
19
+ - 1 — at least one schema violation (unknown enum value, wrong type).
20
+ - 3 — bootstrap failure (missing dependency / schema file).
21
+
22
+ Contract: docs/contracts/cost-profile-defaults.md +
23
+ docs/contracts/memory-visibility-v1.md. Wired into ``task ci`` via
24
+ ``taskfiles/ci-fast.yml``.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ try:
34
+ import yaml
35
+ except ImportError: # pragma: no cover — bootstrap guard
36
+ print("::error::PyYAML not installed; cannot validate agent settings")
37
+ sys.exit(3)
38
+
39
+ try:
40
+ import jsonschema
41
+ except ImportError: # pragma: no cover — bootstrap guard
42
+ print("::error::jsonschema not installed; cannot validate agent settings")
43
+ sys.exit(3)
44
+
45
+ REPO_ROOT = Path(__file__).resolve().parent.parent
46
+ SCHEMA_PATH = REPO_ROOT / "scripts" / "schemas" / "agent-settings.schema.json"
47
+ TEMPLATE_PATH = REPO_ROOT / "config" / "agent-settings.template.yml"
48
+ LOCAL_PATHS = [REPO_ROOT / ".agent-settings.yml"]
49
+
50
+ # Installer-default substitutions, mirroring scripts/install.py so the
51
+ # template validates as it would after a fresh `balanced` install.
52
+ PLACEHOLDERS = {
53
+ "__RULE_LOADING_TIER__": "balanced",
54
+ "__USER_TYPE__": "",
55
+ }
56
+
57
+
58
+ def _load_yaml(path: Path, *, substitute: bool) -> dict | None:
59
+ if not path.is_file():
60
+ return None
61
+ text = path.read_text(encoding="utf-8")
62
+ if substitute:
63
+ for placeholder, value in PLACEHOLDERS.items():
64
+ text = text.replace(placeholder, value)
65
+ try:
66
+ raw = yaml.safe_load(text)
67
+ except yaml.YAMLError as exc:
68
+ print(f"::error file={path}::malformed YAML: {exc}")
69
+ return {}
70
+ if raw is None:
71
+ return {}
72
+ if not isinstance(raw, dict):
73
+ print(f"::error file={path}::top-level must be a mapping")
74
+ return {}
75
+ return raw
76
+
77
+
78
+ def _validate(path: Path, doc: dict, validator: jsonschema.Draft7Validator) -> int:
79
+ errors = sorted(validator.iter_errors(doc), key=lambda e: list(e.path))
80
+ if not errors:
81
+ return 0
82
+ for err in errors:
83
+ loc = ".".join(str(p) for p in err.path) or "<root>"
84
+ print(f"::error file={path}::{loc}: {err.message}")
85
+ return len(errors)
86
+
87
+
88
+ def main() -> int:
89
+ if not SCHEMA_PATH.is_file():
90
+ print(f"::error::schema missing: {SCHEMA_PATH}")
91
+ return 3
92
+ schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
93
+ validator = jsonschema.Draft7Validator(schema)
94
+
95
+ total_errors = 0
96
+ checked = 0
97
+
98
+ template = _load_yaml(TEMPLATE_PATH, substitute=True)
99
+ if template is None:
100
+ print(f"::error file={TEMPLATE_PATH}::template missing")
101
+ return 1
102
+ total_errors += _validate(TEMPLATE_PATH, template, validator)
103
+ checked += 1
104
+
105
+ for local in LOCAL_PATHS:
106
+ doc = _load_yaml(local, substitute=False)
107
+ if doc is None:
108
+ continue
109
+ total_errors += _validate(local, doc, validator)
110
+ checked += 1
111
+
112
+ if total_errors:
113
+ print(f"agent-settings schema: {total_errors} violation(s) across {checked} file(s)")
114
+ return 1
115
+ print(f"agent-settings schema: OK ({checked} file(s) validated)")
116
+ return 0
117
+
118
+
119
+ if __name__ == "__main__":
120
+ sys.exit(main())
@@ -15,7 +15,7 @@
15
15
  # --- Cost profile ---
16
16
  # Master switch for which rule tiers load. Options: minimal | balanced | full.
17
17
  # `balanced` is the recommended default; flip to `minimal` for kernel-only.
18
- cost_profile: balanced
18
+ rule_loading_tier: balanced
19
19
 
20
20
  # --- Version pin (opt-in) ---
21
21
  # Leave commented out to follow whatever `agent-config` is on $PATH (per D4).