@event4u/agent-config 5.7.0 → 5.9.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 (164) 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 +1 -1
  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 +31 -12
  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 +1 -1
  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 +1 -1
  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/skills/check-refs/SKILL.md +1 -1
  83. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
  84. package/.agent-src/skills/git-workflow/SKILL.md +1 -1
  85. package/.agent-src/skills/jira-integration/SKILL.md +1 -1
  86. package/.agent-src/skills/markitdown/SKILL.md +1 -1
  87. package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +195 -77
  88. package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +3 -1
  89. package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +111 -16
  90. package/.agent-src/skills/prediction-pool-optimizer/reference/odds-and-bonus.md +109 -0
  91. package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -1
  92. package/.agent-src/skills/script-writing/SKILL.md +1 -1
  93. package/.agent-src/skills/token-optimizer/SKILL.md +1 -1
  94. package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
  95. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  96. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +52 -5
  97. package/.claude-plugin/marketplace.json +370 -366
  98. package/CHANGELOG.md +77 -0
  99. package/README.md +2 -2
  100. package/config/discovery/session-profiles.yml +37 -0
  101. package/dist/discovery/deprecation-report.md +1 -1
  102. package/dist/discovery/discovery-manifest.json +183 -95
  103. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  104. package/dist/discovery/discovery-manifest.summary.md +3 -3
  105. package/dist/discovery/orphan-report.md +1 -1
  106. package/dist/discovery/packs.json +9 -5
  107. package/dist/discovery/trust-report.md +2 -2
  108. package/dist/discovery/workspaces.json +8 -4
  109. package/dist/mcp/registry-manifest.json +3 -3
  110. package/docs/architecture.md +1 -1
  111. package/docs/catalog.md +7 -3
  112. package/docs/contracts/command-clusters.md +2 -0
  113. package/docs/contracts/session-profile-overlay.md +120 -0
  114. package/docs/customization.md +26 -0
  115. package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +36 -0
  116. package/docs/decisions/ADR-038-canonical-settings-path.md +66 -0
  117. package/docs/decisions/ADR-039-claude-skills-untracked.md +139 -0
  118. package/docs/decisions/INDEX.md +2 -0
  119. package/docs/development.md +12 -0
  120. package/docs/getting-started.md +1 -1
  121. package/docs/guidelines/agent-infra/layered-settings.md +8 -2
  122. package/docs/skills-catalog.md +5 -1
  123. package/llms.txt +4 -0
  124. package/package.json +1 -1
  125. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  126. package/scripts/_cli/cmd_doctor.py +180 -16
  127. package/scripts/_cli/cmd_versions.py +2 -2
  128. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  129. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  130. package/scripts/_lib/agent_settings.py +52 -5
  131. package/scripts/_lib/agent_src.py +30 -0
  132. package/scripts/ai_council/session.py +5 -1
  133. package/scripts/audit_command_surface.py +7 -1
  134. package/scripts/audit_initial_context.py +10 -2
  135. package/scripts/check_gate_paths.py +117 -0
  136. package/scripts/check_references.py +51 -2
  137. package/scripts/check_release_published.py +145 -0
  138. package/scripts/check_test_coverage_diff.py +180 -0
  139. package/scripts/compile_router.py +5 -1
  140. package/scripts/condense.py +79 -2
  141. package/scripts/config/session_profiles.py +492 -0
  142. package/scripts/council_cli.py +5 -1
  143. package/scripts/hook_manifest.yaml +15 -7
  144. package/scripts/hooks/dispatch_hook.py +8 -0
  145. package/scripts/install-hooks.sh +2 -1
  146. package/scripts/install.py +76 -5
  147. package/scripts/inventory_abstraction_budget.py +6 -1
  148. package/scripts/lint_agents_md.py +11 -4
  149. package/scripts/lint_hook_concern_budget.py +5 -1
  150. package/scripts/lint_marketplace.py +18 -7
  151. package/scripts/lint_roadmap_ci_steps.py +5 -1
  152. package/scripts/lint_roadmap_complexity.py +5 -1
  153. package/scripts/mcp_server/prompts.py +5 -1
  154. package/scripts/prediction-pool/pool_winsim.py +236 -0
  155. package/scripts/prediction-pool/score_ev.py +188 -0
  156. package/scripts/profile_staleness_hook.py +69 -0
  157. package/scripts/release.py +54 -31
  158. package/scripts/roadmap_progress_hook.py +56 -6
  159. package/scripts/smoke_quickstart.py +3 -2
  160. package/scripts/sync_agent_settings.py +8 -3
  161. package/scripts/validate_agent_settings.py +5 -1
  162. package/scripts/validate_decision_engine.py +5 -1
  163. package/scripts/measure_roadmap_trajectory.py +0 -112
  164. package/scripts/verify_roadmap_closure.py +0 -327
@@ -58,6 +58,28 @@ SETTINGS_FILE = ".agent-settings.yml"
58
58
  LEGACY_SETTINGS_FILE = ".agent-settings"
59
59
  LEGACY_BACKUP_FILE = ".agent-settings.backup.key-value"
60
60
 
61
+ # Canonical project settings live in the settings layer (agents/settings/),
62
+ # not at the repo root (ADR-038). The repo-root .agent-settings.yml is a
63
+ # back-compat read-fallback that install migrates into the canonical
64
+ # location. Kept inline (no package import) — install.py runs standalone.
65
+ SETTINGS_SUBDIR = ("agents", "settings")
66
+
67
+
68
+ def _canonical_settings_target(project_root: Path) -> Path:
69
+ """Canonical write target: <root>/agents/settings/.agent-settings.yml."""
70
+ return project_root.joinpath(*SETTINGS_SUBDIR, SETTINGS_FILE)
71
+
72
+
73
+ def _resolve_settings_read(project_root: Path) -> Path:
74
+ """Canonical if present, else legacy repo-root file if present, else canonical."""
75
+ canonical = _canonical_settings_target(project_root)
76
+ if canonical.exists():
77
+ return canonical
78
+ legacy = project_root / SETTINGS_FILE
79
+ if legacy.exists():
80
+ return legacy
81
+ return canonical
82
+
61
83
  # Maps legacy flat keys (.agent-settings, key=value) to the new dotted YAML
62
84
  # paths in .agent-settings.yml. Applied once during auto-migration.
63
85
  LEGACY_RENAME_MAP = {
@@ -567,7 +589,7 @@ def ensure_agent_settings(
567
589
  user_type: str = "",
568
590
  packs: "list[str] | None" = None,
569
591
  ) -> None:
570
- target = project_root / SETTINGS_FILE
592
+ target = _canonical_settings_target(project_root)
571
593
  profile_source = package_root / "config" / "profiles" / f"{profile}.ini"
572
594
  template_source = package_root / "config" / "agent-settings.template.yml"
573
595
 
@@ -592,6 +614,17 @@ def ensure_agent_settings(
592
614
  template_body = _render_template(template, profile_values)
593
615
  template_body = _inject_packs(template_body, packs or [])
594
616
 
617
+ # ADR-038: relocate an existing repo-root .agent-settings.yml into the
618
+ # canonical agents/settings/ location, preserving the developer's content
619
+ # (never clobber an already-canonical file).
620
+ legacy_root = project_root / SETTINGS_FILE
621
+ if legacy_root.is_file() and not target.exists():
622
+ target.parent.mkdir(parents=True, exist_ok=True)
623
+ target.write_text(legacy_root.read_text(encoding="utf-8"), encoding="utf-8")
624
+ legacy_root.unlink()
625
+ success(f"Migrated {SETTINGS_FILE} → agents/settings/{SETTINGS_FILE} (ADR-038)")
626
+ return
627
+
595
628
  legacy_target = project_root / LEGACY_SETTINGS_FILE
596
629
  if legacy_target.is_file() and target.exists():
597
630
  warn(
@@ -611,6 +644,7 @@ def ensure_agent_settings(
611
644
  skip(f"{SETTINGS_FILE} already exists")
612
645
  return
613
646
 
647
+ target.parent.mkdir(parents=True, exist_ok=True)
614
648
  write_file(target, template_body)
615
649
  user_type_value = profile_values.get("user_type", "")
616
650
  suffix = f", user_type={user_type_value}" if user_type_value else ""
@@ -2128,6 +2162,41 @@ def _enforce_consumer_global_only(scope: str) -> None:
2128
2162
  )
2129
2163
 
2130
2164
 
2165
+ def _enforce_not_source_repo(scope: str, project_root: Path) -> None:
2166
+ """Refuse a non-global install into the agent-config source repo.
2167
+
2168
+ Python-side mirror of the bash orchestrator's "Source-repo guard"
2169
+ (``scripts/install``). The bash guard only covers the headless
2170
+ ``init`` front door; the wizard reaches the apply engine directly via
2171
+ ``install.py --apply-payload`` (and maintainers can call
2172
+ ``python3 scripts/install.py``), so without this the GUI path could
2173
+ write the ``.augment/`` / ``.claude/`` / ``.cursor/`` projection trees
2174
+ back into the checkout. Both front doors converge on ``main()`` here, so
2175
+ this is the single chokepoint that makes the floor uniform.
2176
+
2177
+ ``--global`` only writes user-scope paths and never the source tree, so
2178
+ it is exempt. Unlike :func:`_enforce_consumer_global_only`, dev-mode does
2179
+ NOT lift this guard — dogfooding bridges into the source tree is an
2180
+ explicit action gated by its own ``AGENT_CONFIG_ALLOW_SELF_INSTALL=1``
2181
+ override (same flag the bash guard honours).
2182
+ """
2183
+ if scope == "global":
2184
+ return
2185
+ if os.environ.get("AGENT_CONFIG_ALLOW_SELF_INSTALL") == "1":
2186
+ return
2187
+ is_source, signature = _is_agent_config_source_repo(project_root)
2188
+ if not is_source:
2189
+ return
2190
+ fail(
2191
+ "Refusing to install agent-config into its own source checkout "
2192
+ f"(detected: {signature}). The source repo is global-only — a "
2193
+ "project-scope install would recreate the .augment/ .claude/ .cursor/ "
2194
+ "projection trees in the repo (double token cost). Run `task sync` to "
2195
+ "regenerate them from .agent-src.uncondensed/ instead, or set "
2196
+ "AGENT_CONFIG_ALLOW_SELF_INSTALL=1 to force."
2197
+ )
2198
+
2199
+
2131
2200
  # --- road-to-global-only-install § Phase 2.2 — three-layer settings reader ---
2132
2201
  #
2133
2202
  # Merge order (per ADR-020 / D9):
@@ -2219,7 +2288,7 @@ def read_layered_settings(
2219
2288
  merged = _load_default_settings(package_root)
2220
2289
  merged = deep_merge(merged, _load_yaml_doc(GLOBAL_AGENT_SETTINGS_PATH))
2221
2290
  if project_root is not None:
2222
- project_file = project_root / SETTINGS_FILE
2291
+ project_file = _resolve_settings_read(project_root)
2223
2292
  merged = deep_merge(merged, _load_yaml_doc(project_file))
2224
2293
  return merged
2225
2294
 
@@ -2354,7 +2423,7 @@ def detect_scope(cwd: Path) -> tuple[str, str]:
2354
2423
  ``.git/`` is explicitly NOT a signal — monorepos, dotfile managers,
2355
2424
  and non-Git workspaces all break it. Pure function; no side effects.
2356
2425
  """
2357
- if (cwd / SETTINGS_FILE).exists():
2426
+ if _resolve_settings_read(cwd).exists():
2358
2427
  return "project", f"existing {SETTINGS_FILE}"
2359
2428
 
2360
2429
  has_manifest = next(
@@ -3524,7 +3593,7 @@ def install_global(
3524
3593
  # `.agent-settings.yml` and the manifest would be untracked noise.
3525
3594
  if (
3526
3595
  project_root is not None
3527
- and (project_root / SETTINGS_FILE).exists()
3596
+ and _resolve_settings_read(project_root).exists()
3528
3597
  and not (project_root / ".agent-src.uncondensed").is_dir()
3529
3598
  ):
3530
3599
  # Collect deployed/marker paths per tool so the manifest records
@@ -3989,7 +4058,7 @@ def install_minimal(target_root: Path, force: bool, user_type: str = "") -> int:
3989
4058
  # the source of truth per ADR-020 § D2; a fresh `--minimal` run
3990
4059
  # without user_type does not write a project-local settings file.
3991
4060
  if user_type:
3992
- settings_dst = target_root / SETTINGS_FILE
4061
+ settings_dst = _canonical_settings_target(target_root)
3993
4062
  if settings_dst.exists() and not force:
3994
4063
  skip(f"{SETTINGS_FILE} already exists (use --force to overwrite)")
3995
4064
  else:
@@ -3998,6 +4067,7 @@ def install_minimal(target_root: Path, force: bool, user_type: str = "") -> int:
3998
4067
  "personal:\n"
3999
4068
  f" user_type: {user_type}\n"
4000
4069
  )
4070
+ settings_dst.parent.mkdir(parents=True, exist_ok=True)
4001
4071
  settings_dst.write_text(body, encoding="utf-8")
4002
4072
  success(f"Wrote {SETTINGS_FILE} (user_type={user_type})")
4003
4073
 
@@ -4657,6 +4727,7 @@ def main(argv: list[str]) -> int:
4657
4727
  custom_path: Path | None = Path(opts.custom_path).resolve() if opts.custom_path else None
4658
4728
  scope = _resolve_scope(opts, detected, detect_reason, custom_path)
4659
4729
  _enforce_consumer_global_only(scope)
4730
+ _enforce_not_source_repo(scope, detect_root)
4660
4731
 
4661
4732
  # Scope validation runs before filesystem / package detection so
4662
4733
  # --tools=X / --scope conflicts fail fast with a directive error
@@ -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
 
@@ -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
  )
@@ -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,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())