@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.
- package/.agent-src/commands/cost-report.md +12 -7
- package/.agent-src/commands/prediction-pool.md +215 -0
- package/.agent-src/commands/set-cost-profile.md +8 -8
- package/.agent-src/commands/sync-agent-settings.md +2 -2
- package/.agent-src/presets/README.md +1 -1
- package/.agent-src/profiles/README.md +1 -1
- package/.agent-src/rules/non-destructive-by-default.md +2 -1
- package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +196 -0
- package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +18 -0
- package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +80 -0
- package/.agent-src/templates/agent-settings.md +7 -7
- package/.agent-src/templates/agents/agent-project-settings.example.yml +2 -2
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +2 -1
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +1 -1
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +9 -7
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +9 -10
- package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +17 -4
- package/.claude-plugin/marketplace.json +3 -1
- package/CHANGELOG.md +48 -0
- package/README.md +2 -2
- package/config/agent-settings.template.yml +11 -2
- package/config/discovery/packs.yml +11 -0
- package/config/discovery/workspaces.yml +1 -1
- package/config/profiles/balanced.ini +1 -1
- package/config/profiles/full.ini +1 -1
- package/config/profiles/minimal.ini +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +80 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +34 -3
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +13 -4
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/server/io/substituteTemplate.js +3 -3
- package/dist/server/io/substituteTemplate.js.map +1 -1
- package/dist/server/routes/settings.js +2 -2
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/{index-DVsyUMZe.js → index-5lFqAKL0.js} +2 -2
- package/dist/ui/assets/index-5lFqAKL0.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/architecture/current-onboard-baseline.md +3 -3
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +7 -5
- package/docs/contracts/adr-level-6-productization.md +1 -1
- package/docs/contracts/config-presets.md +2 -2
- package/docs/contracts/cost-profile-defaults.md +5 -5
- package/docs/contracts/discovery-manifest.schema.json +1 -1
- package/docs/contracts/explain-trace.schema.json +3 -3
- package/docs/contracts/memory-visibility-v1.md +15 -7
- package/docs/contracts/profile-system.md +2 -2
- package/docs/contracts/settings-api.md +3 -3
- package/docs/contracts/value-report-schema.md +14 -1
- package/docs/customization.md +21 -5
- package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +11 -11
- package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +16 -2
- package/docs/decisions/ADR-034-per-skill-model-recommendation-transport.md +1 -1
- package/docs/decisions/ADR-036-global-install-browser-wizard-handoff.md +106 -0
- package/docs/decisions/ADR-037-cost-profile-untangle.md +117 -0
- package/docs/decisions/ADR-rule-kernel-and-router.md +1 -1
- package/docs/decisions/INDEX.md +2 -0
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/layered-settings.md +2 -2
- package/docs/installation.md +3 -3
- package/docs/setup/mcp-client-config.md +1 -1
- package/docs/value.md +9 -7
- package/docs/wizard.md +1 -1
- package/package.json +1 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_explain.py +1 -1
- package/scripts/_cli/explain_last/inputs.py +11 -8
- package/scripts/_cli/explain_last/sections/inputs.py +1 -1
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/agent_settings.py +2 -1
- package/scripts/_lib/value_ladder.py +99 -2
- package/scripts/_lib/value_report.py +30 -16
- package/scripts/ai_council/modes.py +1 -1
- package/scripts/audit_initial_context.py +16 -0
- package/scripts/check_skill_requires.py +143 -0
- package/scripts/condense.py +13 -2
- package/scripts/first-run.sh +11 -11
- package/scripts/install +14 -1
- package/scripts/install.py +127 -428
- package/scripts/install_anthropic_key.sh +1 -1
- package/scripts/install_openai_key.sh +1 -1
- package/scripts/lint_discovery_vocabulary.py +5 -5
- package/scripts/lint_value_dashboard.py +1 -1
- package/scripts/prediction-pool/adapters/_schema.md +42 -0
- package/scripts/prediction-pool/adapters/kicktipp.yml +23 -0
- package/scripts/prediction-pool/poisson_sim.py +167 -0
- package/scripts/render_value_md.py +1 -0
- package/scripts/schemas/agent-settings.schema.json +77 -0
- package/scripts/schemas/skill.schema.json +7 -0
- package/scripts/smoke_quickstart.py +4 -4
- package/scripts/sync_agent_settings.py +4 -2
- package/scripts/validate_agent_settings.py +120 -0
- package/templates/minimal/.agent-settings.yml +1 -1
- 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'
|
|
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'
|
|
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
|
-
`
|
|
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
|
-
|
|
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 &
|
|
139
|
+
overlap_cost = pack_ids & RULE_LOADING_TIER_RESERVED
|
|
140
140
|
if overlap_cost:
|
|
141
|
-
errors.append(f"pack ids collide with
|
|
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())
|
|
@@ -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 `
|
|
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
|
|
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("
|
|
77
|
+
profile = parsed.get("rule_loading_tier")
|
|
78
78
|
if profile != EXPECTED_DEFAULT_PROFILE:
|
|
79
79
|
return _fail(
|
|
80
|
-
f"
|
|
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="
|
|
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(
|
|
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
|
-
|
|
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).
|