@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
@@ -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())
@@ -665,7 +665,7 @@ def execute(
665
665
  resume: bool = False,
666
666
  ) -> None:
667
667
  branch = f"release/{plan.target}"
668
- total = 9
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 from existing release branches.
908
-
909
- Resume mode needs to know which `release/X.Y.Z` is being recovered,
910
- not what the next bump would be. The release branch name is the
911
- canonical anchor: it was committed by step 1 of an earlier run and
912
- is the only state guaranteed to survive a partial pipeline.
913
-
914
- Local branches win over remote, current-branch wins over both — if
915
- you ran `git checkout release/1.15.0`, that's the target. Returns
916
- None if no release branch exists; caller falls back to the regular
917
- bump-inference path.
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
- local_raw = git("for-each-ref", "--format=%(refname:short)", "refs/heads/release/", capture=True)
925
- candidates = [
926
- m.group(1)
927
- for line in local_raw.splitlines()
928
- if (m := _RELEASE_BRANCH_RE.match(line.strip()))
929
- ]
930
- remote_raw = git(
931
- "for-each-ref", "--format=%(refname:short)",
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
- if not candidates:
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
- # Sort semver-aware so 1.10.0 > 1.9.0 (lexicographic would lose).
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) detected in-flight release branch release/{in_flight}")
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 — package-shipped or installed copy."""
120
- for candidate in (
121
- consumer_root / ".augment" / "scripts" / "update_roadmap_progress.py",
122
- consumer_root / ".agent-src" / "scripts" / "update_roadmap_progress.py",
123
- consumer_root / ".agent-src.uncondensed" / "scripts" / "update_roadmap_progress.py",
124
- ):
161
+ """Find the regenerator script.
162
+
163
+ Project-local copy first (project-scoped installs), then the package
164
+ the hook itself ships in (global-only consumers, per ADR-020 — the
165
+ repo has no project-local distributed content). Returns ``None`` only
166
+ when no copy exists in either place.
167
+ """
168
+ for subdir in DIST_SCRIPT_SUBDIRS:
169
+ candidate = consumer_root / subdir / REGEN_NAME
125
170
  if candidate.is_file():
126
171
  return candidate
172
+ for root in _package_roots():
173
+ for subdir in DIST_SCRIPT_SUBDIRS:
174
+ candidate = root / subdir / REGEN_NAME
175
+ if candidate.is_file():
176
+ return candidate
127
177
  return None
128
178
 
129
179
 
@@ -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 = tmpdir / ".agent-settings.yml"
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(".agent-settings.yml not written by installer"), None
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=DEFAULT_SETTINGS,
89
- help=f"target settings file (default: ./{DEFAULT_SETTINGS})")
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 = Path(args.path)
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 / ".agent-settings.yml"]
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 / ".agent-settings.yml"]
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())