@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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Exact-score EV optimiser for prediction-pool-optimizer.
|
|
3
|
+
|
|
4
|
+
Honest replacement for eyeballing the favourite — given each side's expected
|
|
5
|
+
goals (lambda) and the pool's scoring rule, this builds the full Poisson
|
|
6
|
+
score grid and computes the expected points of EVERY candidate tip, then
|
|
7
|
+
prints the EV-maximizing scoreline. It exists to kill two recurring failure
|
|
8
|
+
modes:
|
|
9
|
+
|
|
10
|
+
1. Hallucinated high scorelines (4:2, 1:4, 3:2 ...). Under any partial-points
|
|
11
|
+
rule these are almost never EV-max for a moderate favourite — the points
|
|
12
|
+
live in the tendency and goal-difference tiers, not the exact high score.
|
|
13
|
+
2. Under-tipped draws. A correctly-tipped draw banks the goal-difference
|
|
14
|
+
tier on every draw scoreline, so a 1:1 can beat a 1:0 in a close game.
|
|
15
|
+
The grid surfaces this; intuition does not.
|
|
16
|
+
|
|
17
|
+
The scoring model (configurable points per tier):
|
|
18
|
+
|
|
19
|
+
exact result → --exact (default 4)
|
|
20
|
+
goal diff → --diff (default 3) # same difference, not exact; draw-on-draw lands here
|
|
21
|
+
tendency → --tendency (default 2) # same W/D/L sign only
|
|
22
|
+
else → 0
|
|
23
|
+
|
|
24
|
+
For kicktipp's common "2 / 3 / 5" config run with --tendency 2 --diff 3 --exact 5.
|
|
25
|
+
|
|
26
|
+
It is an APPROXIMATION only in its goal model: a Poisson per side with the
|
|
27
|
+
provided lambdas, sides independent. That is the standard football scoreline
|
|
28
|
+
model and is robust to small lambda changes — but the lambdas themselves must
|
|
29
|
+
come from de-vigged consensus odds (see reference/odds-and-bonus.md), not a
|
|
30
|
+
guess. Feed it real numbers and the EV-max is exact for that model.
|
|
31
|
+
|
|
32
|
+
Input — either two lambdas on the CLI:
|
|
33
|
+
|
|
34
|
+
python3 scripts/prediction-pool/score_ev.py --lh 2.0 --la 0.7
|
|
35
|
+
python3 scripts/prediction-pool/score_ev.py --lh 0.6 --la 2.1 --tendency 2 --diff 3 --exact 5
|
|
36
|
+
|
|
37
|
+
or a JSON file of named matches (batch):
|
|
38
|
+
|
|
39
|
+
python3 scripts/prediction-pool/score_ev.py matches.json --tendency 2 --diff 3 --exact 5
|
|
40
|
+
|
|
41
|
+
matches.json:
|
|
42
|
+
[
|
|
43
|
+
{"match": "Senegal-Iraq", "lh": 2.0, "la": 0.7},
|
|
44
|
+
{"match": "Qatar-Switzerland", "lh": 0.6, "la": 2.1}
|
|
45
|
+
]
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import argparse
|
|
50
|
+
import json
|
|
51
|
+
import math
|
|
52
|
+
import sys
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
MAX_GOALS = 12 # truncation of the Poisson grid; tail beyond this is negligible
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _pois_pmf(k: int, rate: float) -> float:
|
|
59
|
+
if rate <= 0:
|
|
60
|
+
return 1.0 if k == 0 else 0.0
|
|
61
|
+
return math.exp(-rate) * rate ** k / math.factorial(k)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _sign(x: int) -> int:
|
|
65
|
+
return (x > 0) - (x < 0)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _score(th: int, ta: int, ah: int, aa: int,
|
|
69
|
+
pts_exact: float, pts_diff: float, pts_tend: float) -> float:
|
|
70
|
+
"""Points a tip (th:ta) earns against an actual result (ah:aa)."""
|
|
71
|
+
if th == ah and ta == aa:
|
|
72
|
+
return pts_exact
|
|
73
|
+
if (th - ta) == (ah - aa):
|
|
74
|
+
return pts_diff
|
|
75
|
+
if _sign(th - ta) == _sign(ah - aa):
|
|
76
|
+
return pts_tend
|
|
77
|
+
return 0.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def grid(lh: float, la: float, max_goals: int = MAX_GOALS):
|
|
81
|
+
"""Joint probability of every actual scoreline up to max_goals."""
|
|
82
|
+
ph = [_pois_pmf(k, lh) for k in range(max_goals + 1)]
|
|
83
|
+
pa = [_pois_pmf(k, la) for k in range(max_goals + 1)]
|
|
84
|
+
return [[ph[h] * pa[a] for a in range(max_goals + 1)] for h in range(max_goals + 1)]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ev_table(lh: float, la: float, pts_exact: float, pts_diff: float, pts_tend: float,
|
|
88
|
+
max_tip: int = 6, max_goals: int = MAX_GOALS):
|
|
89
|
+
"""EV (expected points) of every candidate tip up to max_tip goals/side."""
|
|
90
|
+
g = grid(lh, la, max_goals)
|
|
91
|
+
rows = []
|
|
92
|
+
for th in range(max_tip + 1):
|
|
93
|
+
for ta in range(max_tip + 1):
|
|
94
|
+
ev = 0.0
|
|
95
|
+
for ah in range(max_goals + 1):
|
|
96
|
+
for aa in range(max_goals + 1):
|
|
97
|
+
p = g[ah][aa]
|
|
98
|
+
if p <= 0:
|
|
99
|
+
continue
|
|
100
|
+
s = _score(th, ta, ah, aa, pts_exact, pts_diff, pts_tend)
|
|
101
|
+
if s:
|
|
102
|
+
ev += p * s
|
|
103
|
+
rows.append((th, ta, ev))
|
|
104
|
+
rows.sort(key=lambda r: r[2], reverse=True)
|
|
105
|
+
return rows, g
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _modal(g) -> tuple[int, int, float]:
|
|
109
|
+
best = (0, 0, 0.0)
|
|
110
|
+
for h in range(len(g)):
|
|
111
|
+
for a in range(len(g[h])):
|
|
112
|
+
if g[h][a] > best[2]:
|
|
113
|
+
best = (h, a, g[h][a])
|
|
114
|
+
return best
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _p_draw(g) -> float:
|
|
118
|
+
return sum(g[i][i] for i in range(len(g)))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def analyse(lh: float, la: float, pts_exact: float, pts_diff: float, pts_tend: float,
|
|
122
|
+
max_tip: int = 6, top: int = 6):
|
|
123
|
+
rows, g = ev_table(lh, la, pts_exact, pts_diff, pts_tend, max_tip)
|
|
124
|
+
mh, ma, mp = _modal(g)
|
|
125
|
+
return {
|
|
126
|
+
"lambda": [lh, la],
|
|
127
|
+
"rule": {"exact": pts_exact, "diff": pts_diff, "tendency": pts_tend},
|
|
128
|
+
"ev_max": {"tip": f"{rows[0][0]}:{rows[0][1]}", "ev": round(rows[0][2], 3)},
|
|
129
|
+
"modal_result": {"score": f"{mh}:{ma}", "prob": round(mp, 3)},
|
|
130
|
+
"p_draw": round(_p_draw(g), 3),
|
|
131
|
+
"ranked": [{"tip": f"{h}:{a}", "ev": round(ev, 3)} for h, a, ev in rows[:top]],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _print_one(name: str | None, res: dict) -> None:
|
|
136
|
+
if name:
|
|
137
|
+
print(f"\n## {name}")
|
|
138
|
+
lh, la = res["lambda"]
|
|
139
|
+
r = res["rule"]
|
|
140
|
+
print(f"lambda {lh}:{la} rule exact={r['exact']} diff={r['diff']} tendency={r['tendency']}")
|
|
141
|
+
print(f"EV-max tip : {res['ev_max']['tip']} (EV {res['ev_max']['ev']})")
|
|
142
|
+
print(f"modal score: {res['modal_result']['score']} (P {res['modal_result']['prob']}) "
|
|
143
|
+
f"P(draw) {res['p_draw']}")
|
|
144
|
+
print("ranked by EV:")
|
|
145
|
+
for row in res["ranked"]:
|
|
146
|
+
flag = " <- EV-max" if row["tip"] == res["ev_max"]["tip"] else ""
|
|
147
|
+
print(f" {row['tip']:>5} EV {row['ev']:.3f}{flag}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main(argv=None) -> int:
|
|
151
|
+
ap = argparse.ArgumentParser(description="Exact-score EV optimiser (Poisson grid).")
|
|
152
|
+
ap.add_argument("matches", nargs="?", help="JSON file of matches [{match,lh,la}]")
|
|
153
|
+
ap.add_argument("--lh", type=float, help="home expected goals (lambda)")
|
|
154
|
+
ap.add_argument("--la", type=float, help="away expected goals (lambda)")
|
|
155
|
+
ap.add_argument("--exact", type=float, default=4.0, help="points for exact result (default 4)")
|
|
156
|
+
ap.add_argument("--diff", type=float, default=3.0, help="points for correct goal difference (default 3)")
|
|
157
|
+
ap.add_argument("--tendency", type=float, default=2.0, help="points for correct tendency (default 2)")
|
|
158
|
+
ap.add_argument("--max-tip", type=int, default=6, help="max goals/side to consider as a tip")
|
|
159
|
+
ap.add_argument("--top", type=int, default=6, help="rows to print per match")
|
|
160
|
+
ap.add_argument("--json", action="store_true", help="emit JSON instead of text")
|
|
161
|
+
args = ap.parse_args(argv)
|
|
162
|
+
|
|
163
|
+
jobs: list[tuple[str | None, float, float]] = []
|
|
164
|
+
if args.matches:
|
|
165
|
+
data = json.loads(Path(args.matches).read_text())
|
|
166
|
+
for m in data:
|
|
167
|
+
jobs.append((m.get("match"), float(m["lh"]), float(m["la"])))
|
|
168
|
+
elif args.lh is not None and args.la is not None:
|
|
169
|
+
jobs.append((None, args.lh, args.la))
|
|
170
|
+
else:
|
|
171
|
+
ap.error("provide either a matches JSON file or --lh and --la")
|
|
172
|
+
|
|
173
|
+
out = []
|
|
174
|
+
for name, lh, la in jobs:
|
|
175
|
+
res = analyse(lh, la, args.exact, args.diff, args.tendency, args.max_tip, args.top)
|
|
176
|
+
if name:
|
|
177
|
+
res["match"] = name
|
|
178
|
+
out.append(res)
|
|
179
|
+
if not args.json:
|
|
180
|
+
_print_one(name, res)
|
|
181
|
+
|
|
182
|
+
if args.json:
|
|
183
|
+
print(json.dumps(out, indent=2))
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Session-profile staleness notice — `session_start` hook.
|
|
3
|
+
|
|
4
|
+
Phase 1 companion to the locked Phase-0.1 decision (option a, explicit
|
|
5
|
+
`/profile deactivate`). The `runtime.active_packs` overlay survives an IDE
|
|
6
|
+
restart, so this hook does **not** reset it — it only surfaces a one-line
|
|
7
|
+
**staleness notice** when a new session starts with an overlay carried over
|
|
8
|
+
from a previous session. Silently resetting on `session_start` is the
|
|
9
|
+
registry-refresh Catch-22 the council ruled out (see
|
|
10
|
+
`agents/settings/contexts/session-host-capability-audit.md`).
|
|
11
|
+
|
|
12
|
+
Contract: never blocks. Reads the JSON envelope on stdin (ignored — the
|
|
13
|
+
notice is derived from the overlay file), emits at most one stderr line,
|
|
14
|
+
returns 0 on every path.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Make `scripts` importable when invoked as a bare script path.
|
|
25
|
+
_REPO = Path(__file__).resolve().parent.parent
|
|
26
|
+
if str(_REPO) not in sys.path:
|
|
27
|
+
sys.path.insert(0, str(_REPO))
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from scripts.config import session_profiles
|
|
31
|
+
except Exception: # pragma: no cover - defensive; never block the loop
|
|
32
|
+
session_profiles = None # type: ignore
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _project_root() -> Path:
|
|
36
|
+
env = os.environ.get("CLAUDE_PROJECT_DIR") or os.environ.get("AGENT_CONFIG_PROJECT_DIR")
|
|
37
|
+
if env:
|
|
38
|
+
return Path(env)
|
|
39
|
+
return Path.cwd()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(argv: list[str] | None = None) -> int:
|
|
43
|
+
ap = argparse.ArgumentParser(description="Session-profile staleness notice (session_start).")
|
|
44
|
+
ap.add_argument("--root", default=None)
|
|
45
|
+
args, _ = ap.parse_known_args(argv)
|
|
46
|
+
|
|
47
|
+
# Drain stdin (the dispatcher passes a JSON envelope); we do not need it.
|
|
48
|
+
try:
|
|
49
|
+
if not sys.stdin.isatty():
|
|
50
|
+
sys.stdin.read()
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
if session_profiles is None:
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
root = Path(args.root) if args.root else _project_root()
|
|
58
|
+
try:
|
|
59
|
+
notice = session_profiles.stale_notice(root)
|
|
60
|
+
except Exception:
|
|
61
|
+
return 0 # fail-open — never block the session
|
|
62
|
+
|
|
63
|
+
if notice:
|
|
64
|
+
print(f"[profile] {notice}", file=sys.stderr)
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
raise SystemExit(main())
|
package/scripts/release.py
CHANGED
|
@@ -665,7 +665,7 @@ def execute(
|
|
|
665
665
|
resume: bool = False,
|
|
666
666
|
) -> None:
|
|
667
667
|
branch = f"release/{plan.target}"
|
|
668
|
-
total =
|
|
668
|
+
total = 10
|
|
669
669
|
|
|
670
670
|
if dry_run:
|
|
671
671
|
print("(dry-run) no git/gh mutations will be performed.")
|
|
@@ -842,6 +842,25 @@ def execute(
|
|
|
842
842
|
"--notes", notes,
|
|
843
843
|
)
|
|
844
844
|
|
|
845
|
+
# ─── 10. delete the merged release branch (local + remote) ───────────────
|
|
846
|
+
# Branch hygiene: a merged-but-undeleted release/X.Y.Z is what made
|
|
847
|
+
# `--resume` mis-detect an old version. Delete it now so it can never
|
|
848
|
+
# accumulate. Idempotent — skips whatever is already gone. Never touches
|
|
849
|
+
# `main` or any tag.
|
|
850
|
+
if dry_run:
|
|
851
|
+
_step(10, total, f"Would delete merged branch {branch} (local + remote)")
|
|
852
|
+
else:
|
|
853
|
+
deleted = []
|
|
854
|
+
if _branch_exists_local(branch) and \
|
|
855
|
+
git("rev-parse", "--abbrev-ref", "HEAD", capture=True) != branch:
|
|
856
|
+
run("git", "branch", "-D", branch, check=False)
|
|
857
|
+
deleted.append("local")
|
|
858
|
+
if _branch_exists_remote(branch):
|
|
859
|
+
run("git", "push", REMOTE, "--delete", branch, check=False)
|
|
860
|
+
deleted.append("remote")
|
|
861
|
+
where = " + ".join(deleted) if deleted else "already gone"
|
|
862
|
+
_step(10, total, f"Delete merged branch {branch} ({where})")
|
|
863
|
+
|
|
845
864
|
print()
|
|
846
865
|
print(f"✅ Released {plan.target}")
|
|
847
866
|
print(f" https://github.com/{REPO_SLUG}/releases/tag/{plan.target}")
|
|
@@ -904,43 +923,47 @@ _RELEASE_BRANCH_RE = re.compile(r"^release/(\d+\.\d+\.\d+)$")
|
|
|
904
923
|
|
|
905
924
|
|
|
906
925
|
def _detect_in_flight_target() -> str | None:
|
|
907
|
-
"""Find the in-flight release target
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
is
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
926
|
+
"""Find the in-flight release target — the SOURCE OF TRUTH is package.json.
|
|
927
|
+
|
|
928
|
+
An "in-flight" release is one whose version was already bumped into
|
|
929
|
+
``main``'s ``package.json`` (and possibly merged) but whose tag has not
|
|
930
|
+
yet been pushed — i.e. the publish step never completed. The canonical
|
|
931
|
+
anchor is therefore ``package.json`` version `V` **with no matching tag
|
|
932
|
+
`V`**, NOT the set of ``release/X.Y.Z`` branches.
|
|
933
|
+
|
|
934
|
+
Why not the branch set: merged release branches are frequently left
|
|
935
|
+
undeleted on the remote, so "highest existing release/* branch" can
|
|
936
|
+
resolve to an OLD, already-published version (e.g. picking 5.4.0 while
|
|
937
|
+
5.8.0 is the real in-flight target) and tag a downgrade. The package.json
|
|
938
|
+
version cannot lie that way — it is the version main currently claims to
|
|
939
|
+
be, and an untagged claim is exactly an incomplete release.
|
|
940
|
+
|
|
941
|
+
Resolution order:
|
|
942
|
+
1. If HEAD is on a ``release/X.Y.Z`` branch, that explicit checkout wins.
|
|
943
|
+
2. Else: read ``package.json`` version `V`. If tag `V` does not exist
|
|
944
|
+
(local or remote), `V` is the in-flight target. If it is already
|
|
945
|
+
tagged, the release is complete → return None (regular bump path).
|
|
946
|
+
|
|
947
|
+
Stale ``release/*`` branches are never used for version detection.
|
|
918
948
|
"""
|
|
919
949
|
head = git("rev-parse", "--abbrev-ref", "HEAD", capture=True)
|
|
920
950
|
m = _RELEASE_BRANCH_RE.match(head)
|
|
921
951
|
if m:
|
|
922
952
|
return m.group(1)
|
|
923
953
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
f"refs/remotes/{REMOTE}/release/", capture=True,
|
|
933
|
-
)
|
|
934
|
-
for line in remote_raw.splitlines():
|
|
935
|
-
bare = line.strip().removeprefix(f"{REMOTE}/")
|
|
936
|
-
if (m := _RELEASE_BRANCH_RE.match(bare)):
|
|
937
|
-
candidates.append(m.group(1))
|
|
954
|
+
try:
|
|
955
|
+
version = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))["version"]
|
|
956
|
+
except (OSError, KeyError, json.JSONDecodeError):
|
|
957
|
+
return None
|
|
958
|
+
try:
|
|
959
|
+
parse_version(version)
|
|
960
|
+
except Exception:
|
|
961
|
+
return None
|
|
938
962
|
|
|
939
|
-
|
|
963
|
+
# An already-tagged version is a completed release, not in-flight.
|
|
964
|
+
if _tag_exists_local(version) or _tag_exists_remote(version):
|
|
940
965
|
return None
|
|
941
|
-
|
|
942
|
-
candidates.sort(key=parse_version)
|
|
943
|
-
return candidates[-1]
|
|
966
|
+
return version
|
|
944
967
|
|
|
945
968
|
|
|
946
969
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -961,7 +984,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
961
984
|
target = args.explicit
|
|
962
985
|
elif in_flight:
|
|
963
986
|
target = in_flight
|
|
964
|
-
print(f"(resume)
|
|
987
|
+
print(f"(resume) in-flight target {in_flight} (package.json version with no tag yet)")
|
|
965
988
|
else:
|
|
966
989
|
target = bump_version(current, bump)
|
|
967
990
|
parse_version(target)
|
|
@@ -55,6 +55,21 @@ ROADMAP_PREFIX = "agents/roadmaps/"
|
|
|
55
55
|
ROADMAP_EXCLUDED_PARTS = frozenset({"archive", "skipped"})
|
|
56
56
|
DASHBOARD_PATH = "agents/roadmaps-progress.md"
|
|
57
57
|
|
|
58
|
+
REGEN_NAME = "update_roadmap_progress.py"
|
|
59
|
+
# Distributed-content script subtrees that may ship the regenerator,
|
|
60
|
+
# in priority order. Project-scoped installs land it under .augment/ or
|
|
61
|
+
# .agent-src/; the package itself carries the same projection.
|
|
62
|
+
DIST_SCRIPT_SUBDIRS = (
|
|
63
|
+
Path(".augment") / "scripts",
|
|
64
|
+
Path(".agent-src") / "scripts",
|
|
65
|
+
Path(".agent-src.uncondensed") / "scripts",
|
|
66
|
+
)
|
|
67
|
+
# Set by the dispatcher (scripts/hooks/dispatch_hook.py) to its own
|
|
68
|
+
# resolved package root, so a globally-installed binary (ADR-020
|
|
69
|
+
# global-only) can locate the shipped regenerator even when the consumer
|
|
70
|
+
# repo carries no project-local distributed content.
|
|
71
|
+
PACKAGE_ROOT_ENV_VAR = "AGENT_CONFIG_PACKAGE_ROOT"
|
|
72
|
+
|
|
58
73
|
|
|
59
74
|
def _candidate_paths(payload: dict) -> list[str]:
|
|
60
75
|
"""Pull every plausible file path out of a PostToolUse payload."""
|
|
@@ -115,15 +130,50 @@ def _is_roadmap_touch(path: str) -> bool:
|
|
|
115
130
|
return True
|
|
116
131
|
|
|
117
132
|
|
|
133
|
+
def _package_roots() -> list[Path]:
|
|
134
|
+
"""Package roots to search for the shipped regenerator, in priority
|
|
135
|
+
order, when the consumer carries no project-local copy.
|
|
136
|
+
|
|
137
|
+
A global-only consumer (ADR-020) never has `.augment/` / `.agent-src/`
|
|
138
|
+
in its repo — those trees are *distributed content*, which global-only
|
|
139
|
+
installs keep in the globally-installed package, not the project. The
|
|
140
|
+
regenerator therefore lives next to the running code, not next to the
|
|
141
|
+
edited roadmap.
|
|
142
|
+
|
|
143
|
+
1. ``AGENT_CONFIG_PACKAGE_ROOT`` — the dispatcher passes its own
|
|
144
|
+
resolved package root (``dispatch_hook.REPO_ROOT``). This is the
|
|
145
|
+
same root the dispatcher already trusts to locate this concern, so
|
|
146
|
+
it survives editable installs, plugin-cache moves, and symlinks
|
|
147
|
+
that a naive ``__file__`` walk would mis-resolve.
|
|
148
|
+
2. This hook's own location (``<pkg>/scripts/roadmap_progress_hook.py``
|
|
149
|
+
→ ``<pkg>``) — last-resort fallback for standalone invocation
|
|
150
|
+
outside the dispatcher.
|
|
151
|
+
"""
|
|
152
|
+
roots: list[Path] = []
|
|
153
|
+
env_root = os.environ.get(PACKAGE_ROOT_ENV_VAR, "").strip()
|
|
154
|
+
if env_root:
|
|
155
|
+
roots.append(Path(env_root).expanduser())
|
|
156
|
+
roots.append(Path(__file__).resolve().parent.parent)
|
|
157
|
+
return roots
|
|
158
|
+
|
|
159
|
+
|
|
118
160
|
def _resolve_regenerator(consumer_root: Path) -> Path | None:
|
|
119
|
-
"""Find the regenerator script
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
161
|
+
"""Find the regenerator script.
|
|
162
|
+
|
|
163
|
+
Project-local copy first (project-scoped installs), then the package
|
|
164
|
+
the hook itself ships in (global-only consumers, per ADR-020 — the
|
|
165
|
+
repo has no project-local distributed content). Returns ``None`` only
|
|
166
|
+
when no copy exists in either place.
|
|
167
|
+
"""
|
|
168
|
+
for subdir in DIST_SCRIPT_SUBDIRS:
|
|
169
|
+
candidate = consumer_root / subdir / REGEN_NAME
|
|
125
170
|
if candidate.is_file():
|
|
126
171
|
return candidate
|
|
172
|
+
for root in _package_roots():
|
|
173
|
+
for subdir in DIST_SCRIPT_SUBDIRS:
|
|
174
|
+
candidate = root / subdir / REGEN_NAME
|
|
175
|
+
if candidate.is_file():
|
|
176
|
+
return candidate
|
|
127
177
|
return None
|
|
128
178
|
|
|
129
179
|
|
|
@@ -61,9 +61,10 @@ def _check_installer_runs(tmpdir: Path) -> tuple[int, Path | None]:
|
|
|
61
61
|
_fail(f"installer exited {result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"),
|
|
62
62
|
None,
|
|
63
63
|
)
|
|
64
|
-
settings
|
|
64
|
+
# ADR-038: installer writes the canonical settings file under agents/settings/.
|
|
65
|
+
settings = tmpdir / "agents" / "settings" / ".agent-settings.yml"
|
|
65
66
|
if not settings.exists():
|
|
66
|
-
return _fail("
|
|
67
|
+
return _fail("agents/settings/.agent-settings.yml not written by installer"), None
|
|
67
68
|
return 0, settings
|
|
68
69
|
|
|
69
70
|
|
|
@@ -85,8 +85,10 @@ def render_diff(old_text: str, new_text: str, path: str) -> str:
|
|
|
85
85
|
|
|
86
86
|
def main(argv: list[str] | None = None) -> int:
|
|
87
87
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
88
|
-
ap.add_argument("--path", default=
|
|
89
|
-
help=
|
|
88
|
+
ap.add_argument("--path", default=None,
|
|
89
|
+
help="target settings file (default: canonical "
|
|
90
|
+
"agents/settings/.agent-settings.yml, falling back to "
|
|
91
|
+
"a legacy repo-root .agent-settings.yml — ADR-038)")
|
|
90
92
|
ap.add_argument("--template", default=str(DEFAULT_TEMPLATE),
|
|
91
93
|
help="path to the settings template")
|
|
92
94
|
ap.add_argument("--profile", default=None,
|
|
@@ -102,7 +104,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
102
104
|
help="suppress summary on success")
|
|
103
105
|
args = ap.parse_args(argv)
|
|
104
106
|
|
|
105
|
-
target =
|
|
107
|
+
target = (
|
|
108
|
+
Path(args.path) if args.path is not None
|
|
109
|
+
else _install._resolve_settings_read(Path.cwd())
|
|
110
|
+
)
|
|
106
111
|
template_path = Path(args.template)
|
|
107
112
|
profile_dir = Path(args.profile_dir)
|
|
108
113
|
|
|
@@ -29,6 +29,10 @@ from __future__ import annotations
|
|
|
29
29
|
import json
|
|
30
30
|
import sys
|
|
31
31
|
from pathlib import Path
|
|
32
|
+
try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
|
|
33
|
+
from scripts._lib.agent_settings import project_settings_path
|
|
34
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
35
|
+
from _lib.agent_settings import project_settings_path
|
|
32
36
|
|
|
33
37
|
try:
|
|
34
38
|
import yaml
|
|
@@ -45,7 +49,7 @@ except ImportError: # pragma: no cover — bootstrap guard
|
|
|
45
49
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
46
50
|
SCHEMA_PATH = REPO_ROOT / "scripts" / "schemas" / "agent-settings.schema.json"
|
|
47
51
|
TEMPLATE_PATH = REPO_ROOT / "config" / "agent-settings.template.yml"
|
|
48
|
-
LOCAL_PATHS = [REPO_ROOT
|
|
52
|
+
LOCAL_PATHS = [project_settings_path(REPO_ROOT)]
|
|
49
53
|
|
|
50
54
|
# Installer-default substitutions, mirroring scripts/install.py so the
|
|
51
55
|
# template validates as it would after a fresh `balanced` install.
|
|
@@ -19,6 +19,10 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import sys
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
|
|
23
|
+
from scripts._lib.agent_settings import project_settings_path
|
|
24
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
25
|
+
from _lib.agent_settings import project_settings_path
|
|
22
26
|
|
|
23
27
|
try:
|
|
24
28
|
import yaml
|
|
@@ -48,7 +52,7 @@ from work_engine.scoring.decision_engine import ( # noqa: E402
|
|
|
48
52
|
# canonical — its absence is itself a regression).
|
|
49
53
|
TEMPLATE_PATH = REPO_ROOT / "config" / "agent-settings.template.yml"
|
|
50
54
|
# Project-level overrides developers may have on disk locally.
|
|
51
|
-
LOCAL_PATHS = [REPO_ROOT
|
|
55
|
+
LOCAL_PATHS = [project_settings_path(REPO_ROOT)]
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
def _load_yaml(path: Path) -> dict | None:
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Phase 5.1 — Roadmap commitment-history measurement.
|
|
3
|
-
|
|
4
|
-
Walks `agents/roadmaps/archive/` and computes per-roadmap checkbox
|
|
5
|
-
completion ratio at archival time. Output: one-line trajectory metric
|
|
6
|
-
per roadmap, plus an aggregate `agents/runtime/reports/roadmap-trajectory.json`.
|
|
7
|
-
|
|
8
|
-
Checkbox grammar (mirrors `scripts/roadmap_progress_check.py`):
|
|
9
|
-
- `[ ]` — open
|
|
10
|
-
- `[x]` — done
|
|
11
|
-
- `[~]` — in-progress
|
|
12
|
-
- `[-]` — cancelled / dropped (counts neither toward open nor closed)
|
|
13
|
-
|
|
14
|
-
Trajectory metric = closed / (open + closed + in-progress); cancelled
|
|
15
|
-
items are excluded from the denominator so a cleanly archived "we
|
|
16
|
-
decided not to do this" doesn't dilute the score.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import argparse
|
|
22
|
-
import json
|
|
23
|
-
import re
|
|
24
|
-
import sys
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
28
|
-
ARCHIVE = ROOT / "agents" / "roadmaps" / "archive"
|
|
29
|
-
REPORT = ROOT / "agents" / "reports" / "roadmap-trajectory.json"
|
|
30
|
-
|
|
31
|
-
CHECKBOX = re.compile(r"^\s*[-*]\s*\[(?P<state>[ x~\-])\]", re.MULTILINE)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def measure(path: Path) -> dict:
|
|
35
|
-
text = path.read_text(encoding="utf-8", errors="replace")
|
|
36
|
-
counts = {"open": 0, "done": 0, "wip": 0, "cancelled": 0}
|
|
37
|
-
for m in CHECKBOX.finditer(text):
|
|
38
|
-
state = m.group("state")
|
|
39
|
-
if state == " ":
|
|
40
|
-
counts["open"] += 1
|
|
41
|
-
elif state == "x":
|
|
42
|
-
counts["done"] += 1
|
|
43
|
-
elif state == "~":
|
|
44
|
-
counts["wip"] += 1
|
|
45
|
-
elif state == "-":
|
|
46
|
-
counts["cancelled"] += 1
|
|
47
|
-
denom = counts["open"] + counts["done"] + counts["wip"]
|
|
48
|
-
ratio = (counts["done"] / denom) if denom else None
|
|
49
|
-
return {
|
|
50
|
-
"file": str(path.relative_to(ROOT)),
|
|
51
|
-
"counts": counts,
|
|
52
|
-
"completion_ratio": ratio,
|
|
53
|
-
"total_actionable": denom,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def main() -> int:
|
|
58
|
-
ap = argparse.ArgumentParser()
|
|
59
|
-
ap.add_argument("--archive", default=str(ARCHIVE))
|
|
60
|
-
ap.add_argument("--report", default=str(REPORT))
|
|
61
|
-
ap.add_argument("--print-table", action="store_true")
|
|
62
|
-
args = ap.parse_args()
|
|
63
|
-
|
|
64
|
-
archive = Path(args.archive)
|
|
65
|
-
if not archive.exists():
|
|
66
|
-
print(f"❌ archive not found: {archive}", file=sys.stderr)
|
|
67
|
-
return 2
|
|
68
|
-
|
|
69
|
-
rows = [measure(p) for p in sorted(archive.glob("*.md"))]
|
|
70
|
-
|
|
71
|
-
# Aggregate: mean, median, count above 80%, count zero-completion
|
|
72
|
-
ratios = [r["completion_ratio"] for r in rows if r["completion_ratio"] is not None]
|
|
73
|
-
aggregate = {
|
|
74
|
-
"roadmaps": len(rows),
|
|
75
|
-
"scored": len(ratios),
|
|
76
|
-
"mean": (sum(ratios) / len(ratios)) if ratios else None,
|
|
77
|
-
"median": sorted(ratios)[len(ratios) // 2] if ratios else None,
|
|
78
|
-
"above_80pct": sum(1 for r in ratios if r >= 0.80),
|
|
79
|
-
"below_50pct": sum(1 for r in ratios if r < 0.50),
|
|
80
|
-
"zero_completion": sum(1 for r in ratios if r == 0.0),
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
out = Path(args.report)
|
|
84
|
-
out.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
-
out.write_text(
|
|
86
|
-
json.dumps({"aggregate": aggregate, "rows": rows}, indent=2) + "\n",
|
|
87
|
-
encoding="utf-8",
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
print(f"✅ Wrote {out.relative_to(ROOT)}")
|
|
91
|
-
print(f" roadmaps={aggregate['roadmaps']} scored={aggregate['scored']}")
|
|
92
|
-
if aggregate["mean"] is not None:
|
|
93
|
-
print(
|
|
94
|
-
f" mean={aggregate['mean']:.1%} median={aggregate['median']:.1%} "
|
|
95
|
-
f"above_80%={aggregate['above_80pct']} below_50%={aggregate['below_50pct']} "
|
|
96
|
-
f"zero={aggregate['zero_completion']}"
|
|
97
|
-
)
|
|
98
|
-
if args.print_table:
|
|
99
|
-
print()
|
|
100
|
-
print(f" {'file':70s} {'ratio':>7s} {'done':>5s} {'open':>5s} {'wip':>5s} {'cx':>5s}")
|
|
101
|
-
for r in sorted(rows, key=lambda x: (x["completion_ratio"] is None, -(x["completion_ratio"] or 0))):
|
|
102
|
-
ratio = "—" if r["completion_ratio"] is None else f"{r['completion_ratio']:.1%}"
|
|
103
|
-
print(
|
|
104
|
-
f" {Path(r['file']).name:70s} {ratio:>7s} "
|
|
105
|
-
f"{r['counts']['done']:>5d} {r['counts']['open']:>5d} "
|
|
106
|
-
f"{r['counts']['wip']:>5d} {r['counts']['cancelled']:>5d}"
|
|
107
|
-
)
|
|
108
|
-
return 0
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if __name__ == "__main__":
|
|
112
|
-
sys.exit(main())
|