@event4u/agent-config 5.6.1 → 5.8.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/agent-handoff.md +1 -1
- package/.agent-src/commands/agent-status.md +1 -1
- package/.agent-src/commands/agents/audit.md +1 -1
- package/.agent-src/commands/agents/init.md +1 -1
- package/.agent-src/commands/agents/user/accept.md +3 -3
- package/.agent-src/commands/agents/user/init.md +4 -4
- package/.agent-src/commands/agents/user/show.md +3 -3
- package/.agent-src/commands/agents/user/update.md +3 -3
- package/.agent-src/commands/agents/user.md +1 -1
- package/.agent-src/commands/agents.md +1 -1
- package/.agent-src/commands/analytics/prune.md +1 -1
- package/.agent-src/commands/analytics/show.md +1 -1
- package/.agent-src/commands/analytics.md +1 -1
- package/.agent-src/commands/bug-fix.md +1 -1
- package/.agent-src/commands/challenge-me.md +1 -1
- package/.agent-src/commands/chat-history/import.md +1 -1
- package/.agent-src/commands/chat-history/learn.md +1 -1
- package/.agent-src/commands/chat-history/show.md +1 -1
- package/.agent-src/commands/chat-history.md +1 -1
- package/.agent-src/commands/check-current-md.md +1 -1
- package/.agent-src/commands/condense.md +1 -1
- package/.agent-src/commands/context.md +1 -1
- package/.agent-src/commands/cost-report.md +13 -8
- package/.agent-src/commands/council.md +3 -3
- package/.agent-src/commands/create-pr/description-only.md +1 -1
- package/.agent-src/commands/create-pr.md +1 -1
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature.md +1 -1
- package/.agent-src/commands/fix/ci.md +1 -1
- package/.agent-src/commands/fix/portability.md +1 -1
- package/.agent-src/commands/fix/pr-bot-comments.md +1 -1
- package/.agent-src/commands/fix/pr-comments.md +1 -1
- package/.agent-src/commands/fix/pr-developer-comments.md +1 -1
- package/.agent-src/commands/fix/refs.md +1 -1
- package/.agent-src/commands/fix/seeder.md +1 -1
- package/.agent-src/commands/fix.md +1 -1
- package/.agent-src/commands/judge.md +1 -1
- package/.agent-src/commands/knowledge/cross-repo.md +1 -1
- package/.agent-src/commands/knowledge/forget.md +1 -1
- package/.agent-src/commands/knowledge/ingest.md +1 -1
- package/.agent-src/commands/knowledge/list.md +1 -1
- package/.agent-src/commands/knowledge.md +1 -1
- package/.agent-src/commands/memory/add.md +1 -1
- package/.agent-src/commands/memory/learn-low-impact.md +1 -1
- package/.agent-src/commands/memory/load.md +1 -1
- package/.agent-src/commands/memory/mine-session.md +1 -1
- package/.agent-src/commands/memory/promote.md +1 -1
- package/.agent-src/commands/memory/propose.md +1 -1
- package/.agent-src/commands/memory.md +1 -1
- package/.agent-src/commands/mode.md +1 -1
- package/.agent-src/commands/optimize/agents-dir.md +1 -1
- package/.agent-src/commands/optimize/augmentignore.md +1 -1
- package/.agent-src/commands/optimize/rtk.md +1 -1
- package/.agent-src/commands/optimize/skills.md +1 -1
- package/.agent-src/commands/optimize.md +1 -1
- package/.agent-src/commands/orchestrate.md +1 -1
- package/.agent-src/commands/override/create.md +1 -1
- package/.agent-src/commands/override/manage.md +1 -1
- package/.agent-src/commands/override.md +1 -1
- package/.agent-src/commands/package-reset.md +1 -1
- package/.agent-src/commands/prediction-pool.md +234 -0
- package/.agent-src/commands/profile/activate.md +81 -0
- package/.agent-src/commands/profile/deactivate.md +68 -0
- package/.agent-src/commands/profile/show.md +70 -0
- package/.agent-src/commands/profile.md +68 -0
- package/.agent-src/commands/project-health.md +1 -1
- package/.agent-src/commands/quality-fix.md +1 -1
- package/.agent-src/commands/roadmap/process-full.md +1 -1
- package/.agent-src/commands/roadmap/process-phase.md +1 -1
- package/.agent-src/commands/roadmap/process-step.md +1 -1
- package/.agent-src/commands/roadmap.md +1 -1
- package/.agent-src/commands/set-cost-profile.md +9 -9
- package/.agent-src/commands/skill/preview.md +3 -3
- package/.agent-src/commands/skill.md +1 -1
- package/.agent-src/commands/skills/discover.md +1 -1
- package/.agent-src/commands/skills.md +1 -1
- package/.agent-src/commands/sync-agent-settings.md +3 -3
- package/.agent-src/commands/sync-gitignore/fix.md +1 -1
- package/.agent-src/commands/sync-gitignore.md +1 -1
- package/.agent-src/commands/update-form-request-messages.md +1 -1
- 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/check-refs/SKILL.md +1 -1
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/.agent-src/skills/git-workflow/SKILL.md +1 -1
- package/.agent-src/skills/jira-integration/SKILL.md +1 -1
- package/.agent-src/skills/markitdown/SKILL.md +1 -1
- package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +314 -0
- package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +20 -0
- package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +175 -0
- package/.agent-src/skills/prediction-pool-optimizer/reference/odds-and-bonus.md +109 -0
- package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -1
- package/.agent-src/skills/script-writing/SKILL.md +1 -1
- package/.agent-src/skills/token-optimizer/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- 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 +54 -6
- 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 +370 -364
- package/CHANGELOG.md +108 -0
- package/README.md +2 -2
- package/config/agent-settings.template.yml +11 -2
- package/config/discovery/packs.yml +11 -0
- package/config/discovery/session-profiles.yml +37 -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 +254 -100
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +41 -6
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +19 -6
- package/dist/mcp/registry-manifest.json +3 -3
- 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 +11 -5
- package/docs/contracts/adr-level-6-productization.md +1 -1
- package/docs/contracts/command-clusters.md +2 -0
- 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/session-profile-overlay.md +120 -0
- package/docs/contracts/settings-api.md +3 -3
- package/docs/contracts/value-report-schema.md +14 -1
- package/docs/customization.md +47 -5
- package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +47 -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-038-canonical-settings-path.md +66 -0
- package/docs/decisions/ADR-039-claude-skills-untracked.md +139 -0
- package/docs/decisions/ADR-rule-kernel-and-router.md +1 -1
- package/docs/decisions/INDEX.md +4 -0
- package/docs/development.md +12 -0
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/layered-settings.md +10 -4
- package/docs/installation.md +3 -3
- package/docs/setup/mcp-client-config.md +1 -1
- package/docs/skills-catalog.md +5 -1
- package/docs/value.md +9 -7
- package/docs/wizard.md +1 -1
- package/llms.txt +4 -0
- package/package.json +1 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_doctor.py +3 -2
- package/scripts/_cli/cmd_explain.py +1 -1
- package/scripts/_cli/cmd_versions.py +2 -2
- 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 +54 -6
- package/scripts/_lib/agent_src.py +30 -0
- 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/ai_council/session.py +5 -1
- package/scripts/audit_command_surface.py +7 -1
- package/scripts/audit_initial_context.py +26 -2
- package/scripts/check_gate_paths.py +117 -0
- package/scripts/check_references.py +51 -2
- package/scripts/check_skill_requires.py +143 -0
- package/scripts/check_test_coverage_diff.py +180 -0
- package/scripts/compile_router.py +5 -1
- package/scripts/condense.py +92 -4
- package/scripts/config/session_profiles.py +492 -0
- package/scripts/council_cli.py +5 -1
- package/scripts/first-run.sh +11 -11
- package/scripts/hook_manifest.yaml +15 -7
- package/scripts/hooks/dispatch_hook.py +8 -0
- package/scripts/install +14 -1
- package/scripts/install-hooks.sh +2 -1
- package/scripts/install.py +203 -433
- package/scripts/install_anthropic_key.sh +1 -1
- package/scripts/install_openai_key.sh +1 -1
- package/scripts/inventory_abstraction_budget.py +6 -1
- package/scripts/lint_agents_md.py +11 -4
- package/scripts/lint_discovery_vocabulary.py +5 -5
- package/scripts/lint_hook_concern_budget.py +5 -1
- package/scripts/lint_marketplace.py +18 -7
- package/scripts/lint_roadmap_ci_steps.py +5 -1
- package/scripts/lint_roadmap_complexity.py +5 -1
- package/scripts/lint_value_dashboard.py +1 -1
- package/scripts/mcp_server/prompts.py +5 -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/prediction-pool/pool_winsim.py +236 -0
- package/scripts/prediction-pool/score_ev.py +188 -0
- package/scripts/profile_staleness_hook.py +69 -0
- package/scripts/render_value_md.py +1 -0
- package/scripts/roadmap_progress_hook.py +56 -6
- package/scripts/schemas/agent-settings.schema.json +77 -0
- package/scripts/schemas/skill.schema.json +7 -0
- package/scripts/smoke_quickstart.py +7 -6
- package/scripts/sync_agent_settings.py +12 -5
- package/scripts/validate_agent_settings.py +124 -0
- package/scripts/validate_decision_engine.py +5 -1
- package/templates/minimal/.agent-settings.yml +1 -1
- package/dist/ui/assets/index-DVsyUMZe.js.map +0 -1
- package/scripts/measure_roadmap_trajectory.py +0 -112
- package/scripts/verify_roadmap_closure.py +0 -327
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Field model + P(finish 1st) simulator for prediction-pool-optimizer.
|
|
3
|
+
|
|
4
|
+
Honest operationalisation of the large-pool strategy note: in a big pool the
|
|
5
|
+
target is **P(finish ahead of the whole field)**, not E(points). Maximizing EV
|
|
6
|
+
makes your tip-set converge with everyone else's EV-max set, so you cannot open
|
|
7
|
+
the gap you need. This script measures that — and greedily finds the few tips
|
|
8
|
+
worth flipping off EV-max to manufacture upside.
|
|
9
|
+
|
|
10
|
+
What it does:
|
|
11
|
+
|
|
12
|
+
1. Models the FIELD: each opponent commits one tip per match, drawn from a
|
|
13
|
+
softmax over the per-match EV table (temperature controls spread — low =
|
|
14
|
+
the crowd clusters tightly on EV-max, high = noisy). This is a model of
|
|
15
|
+
the crowd, stated as such; feed a real field distribution if you have one.
|
|
16
|
+
2. Pre-draws R outcome scenarios from the Poisson grids and pre-scores every
|
|
17
|
+
opponent once, so evaluating any of MY tip-sets is cheap.
|
|
18
|
+
3. Reports P(win) for the EV-max-everywhere baseline, then runs a greedy
|
|
19
|
+
flip search: repeatedly flip the single tip that most raises P(win),
|
|
20
|
+
reporting the EV cost and the P(win) gain per flip, up to --max-flips.
|
|
21
|
+
|
|
22
|
+
The lesson it makes concrete: with small N the EV-max set already wins often
|
|
23
|
+
and flips do not help (don't add variance you don't need); with large N and a
|
|
24
|
+
deficit, a handful of calculated flips can lift P(win) materially at a small
|
|
25
|
+
EV cost. The crossover is empirical — run it.
|
|
26
|
+
|
|
27
|
+
It is an APPROXIMATION: the field is a softmax-EV model, not your real pool's
|
|
28
|
+
tips, and the Poisson grids are only as good as the lambdas you feed (de-vigged
|
|
29
|
+
consensus odds — see reference/odds-and-bonus.md). Outcomes and EV are exact
|
|
30
|
+
for that model; the field shape is a prior.
|
|
31
|
+
|
|
32
|
+
Input JSON:
|
|
33
|
+
{
|
|
34
|
+
"rule": {"exact": 5, "diff": 3, "tendency": 2},
|
|
35
|
+
"participants": 120, # field size N; opponents modelled = N-1 (capped by --max-opponents)
|
|
36
|
+
"my_lead": 0, # my current points minus the rival-to-beat's (negative = behind)
|
|
37
|
+
"field_temperature": 0.6, # softmax temp for crowd spread around EV-max
|
|
38
|
+
"matches": [
|
|
39
|
+
{"match": "A", "lh": 2.0, "la": 0.7},
|
|
40
|
+
{"match": "B", "lh": 0.6, "la": 2.1}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
python3 scripts/prediction-pool/pool_winsim.py pool.json --runs 4000 --max-flips 4 [--seed 1]
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import argparse
|
|
50
|
+
import json
|
|
51
|
+
import math
|
|
52
|
+
import random
|
|
53
|
+
import sys
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
# Reuse the exact-score engine.
|
|
57
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
58
|
+
from score_ev import ev_table, grid, _score # noqa: E402
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_tip(s: str) -> tuple[int, int]:
|
|
62
|
+
h, a = s.split(":")
|
|
63
|
+
return int(h), int(a)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _flat_grid(g):
|
|
67
|
+
"""Flatten a joint grid into (prob, (h,a)) pairs for sampling."""
|
|
68
|
+
flat = []
|
|
69
|
+
for h in range(len(g)):
|
|
70
|
+
for a in range(len(g[h])):
|
|
71
|
+
p = g[h][a]
|
|
72
|
+
if p > 0:
|
|
73
|
+
flat.append((p, (h, a)))
|
|
74
|
+
return flat
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _sample_outcome(flat, rng: random.Random) -> tuple[int, int]:
|
|
78
|
+
r = rng.random()
|
|
79
|
+
acc = 0.0
|
|
80
|
+
for p, ha in flat:
|
|
81
|
+
acc += p
|
|
82
|
+
if r <= acc:
|
|
83
|
+
return ha
|
|
84
|
+
return flat[-1][1]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _softmax_pick(rows, temperature: float, rng: random.Random) -> tuple[int, int]:
|
|
88
|
+
"""Pick a tip (h,a) from EV rows via softmax(EV / temperature)."""
|
|
89
|
+
if temperature <= 0:
|
|
90
|
+
h, a, _ = rows[0]
|
|
91
|
+
return h, a
|
|
92
|
+
top = rows[:24] # the tail has negligible mass; cap for speed
|
|
93
|
+
mx = top[0][2]
|
|
94
|
+
weights = [math.exp((ev - mx) / temperature) for _, _, ev in top]
|
|
95
|
+
tot = sum(weights)
|
|
96
|
+
r = rng.random() * tot
|
|
97
|
+
acc = 0.0
|
|
98
|
+
for (h, a, _), w in zip(top, weights):
|
|
99
|
+
acc += w
|
|
100
|
+
if r <= acc:
|
|
101
|
+
return h, a
|
|
102
|
+
h, a, _ = top[-1]
|
|
103
|
+
return h, a
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run(cfg: dict, runs: int, max_flips: int, max_opponents: int, top_flip: int,
|
|
107
|
+
seed: int):
|
|
108
|
+
rng = random.Random(seed)
|
|
109
|
+
rule = cfg.get("rule", {"exact": 4, "diff": 3, "tendency": 2})
|
|
110
|
+
pe, pd, pt = float(rule["exact"]), float(rule["diff"]), float(rule["tendency"])
|
|
111
|
+
n = int(cfg.get("participants", 20))
|
|
112
|
+
my_lead = float(cfg.get("my_lead", 0))
|
|
113
|
+
temp = float(cfg.get("field_temperature", 0.6))
|
|
114
|
+
matches = cfg["matches"]
|
|
115
|
+
n_opp = max(0, min(n - 1, max_opponents))
|
|
116
|
+
|
|
117
|
+
# Per match: EV table + sampling grid.
|
|
118
|
+
per = []
|
|
119
|
+
for m in matches:
|
|
120
|
+
rows, _ = ev_table(m["lh"], m["la"], pe, pd, pt, max_tip=6)
|
|
121
|
+
g = grid(m["lh"], m["la"])
|
|
122
|
+
per.append({"name": m.get("match", "?"), "rows": rows, "flat": _flat_grid(g)})
|
|
123
|
+
|
|
124
|
+
# Pre-draw R outcome scenarios (one actual scoreline per match per run).
|
|
125
|
+
scenarios = [[_sample_outcome(p["flat"], rng) for p in per] for _ in range(runs)]
|
|
126
|
+
|
|
127
|
+
# Model the field: each opponent commits a fixed tip per match (softmax-EV),
|
|
128
|
+
# then score each opponent across all scenarios. Keep the per-scenario field
|
|
129
|
+
# MAX so any of my tip-sets can be evaluated against it cheaply.
|
|
130
|
+
field_max = [(-1e9) for _ in range(runs)]
|
|
131
|
+
for _ in range(n_opp):
|
|
132
|
+
opp_tips = [_softmax_pick(p["rows"], temp, rng) for p in per]
|
|
133
|
+
for s_idx, sc in enumerate(scenarios):
|
|
134
|
+
tot = 0.0
|
|
135
|
+
for (th, ta), (ah, aa) in zip(opp_tips, sc):
|
|
136
|
+
tot += _score(th, ta, ah, aa, pe, pd, pt)
|
|
137
|
+
if tot > field_max[s_idx]:
|
|
138
|
+
field_max[s_idx] = tot
|
|
139
|
+
|
|
140
|
+
def my_total(tipset, s_idx):
|
|
141
|
+
sc = scenarios[s_idx]
|
|
142
|
+
tot = 0.0
|
|
143
|
+
for (th, ta), (ah, aa) in zip(tipset, sc):
|
|
144
|
+
tot += _score(th, ta, ah, aa, pe, pd, pt)
|
|
145
|
+
return tot
|
|
146
|
+
|
|
147
|
+
def p_win(tipset):
|
|
148
|
+
wins = 0
|
|
149
|
+
for s_idx in range(runs):
|
|
150
|
+
if my_total(tipset, s_idx) + my_lead > field_max[s_idx]:
|
|
151
|
+
wins += 1
|
|
152
|
+
return wins / runs
|
|
153
|
+
|
|
154
|
+
# Baseline: EV-max on every match.
|
|
155
|
+
ev_max_set = [(p["rows"][0][0], p["rows"][0][1]) for p in per]
|
|
156
|
+
base_pwin = p_win(ev_max_set)
|
|
157
|
+
|
|
158
|
+
# Greedy flips: repeatedly flip the one tip that most raises P(win),
|
|
159
|
+
# considering each match's top-`top_flip` EV candidates.
|
|
160
|
+
current = list(ev_max_set)
|
|
161
|
+
flips = []
|
|
162
|
+
used = set()
|
|
163
|
+
for _ in range(max_flips):
|
|
164
|
+
best = None
|
|
165
|
+
for mi, p in enumerate(per):
|
|
166
|
+
if mi in used:
|
|
167
|
+
continue
|
|
168
|
+
for h, a, ev in p["rows"][:top_flip]:
|
|
169
|
+
if (h, a) == current[mi]:
|
|
170
|
+
continue
|
|
171
|
+
trial = list(current)
|
|
172
|
+
trial[mi] = (h, a)
|
|
173
|
+
pw = p_win(trial)
|
|
174
|
+
ev_cost = p["rows"][0][2] - ev
|
|
175
|
+
if best is None or pw > best["pwin"]:
|
|
176
|
+
best = {"mi": mi, "tip": (h, a), "pwin": pw, "ev_cost": ev_cost,
|
|
177
|
+
"name": p["name"]}
|
|
178
|
+
if best is None or best["pwin"] <= (flips[-1]["pwin"] if flips else base_pwin):
|
|
179
|
+
break
|
|
180
|
+
current[best["mi"]] = best["tip"]
|
|
181
|
+
used.add(best["mi"])
|
|
182
|
+
flips.append(best)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"participants": n, "opponents_modelled": n_opp, "runs": runs,
|
|
186
|
+
"my_lead": my_lead, "field_temperature": temp,
|
|
187
|
+
"rule": {"exact": pe, "diff": pd, "tendency": pt},
|
|
188
|
+
"ev_max_set": [f"{per[i]['name']}={h}:{a}" for i, (h, a) in enumerate(ev_max_set)],
|
|
189
|
+
"p_win_ev_max": round(base_pwin, 4),
|
|
190
|
+
"flips": [
|
|
191
|
+
{"match": f["name"], "to": f"{f['tip'][0]}:{f['tip'][1]}",
|
|
192
|
+
"ev_cost": round(f["ev_cost"], 3), "p_win_after": round(f["pwin"], 4)}
|
|
193
|
+
for f in flips
|
|
194
|
+
],
|
|
195
|
+
"p_win_after_flips": round((flips[-1]["pwin"] if flips else base_pwin), 4),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main(argv=None) -> int:
|
|
200
|
+
ap = argparse.ArgumentParser(description="Field model + P(win) simulator.")
|
|
201
|
+
ap.add_argument("config", help="JSON config (rule, participants, my_lead, matches)")
|
|
202
|
+
ap.add_argument("--runs", type=int, default=4000, help="outcome scenarios (default 4000)")
|
|
203
|
+
ap.add_argument("--max-flips", type=int, default=4, help="max tips to flip off EV-max")
|
|
204
|
+
ap.add_argument("--max-opponents", type=int, default=300,
|
|
205
|
+
help="cap opponents modelled (default 300)")
|
|
206
|
+
ap.add_argument("--top-flip", type=int, default=4,
|
|
207
|
+
help="EV candidates per match to consider when flipping")
|
|
208
|
+
ap.add_argument("--seed", type=int, default=1)
|
|
209
|
+
ap.add_argument("--json", action="store_true", help="emit JSON instead of text")
|
|
210
|
+
args = ap.parse_args(argv)
|
|
211
|
+
|
|
212
|
+
cfg = json.loads(Path(args.config).read_text())
|
|
213
|
+
res = run(cfg, args.runs, args.max_flips, args.max_opponents, args.top_flip, args.seed)
|
|
214
|
+
|
|
215
|
+
if args.json:
|
|
216
|
+
print(json.dumps(res, indent=2))
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
print(f"participants {res['participants']} (modelled {res['opponents_modelled']}) "
|
|
220
|
+
f"runs {res['runs']} my_lead {res['my_lead']} field_temp {res['field_temperature']}")
|
|
221
|
+
print(f"EV-max set: {', '.join(res['ev_max_set'])}")
|
|
222
|
+
print(f"P(win) all-EV-max : {res['p_win_ev_max']:.4f}")
|
|
223
|
+
if not res["flips"]:
|
|
224
|
+
print("greedy flips: none improved P(win) — EV-max is already best (small/easy field).")
|
|
225
|
+
else:
|
|
226
|
+
print("suggested flips (greedy, each raises P(win) most):")
|
|
227
|
+
for f in res["flips"]:
|
|
228
|
+
print(f" flip {f['match']} -> {f['to']} (EV cost {f['ev_cost']:+.3f}) "
|
|
229
|
+
f"P(win) {f['p_win_after']:.4f}")
|
|
230
|
+
print(f"P(win) after flips: {res['p_win_after_flips']:.4f} "
|
|
231
|
+
f"(+{res['p_win_after_flips'] - res['p_win_ev_max']:.4f})")
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
sys.exit(main())
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Exact-score EV optimiser for prediction-pool-optimizer.
|
|
3
|
+
|
|
4
|
+
Honest replacement for eyeballing the favourite — given each side's expected
|
|
5
|
+
goals (lambda) and the pool's scoring rule, this builds the full Poisson
|
|
6
|
+
score grid and computes the expected points of EVERY candidate tip, then
|
|
7
|
+
prints the EV-maximizing scoreline. It exists to kill two recurring failure
|
|
8
|
+
modes:
|
|
9
|
+
|
|
10
|
+
1. Hallucinated high scorelines (4:2, 1:4, 3:2 ...). Under any partial-points
|
|
11
|
+
rule these are almost never EV-max for a moderate favourite — the points
|
|
12
|
+
live in the tendency and goal-difference tiers, not the exact high score.
|
|
13
|
+
2. Under-tipped draws. A correctly-tipped draw banks the goal-difference
|
|
14
|
+
tier on every draw scoreline, so a 1:1 can beat a 1:0 in a close game.
|
|
15
|
+
The grid surfaces this; intuition does not.
|
|
16
|
+
|
|
17
|
+
The scoring model (configurable points per tier):
|
|
18
|
+
|
|
19
|
+
exact result → --exact (default 4)
|
|
20
|
+
goal diff → --diff (default 3) # same difference, not exact; draw-on-draw lands here
|
|
21
|
+
tendency → --tendency (default 2) # same W/D/L sign only
|
|
22
|
+
else → 0
|
|
23
|
+
|
|
24
|
+
For kicktipp's common "2 / 3 / 5" config run with --tendency 2 --diff 3 --exact 5.
|
|
25
|
+
|
|
26
|
+
It is an APPROXIMATION only in its goal model: a Poisson per side with the
|
|
27
|
+
provided lambdas, sides independent. That is the standard football scoreline
|
|
28
|
+
model and is robust to small lambda changes — but the lambdas themselves must
|
|
29
|
+
come from de-vigged consensus odds (see reference/odds-and-bonus.md), not a
|
|
30
|
+
guess. Feed it real numbers and the EV-max is exact for that model.
|
|
31
|
+
|
|
32
|
+
Input — either two lambdas on the CLI:
|
|
33
|
+
|
|
34
|
+
python3 scripts/prediction-pool/score_ev.py --lh 2.0 --la 0.7
|
|
35
|
+
python3 scripts/prediction-pool/score_ev.py --lh 0.6 --la 2.1 --tendency 2 --diff 3 --exact 5
|
|
36
|
+
|
|
37
|
+
or a JSON file of named matches (batch):
|
|
38
|
+
|
|
39
|
+
python3 scripts/prediction-pool/score_ev.py matches.json --tendency 2 --diff 3 --exact 5
|
|
40
|
+
|
|
41
|
+
matches.json:
|
|
42
|
+
[
|
|
43
|
+
{"match": "Senegal-Iraq", "lh": 2.0, "la": 0.7},
|
|
44
|
+
{"match": "Qatar-Switzerland", "lh": 0.6, "la": 2.1}
|
|
45
|
+
]
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import argparse
|
|
50
|
+
import json
|
|
51
|
+
import math
|
|
52
|
+
import sys
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
MAX_GOALS = 12 # truncation of the Poisson grid; tail beyond this is negligible
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _pois_pmf(k: int, rate: float) -> float:
|
|
59
|
+
if rate <= 0:
|
|
60
|
+
return 1.0 if k == 0 else 0.0
|
|
61
|
+
return math.exp(-rate) * rate ** k / math.factorial(k)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _sign(x: int) -> int:
|
|
65
|
+
return (x > 0) - (x < 0)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _score(th: int, ta: int, ah: int, aa: int,
|
|
69
|
+
pts_exact: float, pts_diff: float, pts_tend: float) -> float:
|
|
70
|
+
"""Points a tip (th:ta) earns against an actual result (ah:aa)."""
|
|
71
|
+
if th == ah and ta == aa:
|
|
72
|
+
return pts_exact
|
|
73
|
+
if (th - ta) == (ah - aa):
|
|
74
|
+
return pts_diff
|
|
75
|
+
if _sign(th - ta) == _sign(ah - aa):
|
|
76
|
+
return pts_tend
|
|
77
|
+
return 0.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def grid(lh: float, la: float, max_goals: int = MAX_GOALS):
|
|
81
|
+
"""Joint probability of every actual scoreline up to max_goals."""
|
|
82
|
+
ph = [_pois_pmf(k, lh) for k in range(max_goals + 1)]
|
|
83
|
+
pa = [_pois_pmf(k, la) for k in range(max_goals + 1)]
|
|
84
|
+
return [[ph[h] * pa[a] for a in range(max_goals + 1)] for h in range(max_goals + 1)]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ev_table(lh: float, la: float, pts_exact: float, pts_diff: float, pts_tend: float,
|
|
88
|
+
max_tip: int = 6, max_goals: int = MAX_GOALS):
|
|
89
|
+
"""EV (expected points) of every candidate tip up to max_tip goals/side."""
|
|
90
|
+
g = grid(lh, la, max_goals)
|
|
91
|
+
rows = []
|
|
92
|
+
for th in range(max_tip + 1):
|
|
93
|
+
for ta in range(max_tip + 1):
|
|
94
|
+
ev = 0.0
|
|
95
|
+
for ah in range(max_goals + 1):
|
|
96
|
+
for aa in range(max_goals + 1):
|
|
97
|
+
p = g[ah][aa]
|
|
98
|
+
if p <= 0:
|
|
99
|
+
continue
|
|
100
|
+
s = _score(th, ta, ah, aa, pts_exact, pts_diff, pts_tend)
|
|
101
|
+
if s:
|
|
102
|
+
ev += p * s
|
|
103
|
+
rows.append((th, ta, ev))
|
|
104
|
+
rows.sort(key=lambda r: r[2], reverse=True)
|
|
105
|
+
return rows, g
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _modal(g) -> tuple[int, int, float]:
|
|
109
|
+
best = (0, 0, 0.0)
|
|
110
|
+
for h in range(len(g)):
|
|
111
|
+
for a in range(len(g[h])):
|
|
112
|
+
if g[h][a] > best[2]:
|
|
113
|
+
best = (h, a, g[h][a])
|
|
114
|
+
return best
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _p_draw(g) -> float:
|
|
118
|
+
return sum(g[i][i] for i in range(len(g)))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def analyse(lh: float, la: float, pts_exact: float, pts_diff: float, pts_tend: float,
|
|
122
|
+
max_tip: int = 6, top: int = 6):
|
|
123
|
+
rows, g = ev_table(lh, la, pts_exact, pts_diff, pts_tend, max_tip)
|
|
124
|
+
mh, ma, mp = _modal(g)
|
|
125
|
+
return {
|
|
126
|
+
"lambda": [lh, la],
|
|
127
|
+
"rule": {"exact": pts_exact, "diff": pts_diff, "tendency": pts_tend},
|
|
128
|
+
"ev_max": {"tip": f"{rows[0][0]}:{rows[0][1]}", "ev": round(rows[0][2], 3)},
|
|
129
|
+
"modal_result": {"score": f"{mh}:{ma}", "prob": round(mp, 3)},
|
|
130
|
+
"p_draw": round(_p_draw(g), 3),
|
|
131
|
+
"ranked": [{"tip": f"{h}:{a}", "ev": round(ev, 3)} for h, a, ev in rows[:top]],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _print_one(name: str | None, res: dict) -> None:
|
|
136
|
+
if name:
|
|
137
|
+
print(f"\n## {name}")
|
|
138
|
+
lh, la = res["lambda"]
|
|
139
|
+
r = res["rule"]
|
|
140
|
+
print(f"lambda {lh}:{la} rule exact={r['exact']} diff={r['diff']} tendency={r['tendency']}")
|
|
141
|
+
print(f"EV-max tip : {res['ev_max']['tip']} (EV {res['ev_max']['ev']})")
|
|
142
|
+
print(f"modal score: {res['modal_result']['score']} (P {res['modal_result']['prob']}) "
|
|
143
|
+
f"P(draw) {res['p_draw']}")
|
|
144
|
+
print("ranked by EV:")
|
|
145
|
+
for row in res["ranked"]:
|
|
146
|
+
flag = " <- EV-max" if row["tip"] == res["ev_max"]["tip"] else ""
|
|
147
|
+
print(f" {row['tip']:>5} EV {row['ev']:.3f}{flag}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main(argv=None) -> int:
|
|
151
|
+
ap = argparse.ArgumentParser(description="Exact-score EV optimiser (Poisson grid).")
|
|
152
|
+
ap.add_argument("matches", nargs="?", help="JSON file of matches [{match,lh,la}]")
|
|
153
|
+
ap.add_argument("--lh", type=float, help="home expected goals (lambda)")
|
|
154
|
+
ap.add_argument("--la", type=float, help="away expected goals (lambda)")
|
|
155
|
+
ap.add_argument("--exact", type=float, default=4.0, help="points for exact result (default 4)")
|
|
156
|
+
ap.add_argument("--diff", type=float, default=3.0, help="points for correct goal difference (default 3)")
|
|
157
|
+
ap.add_argument("--tendency", type=float, default=2.0, help="points for correct tendency (default 2)")
|
|
158
|
+
ap.add_argument("--max-tip", type=int, default=6, help="max goals/side to consider as a tip")
|
|
159
|
+
ap.add_argument("--top", type=int, default=6, help="rows to print per match")
|
|
160
|
+
ap.add_argument("--json", action="store_true", help="emit JSON instead of text")
|
|
161
|
+
args = ap.parse_args(argv)
|
|
162
|
+
|
|
163
|
+
jobs: list[tuple[str | None, float, float]] = []
|
|
164
|
+
if args.matches:
|
|
165
|
+
data = json.loads(Path(args.matches).read_text())
|
|
166
|
+
for m in data:
|
|
167
|
+
jobs.append((m.get("match"), float(m["lh"]), float(m["la"])))
|
|
168
|
+
elif args.lh is not None and args.la is not None:
|
|
169
|
+
jobs.append((None, args.lh, args.la))
|
|
170
|
+
else:
|
|
171
|
+
ap.error("provide either a matches JSON file or --lh and --la")
|
|
172
|
+
|
|
173
|
+
out = []
|
|
174
|
+
for name, lh, la in jobs:
|
|
175
|
+
res = analyse(lh, la, args.exact, args.diff, args.tendency, args.max_tip, args.top)
|
|
176
|
+
if name:
|
|
177
|
+
res["match"] = name
|
|
178
|
+
out.append(res)
|
|
179
|
+
if not args.json:
|
|
180
|
+
_print_one(name, res)
|
|
181
|
+
|
|
182
|
+
if args.json:
|
|
183
|
+
print(json.dumps(out, indent=2))
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Session-profile staleness notice — `session_start` hook.
|
|
3
|
+
|
|
4
|
+
Phase 1 companion to the locked Phase-0.1 decision (option a, explicit
|
|
5
|
+
`/profile deactivate`). The `runtime.active_packs` overlay survives an IDE
|
|
6
|
+
restart, so this hook does **not** reset it — it only surfaces a one-line
|
|
7
|
+
**staleness notice** when a new session starts with an overlay carried over
|
|
8
|
+
from a previous session. Silently resetting on `session_start` is the
|
|
9
|
+
registry-refresh Catch-22 the council ruled out (see
|
|
10
|
+
`agents/settings/contexts/session-host-capability-audit.md`).
|
|
11
|
+
|
|
12
|
+
Contract: never blocks. Reads the JSON envelope on stdin (ignored — the
|
|
13
|
+
notice is derived from the overlay file), emits at most one stderr line,
|
|
14
|
+
returns 0 on every path.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Make `scripts` importable when invoked as a bare script path.
|
|
25
|
+
_REPO = Path(__file__).resolve().parent.parent
|
|
26
|
+
if str(_REPO) not in sys.path:
|
|
27
|
+
sys.path.insert(0, str(_REPO))
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from scripts.config import session_profiles
|
|
31
|
+
except Exception: # pragma: no cover - defensive; never block the loop
|
|
32
|
+
session_profiles = None # type: ignore
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _project_root() -> Path:
|
|
36
|
+
env = os.environ.get("CLAUDE_PROJECT_DIR") or os.environ.get("AGENT_CONFIG_PROJECT_DIR")
|
|
37
|
+
if env:
|
|
38
|
+
return Path(env)
|
|
39
|
+
return Path.cwd()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(argv: list[str] | None = None) -> int:
|
|
43
|
+
ap = argparse.ArgumentParser(description="Session-profile staleness notice (session_start).")
|
|
44
|
+
ap.add_argument("--root", default=None)
|
|
45
|
+
args, _ = ap.parse_known_args(argv)
|
|
46
|
+
|
|
47
|
+
# Drain stdin (the dispatcher passes a JSON envelope); we do not need it.
|
|
48
|
+
try:
|
|
49
|
+
if not sys.stdin.isatty():
|
|
50
|
+
sys.stdin.read()
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
if session_profiles is None:
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
root = Path(args.root) if args.root else _project_root()
|
|
58
|
+
try:
|
|
59
|
+
notice = session_profiles.stale_notice(root)
|
|
60
|
+
except Exception:
|
|
61
|
+
return 0 # fail-open — never block the session
|
|
62
|
+
|
|
63
|
+
if notice:
|
|
64
|
+
print(f"[profile] {notice}", file=sys.stderr)
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
raise SystemExit(main())
|
|
@@ -55,6 +55,21 @@ ROADMAP_PREFIX = "agents/roadmaps/"
|
|
|
55
55
|
ROADMAP_EXCLUDED_PARTS = frozenset({"archive", "skipped"})
|
|
56
56
|
DASHBOARD_PATH = "agents/roadmaps-progress.md"
|
|
57
57
|
|
|
58
|
+
REGEN_NAME = "update_roadmap_progress.py"
|
|
59
|
+
# Distributed-content script subtrees that may ship the regenerator,
|
|
60
|
+
# in priority order. Project-scoped installs land it under .augment/ or
|
|
61
|
+
# .agent-src/; the package itself carries the same projection.
|
|
62
|
+
DIST_SCRIPT_SUBDIRS = (
|
|
63
|
+
Path(".augment") / "scripts",
|
|
64
|
+
Path(".agent-src") / "scripts",
|
|
65
|
+
Path(".agent-src.uncondensed") / "scripts",
|
|
66
|
+
)
|
|
67
|
+
# Set by the dispatcher (scripts/hooks/dispatch_hook.py) to its own
|
|
68
|
+
# resolved package root, so a globally-installed binary (ADR-020
|
|
69
|
+
# global-only) can locate the shipped regenerator even when the consumer
|
|
70
|
+
# repo carries no project-local distributed content.
|
|
71
|
+
PACKAGE_ROOT_ENV_VAR = "AGENT_CONFIG_PACKAGE_ROOT"
|
|
72
|
+
|
|
58
73
|
|
|
59
74
|
def _candidate_paths(payload: dict) -> list[str]:
|
|
60
75
|
"""Pull every plausible file path out of a PostToolUse payload."""
|
|
@@ -115,15 +130,50 @@ def _is_roadmap_touch(path: str) -> bool:
|
|
|
115
130
|
return True
|
|
116
131
|
|
|
117
132
|
|
|
133
|
+
def _package_roots() -> list[Path]:
|
|
134
|
+
"""Package roots to search for the shipped regenerator, in priority
|
|
135
|
+
order, when the consumer carries no project-local copy.
|
|
136
|
+
|
|
137
|
+
A global-only consumer (ADR-020) never has `.augment/` / `.agent-src/`
|
|
138
|
+
in its repo — those trees are *distributed content*, which global-only
|
|
139
|
+
installs keep in the globally-installed package, not the project. The
|
|
140
|
+
regenerator therefore lives next to the running code, not next to the
|
|
141
|
+
edited roadmap.
|
|
142
|
+
|
|
143
|
+
1. ``AGENT_CONFIG_PACKAGE_ROOT`` — the dispatcher passes its own
|
|
144
|
+
resolved package root (``dispatch_hook.REPO_ROOT``). This is the
|
|
145
|
+
same root the dispatcher already trusts to locate this concern, so
|
|
146
|
+
it survives editable installs, plugin-cache moves, and symlinks
|
|
147
|
+
that a naive ``__file__`` walk would mis-resolve.
|
|
148
|
+
2. This hook's own location (``<pkg>/scripts/roadmap_progress_hook.py``
|
|
149
|
+
→ ``<pkg>``) — last-resort fallback for standalone invocation
|
|
150
|
+
outside the dispatcher.
|
|
151
|
+
"""
|
|
152
|
+
roots: list[Path] = []
|
|
153
|
+
env_root = os.environ.get(PACKAGE_ROOT_ENV_VAR, "").strip()
|
|
154
|
+
if env_root:
|
|
155
|
+
roots.append(Path(env_root).expanduser())
|
|
156
|
+
roots.append(Path(__file__).resolve().parent.parent)
|
|
157
|
+
return roots
|
|
158
|
+
|
|
159
|
+
|
|
118
160
|
def _resolve_regenerator(consumer_root: Path) -> Path | None:
|
|
119
|
-
"""Find the regenerator script
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
161
|
+
"""Find the regenerator script.
|
|
162
|
+
|
|
163
|
+
Project-local copy first (project-scoped installs), then the package
|
|
164
|
+
the hook itself ships in (global-only consumers, per ADR-020 — the
|
|
165
|
+
repo has no project-local distributed content). Returns ``None`` only
|
|
166
|
+
when no copy exists in either place.
|
|
167
|
+
"""
|
|
168
|
+
for subdir in DIST_SCRIPT_SUBDIRS:
|
|
169
|
+
candidate = consumer_root / subdir / REGEN_NAME
|
|
125
170
|
if candidate.is_file():
|
|
126
171
|
return candidate
|
|
172
|
+
for root in _package_roots():
|
|
173
|
+
for subdir in DIST_SCRIPT_SUBDIRS:
|
|
174
|
+
candidate = root / subdir / REGEN_NAME
|
|
175
|
+
if candidate.is_file():
|
|
176
|
+
return candidate
|
|
127
177
|
return None
|
|
128
178
|
|
|
129
179
|
|
|
@@ -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,
|