@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.
- package/.agent-src/commands/agent-handoff.md +1 -1
- package/.agent-src/commands/agent-status.md +1 -1
- package/.agent-src/commands/agents/audit.md +1 -1
- package/.agent-src/commands/agents/init.md +1 -1
- package/.agent-src/commands/agents/user/accept.md +3 -3
- package/.agent-src/commands/agents/user/init.md +4 -4
- package/.agent-src/commands/agents/user/show.md +3 -3
- package/.agent-src/commands/agents/user/update.md +3 -3
- package/.agent-src/commands/agents/user.md +1 -1
- package/.agent-src/commands/agents.md +1 -1
- package/.agent-src/commands/analytics/prune.md +1 -1
- package/.agent-src/commands/analytics/show.md +1 -1
- package/.agent-src/commands/analytics.md +1 -1
- package/.agent-src/commands/bug-fix.md +1 -1
- package/.agent-src/commands/challenge-me.md +1 -1
- package/.agent-src/commands/chat-history/import.md +1 -1
- package/.agent-src/commands/chat-history/learn.md +1 -1
- package/.agent-src/commands/chat-history/show.md +1 -1
- package/.agent-src/commands/chat-history.md +1 -1
- package/.agent-src/commands/check-current-md.md +1 -1
- package/.agent-src/commands/condense.md +1 -1
- package/.agent-src/commands/context.md +1 -1
- package/.agent-src/commands/cost-report.md +1 -1
- package/.agent-src/commands/council.md +3 -3
- package/.agent-src/commands/create-pr/description-only.md +1 -1
- package/.agent-src/commands/create-pr.md +1 -1
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature.md +1 -1
- package/.agent-src/commands/fix/ci.md +1 -1
- package/.agent-src/commands/fix/portability.md +1 -1
- package/.agent-src/commands/fix/pr-bot-comments.md +1 -1
- package/.agent-src/commands/fix/pr-comments.md +1 -1
- package/.agent-src/commands/fix/pr-developer-comments.md +1 -1
- package/.agent-src/commands/fix/refs.md +1 -1
- package/.agent-src/commands/fix/seeder.md +1 -1
- package/.agent-src/commands/fix.md +1 -1
- package/.agent-src/commands/judge.md +1 -1
- package/.agent-src/commands/knowledge/cross-repo.md +1 -1
- package/.agent-src/commands/knowledge/forget.md +1 -1
- package/.agent-src/commands/knowledge/ingest.md +1 -1
- package/.agent-src/commands/knowledge/list.md +1 -1
- package/.agent-src/commands/knowledge.md +1 -1
- package/.agent-src/commands/memory/add.md +1 -1
- package/.agent-src/commands/memory/learn-low-impact.md +1 -1
- package/.agent-src/commands/memory/load.md +1 -1
- package/.agent-src/commands/memory/mine-session.md +1 -1
- package/.agent-src/commands/memory/promote.md +1 -1
- package/.agent-src/commands/memory/propose.md +1 -1
- package/.agent-src/commands/memory.md +1 -1
- package/.agent-src/commands/mode.md +1 -1
- package/.agent-src/commands/optimize/agents-dir.md +1 -1
- package/.agent-src/commands/optimize/augmentignore.md +1 -1
- package/.agent-src/commands/optimize/rtk.md +1 -1
- package/.agent-src/commands/optimize/skills.md +1 -1
- package/.agent-src/commands/optimize.md +1 -1
- package/.agent-src/commands/orchestrate.md +1 -1
- package/.agent-src/commands/override/create.md +1 -1
- package/.agent-src/commands/override/manage.md +1 -1
- package/.agent-src/commands/override.md +1 -1
- package/.agent-src/commands/package-reset.md +1 -1
- package/.agent-src/commands/prediction-pool.md +31 -12
- package/.agent-src/commands/profile/activate.md +81 -0
- package/.agent-src/commands/profile/deactivate.md +68 -0
- package/.agent-src/commands/profile/show.md +70 -0
- package/.agent-src/commands/profile.md +68 -0
- package/.agent-src/commands/project-health.md +1 -1
- package/.agent-src/commands/quality-fix.md +1 -1
- package/.agent-src/commands/roadmap/process-full.md +1 -1
- package/.agent-src/commands/roadmap/process-phase.md +1 -1
- package/.agent-src/commands/roadmap/process-step.md +1 -1
- package/.agent-src/commands/roadmap.md +1 -1
- package/.agent-src/commands/set-cost-profile.md +1 -1
- package/.agent-src/commands/skill/preview.md +3 -3
- package/.agent-src/commands/skill.md +1 -1
- package/.agent-src/commands/skills/discover.md +1 -1
- package/.agent-src/commands/skills.md +1 -1
- package/.agent-src/commands/sync-agent-settings.md +1 -1
- package/.agent-src/commands/sync-gitignore/fix.md +1 -1
- package/.agent-src/commands/sync-gitignore.md +1 -1
- package/.agent-src/commands/update-form-request-messages.md +1 -1
- package/.agent-src/skills/check-refs/SKILL.md +1 -1
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/.agent-src/skills/git-workflow/SKILL.md +1 -1
- package/.agent-src/skills/jira-integration/SKILL.md +1 -1
- package/.agent-src/skills/markitdown/SKILL.md +1 -1
- package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +195 -77
- package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +3 -1
- package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +111 -16
- package/.agent-src/skills/prediction-pool-optimizer/reference/odds-and-bonus.md +109 -0
- package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -1
- package/.agent-src/skills/script-writing/SKILL.md +1 -1
- package/.agent-src/skills/token-optimizer/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +52 -5
- package/.claude-plugin/marketplace.json +370 -366
- package/CHANGELOG.md +77 -0
- package/README.md +2 -2
- package/config/discovery/session-profiles.yml +37 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +183 -95
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +9 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +8 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +7 -3
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/session-profile-overlay.md +120 -0
- package/docs/customization.md +26 -0
- package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +36 -0
- package/docs/decisions/ADR-038-canonical-settings-path.md +66 -0
- package/docs/decisions/ADR-039-claude-skills-untracked.md +139 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/development.md +12 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +8 -2
- package/docs/skills-catalog.md +5 -1
- package/llms.txt +4 -0
- package/package.json +1 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_doctor.py +180 -16
- package/scripts/_cli/cmd_versions.py +2 -2
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/agent_settings.py +52 -5
- package/scripts/_lib/agent_src.py +30 -0
- package/scripts/ai_council/session.py +5 -1
- package/scripts/audit_command_surface.py +7 -1
- package/scripts/audit_initial_context.py +10 -2
- package/scripts/check_gate_paths.py +117 -0
- package/scripts/check_references.py +51 -2
- package/scripts/check_release_published.py +145 -0
- package/scripts/check_test_coverage_diff.py +180 -0
- package/scripts/compile_router.py +5 -1
- package/scripts/condense.py +79 -2
- package/scripts/config/session_profiles.py +492 -0
- package/scripts/council_cli.py +5 -1
- package/scripts/hook_manifest.yaml +15 -7
- package/scripts/hooks/dispatch_hook.py +8 -0
- package/scripts/install-hooks.sh +2 -1
- package/scripts/install.py +76 -5
- package/scripts/inventory_abstraction_budget.py +6 -1
- package/scripts/lint_agents_md.py +11 -4
- package/scripts/lint_hook_concern_budget.py +5 -1
- package/scripts/lint_marketplace.py +18 -7
- package/scripts/lint_roadmap_ci_steps.py +5 -1
- package/scripts/lint_roadmap_complexity.py +5 -1
- package/scripts/mcp_server/prompts.py +5 -1
- package/scripts/prediction-pool/pool_winsim.py +236 -0
- package/scripts/prediction-pool/score_ev.py +188 -0
- package/scripts/profile_staleness_hook.py +69 -0
- package/scripts/release.py +54 -31
- package/scripts/roadmap_progress_hook.py +56 -6
- package/scripts/smoke_quickstart.py +3 -2
- package/scripts/sync_agent_settings.py +8 -3
- package/scripts/validate_agent_settings.py +5 -1
- package/scripts/validate_decision_engine.py +5 -1
- package/scripts/measure_roadmap_trajectory.py +0 -112
- package/scripts/verify_roadmap_closure.py +0 -327
package/scripts/install.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
128
|
-
#
|
|
129
|
-
# skills
|
|
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
|
-
|
|
139
|
-
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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())
|