@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
@@ -104,5 +104,5 @@ echo " Remove: rm ${TARGET_FILE}"
104
104
  echo
105
105
  echo "ℹ️ Key install ≠ enable. To use this provider in /council:"
106
106
  echo " set ai_council.enabled: true and ai_council.members.anthropic.enabled: true"
107
- echo " in .agent-settings.yml. Council is a 'full' cost_profile feature; under"
107
+ echo " in .agent-settings.yml. Council is a 'full' rule_loading_tier feature; under"
108
108
  echo " 'minimal' / 'balanced' the runtime hooks stay inactive."
@@ -104,5 +104,5 @@ echo " Remove: rm ${TARGET_FILE}"
104
104
  echo
105
105
  echo "ℹ️ Key install ≠ enable. To use this provider in /council:"
106
106
  echo " set ai_council.enabled: true and ai_council.members.openai.enabled: true"
107
- echo " in .agent-settings.yml. Council is a 'full' cost_profile feature; under"
107
+ echo " in .agent-settings.yml. Council is a 'full' rule_loading_tier feature; under"
108
108
  echo " 'minimal' / 'balanced' the runtime hooks stay inactive."
@@ -45,7 +45,12 @@ try:
45
45
  except ImportError:
46
46
  script_output = None # graceful fallback when running outside repo
47
47
 
48
- CORE_SRC = REPO_ROOT / "packages" / "core" / ".agent-src.uncondensed"
48
+ from _lib.agent_src import resolve_package_core_path # noqa: E402
49
+
50
+ CORE_SRC = resolve_package_core_path(".agent-src.uncondensed")
51
+ # Enforced packages/core targets — read by scripts/check_gate_paths.py so a
52
+ # future move that desyncs this path fails CI instead of silently no-opping.
53
+ GATE_CORE_PATHS = (CORE_SRC,)
49
54
  DIRECTIVES_ROOT = CORE_SRC / "templates" / "scripts" / "work_engine" / "directives"
50
55
  EVIDENCE_DIR = REPO_ROOT / "agents" / "evidence" / "analysis"
51
56
 
@@ -23,8 +23,18 @@ from dataclasses import dataclass
23
23
  from pathlib import Path
24
24
 
25
25
  ROOT = Path(__file__).resolve().parent.parent
26
+ sys.path.insert(0, str(ROOT / "scripts"))
27
+ from _lib.agent_src import resolve_package_core_path # noqa: E402
28
+
26
29
  QUIET = "--quiet" in sys.argv
27
30
 
31
+ _CONSUMER_TEMPLATE = resolve_package_core_path(".agent-src.uncondensed/templates/AGENTS.md")
32
+ # Enforced packages/core target — read by scripts/check_gate_paths.py so a
33
+ # future move that desyncs this path fails CI instead of silently no-opping.
34
+ # Only the consumer-template lives under packages/core; the package-root
35
+ # AGENTS.md sits at the repo root and is intentionally excluded.
36
+ GATE_CORE_PATHS = (_CONSUMER_TEMPLATE,)
37
+
28
38
  LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
29
39
  PATH_BACKTICK_RE = re.compile(r"`[^`]*/[^`]*`")
30
40
  BULLET_RE = re.compile(r"^\s*[-*+]\s+")
@@ -64,10 +74,7 @@ class Target:
64
74
 
65
75
  TARGETS = [
66
76
  Target(ROOT / "AGENTS.md", "package-root", 3000, 2800, template=False),
67
- Target(
68
- ROOT / "packages" / "core" / ".agent-src.uncondensed" / "templates" / "AGENTS.md",
69
- "consumer-template", 2500, 2300, template=True,
70
- ),
77
+ Target(_CONSUMER_TEMPLATE, "consumer-template", 2500, 2300, template=True),
71
78
  ]
72
79
 
73
80
 
@@ -10,7 +10,7 @@ every pack listed as a workspace's default/optional MUST list that
10
10
  workspace in its own `workspaces:` array (and vice versa).
11
11
 
12
12
  Non-overlap (ADR-010 alignment): no pack id may collide with a
13
- `cost_profile` value (minimal/balanced/full/custom) or a `profile.id`
13
+ `rule_loading_tier` value (minimal/balanced/full/custom) or a `profile.id`
14
14
  value (founder/developer/content_creator/agency/finance/ops).
15
15
 
16
16
  Cap: ≤ 150 LOC, stdlib + PyYAML. Exit 0 clean, 1 on failure.
@@ -41,11 +41,11 @@ ADR_PACKS: frozenset[str] = frozenset({
41
41
  "typescript", "react", "nextjs", "python", "product-basic",
42
42
  "product-discovery", "finance-basic", "finance-advanced",
43
43
  "gtm-sales", "gtm-marketing", "ops-people", "founder-strategy", "small-business",
44
- "construction", "ai-video", "meta",
44
+ "construction", "ai-video", "fun", "meta",
45
45
  })
46
46
 
47
47
  # ADR-010 non-overlap reservations.
48
- COST_PROFILE_RESERVED: frozenset[str] = frozenset({
48
+ RULE_LOADING_TIER_RESERVED: frozenset[str] = frozenset({
49
49
  "minimal", "balanced", "full", "custom",
50
50
  })
51
51
  PROFILE_ID_RESERVED: frozenset[str] = frozenset({
@@ -136,9 +136,9 @@ def lint(quiet: bool) -> int:
136
136
  )
137
137
 
138
138
  # 5. Non-overlap (ADR-010).
139
- overlap_cost = pack_ids & COST_PROFILE_RESERVED
139
+ overlap_cost = pack_ids & RULE_LOADING_TIER_RESERVED
140
140
  if overlap_cost:
141
- errors.append(f"pack ids collide with cost_profile values: {sorted(overlap_cost)}")
141
+ errors.append(f"pack ids collide with rule_loading_tier values: {sorted(overlap_cost)}")
142
142
  overlap_profile = pack_ids & PROFILE_ID_RESERVED
143
143
  if overlap_profile:
144
144
  errors.append(f"pack ids collide with profile.id values: {sorted(overlap_profile)}")
@@ -49,10 +49,14 @@ import argparse
49
49
  import re
50
50
  import sys
51
51
  from pathlib import Path
52
+ try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
53
+ from scripts._lib.agent_settings import project_settings_path
54
+ except ModuleNotFoundError: # pragma: no cover
55
+ from _lib.agent_settings import project_settings_path
52
56
 
53
57
  REPO_ROOT = Path(__file__).resolve().parent.parent
54
58
  DEFAULT_MANIFEST = REPO_ROOT / "scripts" / "hook_manifest.yaml"
55
- DEFAULT_SETTINGS = REPO_ROOT / ".agent-settings.yml"
59
+ DEFAULT_SETTINGS = project_settings_path(REPO_ROOT)
56
60
 
57
61
  DEFAULT_MAX_PER_EVENT = 8
58
62
  DEFAULT_TIER1: list[str] = []
@@ -25,7 +25,14 @@ from pathlib import Path
25
25
  ROOT = Path(".")
26
26
  MARKETPLACE = ROOT / ".claude-plugin" / "marketplace.json"
27
27
  PACKAGE_JSON = ROOT / "package.json"
28
- CLAUDE_SKILLS_DIR = ROOT / ".claude" / "skills"
28
+ # Committed marketplace skill sources (git-consumed): real skills resolve to
29
+ # .agent-src/skills/<name>; command-as-skill entries to the committed
30
+ # .claude-plugin/skills/<slug> projection. .claude/skills/ is a gitignored
31
+ # local channel and is intentionally NOT a marketplace source.
32
+ SKILL_SOURCE_DIRS = (
33
+ ROOT / ".agent-src" / "skills",
34
+ ROOT / ".claude-plugin" / "skills",
35
+ )
29
36
 
30
37
 
31
38
  def fail(errors: list[str]) -> int:
@@ -124,9 +131,10 @@ def main() -> int:
124
131
  if not skill_md.exists():
125
132
  errors.append(f"{entry} has no SKILL.md: `{path}`")
126
133
 
127
- # Reverse-completeness: every SKILL.md on disk under .claude/skills/
128
- # must appear in some plugin's skills[]. Catches the drift where new
129
- # skills are generated but never added to the marketplace manifest.
134
+ # Reverse-completeness: every SKILL.md on disk under the committed skill
135
+ # sources (.agent-src/skills/ + .claude-plugin/skills/) must appear in some
136
+ # plugin's skills[]. Catches the drift where new skills/commands are
137
+ # generated but never added to the marketplace manifest.
130
138
  listed: set[str] = set()
131
139
  for plugin in plugins:
132
140
  if not isinstance(plugin, dict):
@@ -135,13 +143,16 @@ def main() -> int:
135
143
  if isinstance(path, str):
136
144
  listed.add(path.removeprefix("./"))
137
145
 
138
- if CLAUDE_SKILLS_DIR.exists():
139
- for skill_dir in sorted(CLAUDE_SKILLS_DIR.iterdir()):
146
+ for source_dir in SKILL_SOURCE_DIRS:
147
+ if not source_dir.exists():
148
+ continue
149
+ prefix = source_dir.relative_to(ROOT).as_posix()
150
+ for skill_dir in sorted(source_dir.iterdir()):
140
151
  if not skill_dir.is_dir():
141
152
  continue
142
153
  if not (skill_dir / "SKILL.md").exists():
143
154
  continue
144
- rel = f".claude/skills/{skill_dir.name}"
155
+ rel = f"{prefix}/{skill_dir.name}"
145
156
  if rel not in listed:
146
157
  errors.append(
147
158
  f"skill exists on disk but is not listed in marketplace.json: "
@@ -22,12 +22,16 @@ from __future__ import annotations
22
22
  import re
23
23
  import sys
24
24
  from pathlib import Path
25
+ try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
26
+ from scripts._lib.agent_settings import project_settings_path
27
+ except ModuleNotFoundError: # pragma: no cover
28
+ from _lib.agent_settings import project_settings_path
25
29
 
26
30
  QUIET = "--quiet" in sys.argv
27
31
 
28
32
  REPO_ROOT = Path(__file__).resolve().parent.parent
29
33
  ROADMAP_GLOB = "agents/roadmaps/*.md"
30
- SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
34
+ SETTINGS_FILE = project_settings_path(REPO_ROOT)
31
35
  LOCAL_AUTO_RUN_PAT = re.compile(
32
36
  r"^\s*local_auto_run:\s*(true|false)\s*(?:#.*)?$", re.MULTILINE
33
37
  )
@@ -23,6 +23,10 @@ from __future__ import annotations
23
23
  import re
24
24
  import sys
25
25
  from pathlib import Path
26
+ try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
27
+ from scripts._lib.agent_settings import project_settings_path
28
+ except ModuleNotFoundError: # pragma: no cover
29
+ from _lib.agent_settings import project_settings_path
26
30
 
27
31
  QUIET = "--quiet" in sys.argv
28
32
 
@@ -30,7 +34,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
30
34
  ROADMAP_GLOB = "agents/roadmaps/*.md"
31
35
  LIGHTWEIGHT_LINE_CAP = 600
32
36
  LIGHTWEIGHT_PHASE_CAP = 6
33
- SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
37
+ SETTINGS_FILE = project_settings_path(REPO_ROOT)
34
38
  HORIZON_WEEKS_PAT = re.compile(
35
39
  r"^\s*horizon_weeks:\s*(\d+)\s*(?:#.*)?$", re.MULTILINE
36
40
  )
@@ -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:
@@ -21,6 +21,10 @@ from __future__ import annotations
21
21
  import dataclasses
22
22
  from dataclasses import dataclass, field
23
23
  from pathlib import Path
24
+ try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
25
+ from scripts._lib.agent_settings import project_settings_path
26
+ except ModuleNotFoundError: # pragma: no cover
27
+ from _lib.agent_settings import project_settings_path
24
28
  from typing import Any, Literal
25
29
 
26
30
  # Phase 1 hand-picked skills — kept for the Phase-1 entrypoint
@@ -128,7 +132,7 @@ def _load_active_user_type(root: Path) -> str:
128
132
  the loader (consistent with `_strip_frontmatter`). Only matches
129
133
  `user_type:` directly under the top-level `personal:` block.
130
134
  """
131
- settings = root / ".agent-settings.yml"
135
+ settings = project_settings_path(root)
132
136
  if not settings.is_file():
133
137
  return ""
134
138
  try:
@@ -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())