@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.
Files changed (225) hide show
  1. package/.agent-src/commands/agent-handoff.md +1 -1
  2. package/.agent-src/commands/agent-status.md +1 -1
  3. package/.agent-src/commands/agents/audit.md +1 -1
  4. package/.agent-src/commands/agents/init.md +1 -1
  5. package/.agent-src/commands/agents/user/accept.md +3 -3
  6. package/.agent-src/commands/agents/user/init.md +4 -4
  7. package/.agent-src/commands/agents/user/show.md +3 -3
  8. package/.agent-src/commands/agents/user/update.md +3 -3
  9. package/.agent-src/commands/agents/user.md +1 -1
  10. package/.agent-src/commands/agents.md +1 -1
  11. package/.agent-src/commands/analytics/prune.md +1 -1
  12. package/.agent-src/commands/analytics/show.md +1 -1
  13. package/.agent-src/commands/analytics.md +1 -1
  14. package/.agent-src/commands/bug-fix.md +1 -1
  15. package/.agent-src/commands/challenge-me.md +1 -1
  16. package/.agent-src/commands/chat-history/import.md +1 -1
  17. package/.agent-src/commands/chat-history/learn.md +1 -1
  18. package/.agent-src/commands/chat-history/show.md +1 -1
  19. package/.agent-src/commands/chat-history.md +1 -1
  20. package/.agent-src/commands/check-current-md.md +1 -1
  21. package/.agent-src/commands/condense.md +1 -1
  22. package/.agent-src/commands/context.md +1 -1
  23. package/.agent-src/commands/cost-report.md +13 -8
  24. package/.agent-src/commands/council.md +3 -3
  25. package/.agent-src/commands/create-pr/description-only.md +1 -1
  26. package/.agent-src/commands/create-pr.md +1 -1
  27. package/.agent-src/commands/e2e-heal.md +1 -1
  28. package/.agent-src/commands/e2e-plan.md +1 -1
  29. package/.agent-src/commands/feature.md +1 -1
  30. package/.agent-src/commands/fix/ci.md +1 -1
  31. package/.agent-src/commands/fix/portability.md +1 -1
  32. package/.agent-src/commands/fix/pr-bot-comments.md +1 -1
  33. package/.agent-src/commands/fix/pr-comments.md +1 -1
  34. package/.agent-src/commands/fix/pr-developer-comments.md +1 -1
  35. package/.agent-src/commands/fix/refs.md +1 -1
  36. package/.agent-src/commands/fix/seeder.md +1 -1
  37. package/.agent-src/commands/fix.md +1 -1
  38. package/.agent-src/commands/judge.md +1 -1
  39. package/.agent-src/commands/knowledge/cross-repo.md +1 -1
  40. package/.agent-src/commands/knowledge/forget.md +1 -1
  41. package/.agent-src/commands/knowledge/ingest.md +1 -1
  42. package/.agent-src/commands/knowledge/list.md +1 -1
  43. package/.agent-src/commands/knowledge.md +1 -1
  44. package/.agent-src/commands/memory/add.md +1 -1
  45. package/.agent-src/commands/memory/learn-low-impact.md +1 -1
  46. package/.agent-src/commands/memory/load.md +1 -1
  47. package/.agent-src/commands/memory/mine-session.md +1 -1
  48. package/.agent-src/commands/memory/promote.md +1 -1
  49. package/.agent-src/commands/memory/propose.md +1 -1
  50. package/.agent-src/commands/memory.md +1 -1
  51. package/.agent-src/commands/mode.md +1 -1
  52. package/.agent-src/commands/optimize/agents-dir.md +1 -1
  53. package/.agent-src/commands/optimize/augmentignore.md +1 -1
  54. package/.agent-src/commands/optimize/rtk.md +1 -1
  55. package/.agent-src/commands/optimize/skills.md +1 -1
  56. package/.agent-src/commands/optimize.md +1 -1
  57. package/.agent-src/commands/orchestrate.md +1 -1
  58. package/.agent-src/commands/override/create.md +1 -1
  59. package/.agent-src/commands/override/manage.md +1 -1
  60. package/.agent-src/commands/override.md +1 -1
  61. package/.agent-src/commands/package-reset.md +1 -1
  62. package/.agent-src/commands/prediction-pool.md +234 -0
  63. package/.agent-src/commands/profile/activate.md +81 -0
  64. package/.agent-src/commands/profile/deactivate.md +68 -0
  65. package/.agent-src/commands/profile/show.md +70 -0
  66. package/.agent-src/commands/profile.md +68 -0
  67. package/.agent-src/commands/project-health.md +1 -1
  68. package/.agent-src/commands/quality-fix.md +1 -1
  69. package/.agent-src/commands/roadmap/process-full.md +1 -1
  70. package/.agent-src/commands/roadmap/process-phase.md +1 -1
  71. package/.agent-src/commands/roadmap/process-step.md +1 -1
  72. package/.agent-src/commands/roadmap.md +1 -1
  73. package/.agent-src/commands/set-cost-profile.md +9 -9
  74. package/.agent-src/commands/skill/preview.md +3 -3
  75. package/.agent-src/commands/skill.md +1 -1
  76. package/.agent-src/commands/skills/discover.md +1 -1
  77. package/.agent-src/commands/skills.md +1 -1
  78. package/.agent-src/commands/sync-agent-settings.md +3 -3
  79. package/.agent-src/commands/sync-gitignore/fix.md +1 -1
  80. package/.agent-src/commands/sync-gitignore.md +1 -1
  81. package/.agent-src/commands/update-form-request-messages.md +1 -1
  82. package/.agent-src/presets/README.md +1 -1
  83. package/.agent-src/profiles/README.md +1 -1
  84. package/.agent-src/rules/non-destructive-by-default.md +2 -1
  85. package/.agent-src/skills/check-refs/SKILL.md +1 -1
  86. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
  87. package/.agent-src/skills/git-workflow/SKILL.md +1 -1
  88. package/.agent-src/skills/jira-integration/SKILL.md +1 -1
  89. package/.agent-src/skills/markitdown/SKILL.md +1 -1
  90. package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +314 -0
  91. package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +20 -0
  92. package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +175 -0
  93. package/.agent-src/skills/prediction-pool-optimizer/reference/odds-and-bonus.md +109 -0
  94. package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -1
  95. package/.agent-src/skills/script-writing/SKILL.md +1 -1
  96. package/.agent-src/skills/token-optimizer/SKILL.md +1 -1
  97. package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
  98. package/.agent-src/templates/agent-settings.md +7 -7
  99. package/.agent-src/templates/agents/agent-project-settings.example.yml +2 -2
  100. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +54 -6
  101. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +1 -1
  102. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +9 -7
  103. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +9 -10
  104. package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +17 -4
  105. package/.claude-plugin/marketplace.json +370 -364
  106. package/CHANGELOG.md +108 -0
  107. package/README.md +2 -2
  108. package/config/agent-settings.template.yml +11 -2
  109. package/config/discovery/packs.yml +11 -0
  110. package/config/discovery/session-profiles.yml +37 -0
  111. package/config/discovery/workspaces.yml +1 -1
  112. package/config/profiles/balanced.ini +1 -1
  113. package/config/profiles/full.ini +1 -1
  114. package/config/profiles/minimal.ini +1 -1
  115. package/dist/discovery/deprecation-report.md +1 -1
  116. package/dist/discovery/discovery-manifest.json +254 -100
  117. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  118. package/dist/discovery/discovery-manifest.summary.md +4 -3
  119. package/dist/discovery/orphan-report.md +1 -1
  120. package/dist/discovery/packs.json +41 -6
  121. package/dist/discovery/trust-report.md +3 -3
  122. package/dist/discovery/workspaces.json +19 -6
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/server/io/substituteTemplate.js +3 -3
  125. package/dist/server/io/substituteTemplate.js.map +1 -1
  126. package/dist/server/routes/settings.js +2 -2
  127. package/dist/server/routes/settings.js.map +1 -1
  128. package/dist/server/schemas/settings.js +4 -2
  129. package/dist/server/schemas/settings.js.map +1 -1
  130. package/dist/ui/assets/{index-DVsyUMZe.js → index-5lFqAKL0.js} +2 -2
  131. package/dist/ui/assets/index-5lFqAKL0.js.map +1 -0
  132. package/dist/ui/index.html +1 -1
  133. package/docs/architecture/current-onboard-baseline.md +3 -3
  134. package/docs/architecture.md +2 -2
  135. package/docs/catalog.md +11 -5
  136. package/docs/contracts/adr-level-6-productization.md +1 -1
  137. package/docs/contracts/command-clusters.md +2 -0
  138. package/docs/contracts/config-presets.md +2 -2
  139. package/docs/contracts/cost-profile-defaults.md +5 -5
  140. package/docs/contracts/discovery-manifest.schema.json +1 -1
  141. package/docs/contracts/explain-trace.schema.json +3 -3
  142. package/docs/contracts/memory-visibility-v1.md +15 -7
  143. package/docs/contracts/profile-system.md +2 -2
  144. package/docs/contracts/session-profile-overlay.md +120 -0
  145. package/docs/contracts/settings-api.md +3 -3
  146. package/docs/contracts/value-report-schema.md +14 -1
  147. package/docs/customization.md +47 -5
  148. package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +47 -11
  149. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +16 -2
  150. package/docs/decisions/ADR-034-per-skill-model-recommendation-transport.md +1 -1
  151. package/docs/decisions/ADR-036-global-install-browser-wizard-handoff.md +106 -0
  152. package/docs/decisions/ADR-037-cost-profile-untangle.md +117 -0
  153. package/docs/decisions/ADR-038-canonical-settings-path.md +66 -0
  154. package/docs/decisions/ADR-039-claude-skills-untracked.md +139 -0
  155. package/docs/decisions/ADR-rule-kernel-and-router.md +1 -1
  156. package/docs/decisions/INDEX.md +4 -0
  157. package/docs/development.md +12 -0
  158. package/docs/getting-started.md +2 -2
  159. package/docs/guidelines/agent-infra/layered-settings.md +10 -4
  160. package/docs/installation.md +3 -3
  161. package/docs/setup/mcp-client-config.md +1 -1
  162. package/docs/skills-catalog.md +5 -1
  163. package/docs/value.md +9 -7
  164. package/docs/wizard.md +1 -1
  165. package/llms.txt +4 -0
  166. package/package.json +1 -1
  167. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  168. package/scripts/_cli/cmd_doctor.py +3 -2
  169. package/scripts/_cli/cmd_explain.py +1 -1
  170. package/scripts/_cli/cmd_versions.py +2 -2
  171. package/scripts/_cli/explain_last/inputs.py +11 -8
  172. package/scripts/_cli/explain_last/sections/inputs.py +1 -1
  173. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  174. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  175. package/scripts/_lib/agent_settings.py +54 -6
  176. package/scripts/_lib/agent_src.py +30 -0
  177. package/scripts/_lib/value_ladder.py +99 -2
  178. package/scripts/_lib/value_report.py +30 -16
  179. package/scripts/ai_council/modes.py +1 -1
  180. package/scripts/ai_council/session.py +5 -1
  181. package/scripts/audit_command_surface.py +7 -1
  182. package/scripts/audit_initial_context.py +26 -2
  183. package/scripts/check_gate_paths.py +117 -0
  184. package/scripts/check_references.py +51 -2
  185. package/scripts/check_skill_requires.py +143 -0
  186. package/scripts/check_test_coverage_diff.py +180 -0
  187. package/scripts/compile_router.py +5 -1
  188. package/scripts/condense.py +92 -4
  189. package/scripts/config/session_profiles.py +492 -0
  190. package/scripts/council_cli.py +5 -1
  191. package/scripts/first-run.sh +11 -11
  192. package/scripts/hook_manifest.yaml +15 -7
  193. package/scripts/hooks/dispatch_hook.py +8 -0
  194. package/scripts/install +14 -1
  195. package/scripts/install-hooks.sh +2 -1
  196. package/scripts/install.py +203 -433
  197. package/scripts/install_anthropic_key.sh +1 -1
  198. package/scripts/install_openai_key.sh +1 -1
  199. package/scripts/inventory_abstraction_budget.py +6 -1
  200. package/scripts/lint_agents_md.py +11 -4
  201. package/scripts/lint_discovery_vocabulary.py +5 -5
  202. package/scripts/lint_hook_concern_budget.py +5 -1
  203. package/scripts/lint_marketplace.py +18 -7
  204. package/scripts/lint_roadmap_ci_steps.py +5 -1
  205. package/scripts/lint_roadmap_complexity.py +5 -1
  206. package/scripts/lint_value_dashboard.py +1 -1
  207. package/scripts/mcp_server/prompts.py +5 -1
  208. package/scripts/prediction-pool/adapters/_schema.md +42 -0
  209. package/scripts/prediction-pool/adapters/kicktipp.yml +23 -0
  210. package/scripts/prediction-pool/poisson_sim.py +167 -0
  211. package/scripts/prediction-pool/pool_winsim.py +236 -0
  212. package/scripts/prediction-pool/score_ev.py +188 -0
  213. package/scripts/profile_staleness_hook.py +69 -0
  214. package/scripts/render_value_md.py +1 -0
  215. package/scripts/roadmap_progress_hook.py +56 -6
  216. package/scripts/schemas/agent-settings.schema.json +77 -0
  217. package/scripts/schemas/skill.schema.json +7 -0
  218. package/scripts/smoke_quickstart.py +7 -6
  219. package/scripts/sync_agent_settings.py +12 -5
  220. package/scripts/validate_agent_settings.py +124 -0
  221. package/scripts/validate_decision_engine.py +5 -1
  222. package/templates/minimal/.agent-settings.yml +1 -1
  223. package/dist/ui/assets/index-DVsyUMZe.js.map +0 -1
  224. package/scripts/measure_roadmap_trajectory.py +0 -112
  225. 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())
@@ -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
 
@@ -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 — package-shipped or installed copy."""
120
- for candidate in (
121
- consumer_root / ".augment" / "scripts" / "update_roadmap_progress.py",
122
- consumer_root / ".agent-src" / "scripts" / "update_roadmap_progress.py",
123
- consumer_root / ".agent-src.uncondensed" / "scripts" / "update_roadmap_progress.py",
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,