@seanyao/roll 0.5.0 → 2.602.2
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/CHANGELOG.md +736 -0
- package/LICENSE +21 -0
- package/README.md +65 -165
- package/bin/dream-test-quality-scan +110 -0
- package/bin/roll +15030 -814
- package/conventions/config.yaml +17 -1
- package/conventions/global/AGENTS.md +146 -100
- package/conventions/global/CLAUDE.md +1 -21
- package/conventions/global/GEMINI.md +8 -22
- package/conventions/global/project_rules.md +9 -0
- package/conventions/templates/backend-service/AGENTS.md +30 -81
- package/conventions/templates/backend-service/GEMINI.md +3 -3
- package/conventions/templates/backend-service/project_rules.md +16 -0
- package/conventions/templates/cli/AGENTS.md +31 -58
- package/conventions/templates/cli/CLAUDE.md +3 -5
- package/conventions/templates/cli/GEMINI.md +3 -3
- package/conventions/templates/cli/project_rules.md +16 -0
- package/conventions/templates/frontend-only/AGENTS.md +29 -64
- package/conventions/templates/frontend-only/GEMINI.md +3 -3
- package/conventions/templates/frontend-only/project_rules.md +14 -0
- package/conventions/templates/fullstack/AGENTS.md +31 -79
- package/conventions/templates/fullstack/CLAUDE.md +1 -1
- package/conventions/templates/fullstack/GEMINI.md +3 -3
- package/conventions/templates/fullstack/project_rules.md +15 -0
- package/lib/README.md +42 -0
- package/lib/__pycache__/github_sync.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop-fmt.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop_result_eval.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop_unstick.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/prices_fetcher.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-home.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_git.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
- package/lib/agent_usage/README.md +49 -0
- package/lib/agent_usage/__init__.py +108 -0
- package/lib/agent_usage/__pycache__/__init__.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/gemini.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/kimi.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/openai.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/pi.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/pi_emit.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/qwen.cpython-314.pyc +0 -0
- package/lib/agent_usage/gemini.py +127 -0
- package/lib/agent_usage/kimi.py +278 -0
- package/lib/agent_usage/kimi_emit.py +123 -0
- package/lib/agent_usage/openai.py +126 -0
- package/lib/agent_usage/pi.py +200 -0
- package/lib/agent_usage/pi_emit.py +135 -0
- package/lib/agent_usage/qwen.py +128 -0
- package/lib/backfill-pi-usage.py +243 -0
- package/lib/changelog_audit.py +155 -0
- package/lib/changelog_generate.py +263 -0
- package/lib/context_feed_budget.sh +194 -0
- package/lib/github_sync.py +876 -0
- package/lib/i18n/README.md +54 -0
- package/lib/i18n/agent.sh +75 -0
- package/lib/i18n/alert.sh +20 -0
- package/lib/i18n/backlog.sh +96 -0
- package/lib/i18n/brief.sh +5 -0
- package/lib/i18n/changelog.sh +5 -0
- package/lib/i18n/ci.sh +15 -0
- package/lib/i18n/debug.sh +0 -0
- package/lib/i18n/doctor.sh +44 -0
- package/lib/i18n/dream.sh +0 -0
- package/lib/i18n/init.sh +91 -0
- package/lib/i18n/lang.sh +10 -0
- package/lib/i18n/loop.sh +140 -0
- package/lib/i18n/migrate.sh +74 -0
- package/lib/i18n/offboard.sh +31 -0
- package/lib/i18n/onboard.sh +0 -0
- package/lib/i18n/peer.sh +41 -0
- package/lib/i18n/peer_help.sh +25 -0
- package/lib/i18n/peer_reset.sh +7 -0
- package/lib/i18n/peer_status.sh +5 -0
- package/lib/i18n/prices.sh +3 -0
- package/lib/i18n/prices_refresh.sh +17 -0
- package/lib/i18n/prices_show.sh +7 -0
- package/lib/i18n/propose.sh +0 -0
- package/lib/i18n/release.sh +0 -0
- package/lib/i18n/research.sh +0 -0
- package/lib/i18n/review_pr.sh +0 -0
- package/lib/i18n/sentinel.sh +0 -0
- package/lib/i18n/setup.sh +3 -0
- package/lib/i18n/shared.sh +157 -0
- package/lib/i18n/skills/roll-brief.sh +47 -0
- package/lib/i18n/skills/roll-build.sh +97 -0
- package/lib/i18n/skills/roll-design.sh +18 -0
- package/lib/i18n/skills/roll-fix.sh +53 -0
- package/lib/i18n/skills/roll-loop.sh +28 -0
- package/lib/i18n/skills/roll-onboard.sh +33 -0
- package/lib/i18n/skills_catalog.sh +30 -0
- package/lib/i18n/slides.sh +3 -0
- package/lib/i18n/slides_build.sh +38 -0
- package/lib/i18n/slides_delete.sh +19 -0
- package/lib/i18n/slides_list.sh +14 -0
- package/lib/i18n/slides_logs.sh +12 -0
- package/lib/i18n/slides_new.sh +15 -0
- package/lib/i18n/slides_preview.sh +14 -0
- package/lib/i18n/slides_templates.sh +7 -0
- package/lib/i18n/status.sh +21 -0
- package/lib/i18n/update.sh +24 -0
- package/lib/i18n.sh +211 -0
- package/lib/loop-exit-summary.py +393 -0
- package/lib/loop-fmt.py +589 -0
- package/lib/loop_pick_agent.py +316 -0
- package/lib/loop_result_eval.py +469 -0
- package/lib/loop_unstick.py +180 -0
- package/lib/model_prices.py +194 -0
- package/lib/prices/README.md +35 -0
- package/lib/prices/snapshot-2026-05-22.json +22 -0
- package/lib/prices/snapshot-2026-05-23-deepseek.json +15 -0
- package/lib/prices/snapshot-2026-05-23-kimi.json +15 -0
- package/lib/prices_fetcher.py +285 -0
- package/lib/roll-backlog.py +225 -0
- package/lib/roll-brief.py +286 -0
- package/lib/roll-help.py +158 -0
- package/lib/roll-home.py +556 -0
- package/lib/roll-init.py +156 -0
- package/lib/roll-loop-status.py +1683 -0
- package/lib/roll-loop-story.py +191 -0
- package/lib/roll-onboard-render.py +378 -0
- package/lib/roll-peer.py +252 -0
- package/lib/roll-plan-validate.py +386 -0
- package/lib/roll-setup.py +102 -0
- package/lib/roll-status.py +367 -0
- package/lib/roll_git.py +41 -0
- package/lib/roll_render.py +414 -0
- package/lib/slides/components/README.md +123 -0
- package/lib/slides/components/cards-2.html +9 -0
- package/lib/slides/components/cards-3.html +9 -0
- package/lib/slides/components/cards-4.html +9 -0
- package/lib/slides/components/compare.html +22 -0
- package/lib/slides/components/highlight.html +9 -0
- package/lib/slides/components/pipeline.html +12 -0
- package/lib/slides/components/plain.html +7 -0
- package/lib/slides/components/quote.html +4 -0
- package/lib/slides/components/timeline.html +9 -0
- package/lib/slides/templates/introduction-v3.html +571 -0
- package/lib/slides/templates/pitch.html +0 -0
- package/lib/slides-render.py +778 -0
- package/lib/slides-validate.py +357 -0
- package/lib/test_quality_gate.py +143 -0
- package/package.json +8 -7
- package/skills/roll-.changelog/SKILL.md +406 -33
- package/skills/roll-.clarify/SKILL.md +5 -2
- package/skills/roll-.dream/SKILL.md +374 -0
- package/skills/roll-.echo/SKILL.md +5 -2
- package/skills/roll-.qa/SKILL.md +57 -3
- package/skills/roll-.review/SKILL.md +42 -3
- package/skills/roll-brief/SKILL.md +209 -0
- package/skills/roll-build/SKILL.md +308 -63
- package/skills/roll-debug/SKILL.md +341 -162
- package/skills/roll-debug/injectable-bb.js +263 -0
- package/skills/roll-deck/SKILL.md +296 -0
- package/skills/roll-design/ENGINEERING_CHECKLIST.md +1 -1
- package/skills/roll-design/SKILL.md +733 -94
- package/skills/roll-doc/SKILL.md +595 -0
- package/skills/roll-doctor/SKILL.md +192 -0
- package/skills/roll-fix/SKILL.md +149 -32
- package/skills/{roll-jot → roll-idea}/SKILL.md +18 -10
- package/skills/roll-loop/SKILL.md +579 -0
- package/skills/roll-notes/SKILL.md +103 -0
- package/skills/roll-onboard/SKILL.md +234 -0
- package/skills/roll-peer/SKILL.md +336 -0
- package/skills/roll-propose/SKILL.md +157 -0
- package/skills/roll-review-pr/SKILL.md +58 -0
- package/skills/roll-sentinel/SKILL.md +11 -2
- package/skills/roll-spar/SKILL.md +8 -6
- package/template/.github/workflows/ci.yml +5 -2
- package/template/AGENTS.md +20 -74
- package/skills/roll-research/SKILL.md +0 -307
- package/skills/roll-research/references/schema.json +0 -162
- package/skills/roll-research/scripts/md_to_pdf.py +0 -289
- package/tools/roll-fetch/SKILL.md +0 -182
- package/tools/roll-fetch/package.json +0 -15
- package/tools/roll-fetch/smart-web-fetch.js +0 -558
- package/tools/roll-probe/SKILL.md +0 -84
- /package/template/{BACKLOG.md → .roll/backlog.md} +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Classify a backlog story into a complexity tier (US-AGENT-022).
|
|
3
|
+
|
|
4
|
+
Supersedes the three-dimensional (type/est_min/risk_zone) hard-rule matcher
|
|
5
|
+
and the history-driven soft preference (US-AGENT-004/005). Routing now turns
|
|
6
|
+
on a single axis: the story's ``est_min`` estimate maps to one of three
|
|
7
|
+
complexity tiers — ``easy`` / ``default`` / ``hard``. The tier → agent
|
|
8
|
+
resolution (reading ``agents.yaml`` slots, fallback) lands in US-AGENT-023;
|
|
9
|
+
this module is the pure classifier.
|
|
10
|
+
|
|
11
|
+
Emits a single line on stdout::
|
|
12
|
+
|
|
13
|
+
<tier> <rationale>
|
|
14
|
+
|
|
15
|
+
where ``tier`` is one of ``easy`` / ``default`` / ``hard``.
|
|
16
|
+
|
|
17
|
+
Tier boundaries (centralised constants, intentionally NOT user-configurable
|
|
18
|
+
to keep routing variance to a single axis):
|
|
19
|
+
|
|
20
|
+
est_min <= 8 → easy
|
|
21
|
+
8 < est_min <= 20 → default
|
|
22
|
+
est_min > 20 → hard
|
|
23
|
+
missing / illegal est → default
|
|
24
|
+
|
|
25
|
+
Exit codes:
|
|
26
|
+
0 — tier classified (always succeeds once the story is found)
|
|
27
|
+
1 — story id not found in backlog / unrecoverable error
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
loop_pick_agent.py --story-id US-AGENT-022 --backlog .roll/backlog.md
|
|
31
|
+
loop_pick_agent.py --est-min 12 # classify a bare estimate
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import re
|
|
37
|
+
import sys
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
# Complexity-tier boundaries. Single source of truth — change here only.
|
|
41
|
+
EASY_MAX_MIN = 8 # est_min <= 8 → easy
|
|
42
|
+
HARD_MIN_MIN = 20 # est_min > 20 → hard
|
|
43
|
+
TIER_EASY = "easy"
|
|
44
|
+
TIER_DEFAULT = "default"
|
|
45
|
+
TIER_HARD = "hard"
|
|
46
|
+
|
|
47
|
+
PROFILE_BLOCK_RE = re.compile(r"\*\*Agent profile:\*\*")
|
|
48
|
+
EST_RE = re.compile(r"^\s*-\s*est_min:\s*(\d+)")
|
|
49
|
+
ANCHOR_TEMPLATE = '<a id="{anchor}"></a>'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _classify_complexity(est_min) -> str:
|
|
53
|
+
"""Map an ``est_min`` estimate onto a complexity tier.
|
|
54
|
+
|
|
55
|
+
``<= 8`` → easy, ``> 20`` → hard, ``8 < x <= 20`` → default. A missing or
|
|
56
|
+
non-integer estimate (None, "", non-numeric) falls back to ``default``.
|
|
57
|
+
"""
|
|
58
|
+
if est_min is None:
|
|
59
|
+
return TIER_DEFAULT
|
|
60
|
+
try:
|
|
61
|
+
n = int(est_min)
|
|
62
|
+
except (TypeError, ValueError):
|
|
63
|
+
return TIER_DEFAULT
|
|
64
|
+
if n < 0:
|
|
65
|
+
# Negative estimate is invalid data → treat like missing.
|
|
66
|
+
return TIER_DEFAULT
|
|
67
|
+
if n <= EASY_MAX_MIN:
|
|
68
|
+
return TIER_EASY
|
|
69
|
+
if n > HARD_MIN_MIN:
|
|
70
|
+
return TIER_HARD
|
|
71
|
+
return TIER_DEFAULT
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
# US-AGENT-030: transparent, auditable in-tier soft nudge.
|
|
76
|
+
#
|
|
77
|
+
# The complexity tier (easy/default/hard from ``_classify_complexity``) is a
|
|
78
|
+
# HARD constraint — it decides which agents.yaml slot is consulted and a task is
|
|
79
|
+
# NEVER moved out of its tier. On top of that hard floor this adds a SOFT
|
|
80
|
+
# priority: among the candidate agents already associated with this tier, prefer
|
|
81
|
+
# the one with the best per-(agent × story_type) historical hit-rate.
|
|
82
|
+
#
|
|
83
|
+
# How this differs from the US-AGENT-022-retired soft preference (the whole
|
|
84
|
+
# point of the story):
|
|
85
|
+
# - deterministic: same history in → same agent out. No rng, no time seed,
|
|
86
|
+
# no decay clock. ``nudge_within_tier`` is a pure function of its arguments.
|
|
87
|
+
# - auditable: every decision returns a human-readable rationale string that
|
|
88
|
+
# the caller logs into runs.jsonl + the event log.
|
|
89
|
+
# - sample floor: a (agent, story_type) combo below ``sample_floor`` does not
|
|
90
|
+
# participate; the slot agent is kept and the audit line says so.
|
|
91
|
+
# - one switch: ``enabled=False`` makes this an exact identity — it returns
|
|
92
|
+
# the slot agent unchanged, behaving precisely like US-AGENT-023.
|
|
93
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
# Default minimum samples a (agent × story_type) combo needs before its hit-rate
|
|
96
|
+
# is allowed to influence routing. Below this the combo is statistically
|
|
97
|
+
# meaningless, so we keep the operator's slot choice. Centralised constant.
|
|
98
|
+
SAMPLE_FLOOR = 8
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def nudge_within_tier(slot_agent, candidates, story_type, hit_rates,
|
|
102
|
+
sample_floor=SAMPLE_FLOOR, enabled=True):
|
|
103
|
+
"""Reorder same-tier candidates by historical hit-rate; return the winner.
|
|
104
|
+
|
|
105
|
+
Pure function — no I/O, no randomness, no clock. Given the same arguments
|
|
106
|
+
it always returns the same ``(chosen_agent, rationale)`` pair.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
slot_agent: the agent the est_min tier slot resolved to (the hard-floor
|
|
110
|
+
default). Always the fallback / tie-break winner.
|
|
111
|
+
candidates: iterable of in-tier candidate agent names (already
|
|
112
|
+
constrained to this tier + installed by the caller). The
|
|
113
|
+
slot agent is folded in even if absent.
|
|
114
|
+
story_type: the story's type bucket (e.g. "US" / "FIX"); the hit-rate is
|
|
115
|
+
looked up per (agent, story_type).
|
|
116
|
+
hit_rates: {"<agent>\\x1f<story_type>": {"hit_rate": float,
|
|
117
|
+
"sample_n": int}} (the loop_result_eval read model).
|
|
118
|
+
sample_floor: combos with sample_n < this are ignored (default 8).
|
|
119
|
+
enabled: when False, returns (slot_agent, "<reason: disabled>") with
|
|
120
|
+
no reordering — exact US-AGENT-023 behaviour.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
(chosen_agent, rationale) where rationale is a one-line audit string.
|
|
124
|
+
"""
|
|
125
|
+
if not slot_agent:
|
|
126
|
+
return (slot_agent, "no slot agent; nudge skipped")
|
|
127
|
+
if not enabled:
|
|
128
|
+
return (slot_agent, "nudge disabled; keeping est_min slot %s" % slot_agent)
|
|
129
|
+
|
|
130
|
+
# Build the candidate set: the slot agent is always in the running, plus any
|
|
131
|
+
# caller-supplied in-tier candidates. De-dup but keep a deterministic order
|
|
132
|
+
# (slot agent first, then the rest sorted) so iteration is reproducible.
|
|
133
|
+
seen = {slot_agent}
|
|
134
|
+
rest = []
|
|
135
|
+
for c in (candidates or []):
|
|
136
|
+
if c and c not in seen:
|
|
137
|
+
seen.add(c)
|
|
138
|
+
rest.append(c)
|
|
139
|
+
ordered = [slot_agent] + sorted(rest)
|
|
140
|
+
|
|
141
|
+
def _stat(agent):
|
|
142
|
+
key = "%s\x1f%s" % (agent, story_type)
|
|
143
|
+
st = (hit_rates or {}).get(key) or {}
|
|
144
|
+
try:
|
|
145
|
+
n = int(st.get("sample_n", 0))
|
|
146
|
+
except (TypeError, ValueError):
|
|
147
|
+
n = 0
|
|
148
|
+
try:
|
|
149
|
+
hr = float(st.get("hit_rate", 0.0))
|
|
150
|
+
except (TypeError, ValueError):
|
|
151
|
+
hr = 0.0
|
|
152
|
+
return hr, n
|
|
153
|
+
|
|
154
|
+
# Eligible = combos that clear the sample floor.
|
|
155
|
+
eligible = []
|
|
156
|
+
for a in ordered:
|
|
157
|
+
hr, n = _stat(a)
|
|
158
|
+
if n >= sample_floor:
|
|
159
|
+
eligible.append((a, hr, n))
|
|
160
|
+
|
|
161
|
+
if not eligible:
|
|
162
|
+
return (slot_agent,
|
|
163
|
+
"n<%d for all %s candidates in this tier; keeping slot %s"
|
|
164
|
+
% (sample_floor, story_type, slot_agent))
|
|
165
|
+
|
|
166
|
+
# Best hit-rate wins. Deterministic tie-break: the slot agent first (it is
|
|
167
|
+
# always index 0 in ``ordered``), then the earliest candidate in the stable
|
|
168
|
+
# order. Sort by (-hit_rate, ordered_index) so ties never depend on dict
|
|
169
|
+
# iteration or locale.
|
|
170
|
+
index_of = {}
|
|
171
|
+
for i, a in enumerate(ordered):
|
|
172
|
+
index_of[a] = i
|
|
173
|
+
eligible.sort(key=lambda t: (-t[1], index_of[t[0]]))
|
|
174
|
+
best_agent, best_hr, best_n = eligible[0]
|
|
175
|
+
|
|
176
|
+
slot_hr, slot_n = _stat(slot_agent)
|
|
177
|
+
if best_agent == slot_agent:
|
|
178
|
+
return (slot_agent,
|
|
179
|
+
"%s best for %s in-tier (hit_rate %.2f, n=%d); slot kept"
|
|
180
|
+
% (slot_agent, story_type, best_hr, best_n))
|
|
181
|
+
return (best_agent,
|
|
182
|
+
"%s in-tier hit_rate %.2f (n=%d) > slot %s %.2f (n=%d) for %s -> prefer %s"
|
|
183
|
+
% (best_agent, best_hr, best_n, slot_agent, slot_hr, slot_n,
|
|
184
|
+
story_type, best_agent))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _id_to_anchor(story_id: str) -> str:
|
|
188
|
+
return story_id.lower()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _find_feature_md(backlog_path: Path, story_id: str) -> Path | None:
|
|
192
|
+
"""Resolve feature md path by scanning backlog rows for the story id."""
|
|
193
|
+
if not backlog_path.exists():
|
|
194
|
+
return None
|
|
195
|
+
link_re = re.compile(
|
|
196
|
+
r"\[" + re.escape(story_id) + r"\]\((\.roll/features/[^)]+?)#",
|
|
197
|
+
re.IGNORECASE,
|
|
198
|
+
)
|
|
199
|
+
for line in backlog_path.read_text(encoding="utf-8").splitlines():
|
|
200
|
+
m = link_re.search(line)
|
|
201
|
+
if m:
|
|
202
|
+
return Path(m.group(1))
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _read_est_min(feature_md: Path, story_id: str):
|
|
207
|
+
"""Return the story's est_min as an int, or None if not found."""
|
|
208
|
+
if not feature_md.exists():
|
|
209
|
+
return None
|
|
210
|
+
anchor = ANCHOR_TEMPLATE.format(anchor=_id_to_anchor(story_id))
|
|
211
|
+
text = feature_md.read_text(encoding="utf-8")
|
|
212
|
+
if anchor not in text:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
# Slice from the anchor to the next anchor or EOF.
|
|
216
|
+
start = text.index(anchor)
|
|
217
|
+
next_anchor_match = re.search(r'<a id="[^"]+"></a>', text[start + len(anchor):])
|
|
218
|
+
end = start + len(anchor) + (next_anchor_match.start() if next_anchor_match else len(text))
|
|
219
|
+
section = text[start:end]
|
|
220
|
+
|
|
221
|
+
if not PROFILE_BLOCK_RE.search(section):
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
for line in section.splitlines():
|
|
225
|
+
m = EST_RE.match(line)
|
|
226
|
+
if m:
|
|
227
|
+
return int(m.group(1))
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def classify_story(story_id: str, backlog_path: Path) -> tuple[str, str] | None:
|
|
232
|
+
"""Return (tier, rationale) for a backlog story, or None on hard error."""
|
|
233
|
+
feature_md = _find_feature_md(backlog_path, story_id)
|
|
234
|
+
if feature_md is None:
|
|
235
|
+
return None # story id not in backlog
|
|
236
|
+
est_min = _read_est_min(feature_md, story_id)
|
|
237
|
+
tier = _classify_complexity(est_min)
|
|
238
|
+
if est_min is None:
|
|
239
|
+
rationale = f"no est_min for {story_id}; tier={tier} (default)"
|
|
240
|
+
else:
|
|
241
|
+
rationale = f"est_min={est_min} → tier={tier}"
|
|
242
|
+
return (tier, rationale)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def main() -> int:
|
|
246
|
+
parser = argparse.ArgumentParser()
|
|
247
|
+
parser.add_argument("--story-id")
|
|
248
|
+
parser.add_argument("--backlog", default=".roll/backlog.md")
|
|
249
|
+
parser.add_argument("--est-min", default=None,
|
|
250
|
+
help="classify a bare estimate without a backlog lookup")
|
|
251
|
+
# Accepted for backward-compatible invocation; routing no longer reads
|
|
252
|
+
# agent-routes.yaml or runs.jsonl (US-AGENT-022 retires the 3-dim matcher
|
|
253
|
+
# and history soft preference). Tier→agent resolution is US-AGENT-023.
|
|
254
|
+
parser.add_argument("--routes", default=None, help=argparse.SUPPRESS)
|
|
255
|
+
parser.add_argument("--runs", default=None, help=argparse.SUPPRESS)
|
|
256
|
+
# US-AGENT-030: in-tier soft nudge. When --nudge is given, the other --nudge-*
|
|
257
|
+
# args drive nudge_within_tier and the chosen agent + rationale are printed as
|
|
258
|
+
# "<agent>\t<rationale>" (tab-separated so the rationale can carry spaces).
|
|
259
|
+
parser.add_argument("--nudge", action="store_true",
|
|
260
|
+
help="reorder in-tier candidates by historical hit-rate")
|
|
261
|
+
parser.add_argument("--slot-agent", default=None,
|
|
262
|
+
help="the est_min tier slot agent (nudge hard-floor default)")
|
|
263
|
+
parser.add_argument("--story-type", default="",
|
|
264
|
+
help="story type bucket for the hit-rate lookup (US/FIX/...)")
|
|
265
|
+
parser.add_argument("--candidates", default="",
|
|
266
|
+
help="comma-separated in-tier candidate agent names")
|
|
267
|
+
parser.add_argument("--hit-rates", default=None,
|
|
268
|
+
help="hit-rate read model JSON (from loop_result_eval --hit-rates); "
|
|
269
|
+
"reads stdin if omitted")
|
|
270
|
+
parser.add_argument("--sample-floor", type=int, default=SAMPLE_FLOOR,
|
|
271
|
+
help="min sample_n a combo needs to influence routing")
|
|
272
|
+
parser.add_argument("--disabled", action="store_true",
|
|
273
|
+
help="run the identity path (exact US-AGENT-023 behaviour)")
|
|
274
|
+
args = parser.parse_args()
|
|
275
|
+
|
|
276
|
+
if args.nudge:
|
|
277
|
+
if not args.slot_agent:
|
|
278
|
+
print("loop_pick_agent: --slot-agent required with --nudge", file=sys.stderr)
|
|
279
|
+
return 1
|
|
280
|
+
import json
|
|
281
|
+
raw = args.hit_rates
|
|
282
|
+
if raw is None:
|
|
283
|
+
raw = sys.stdin.read()
|
|
284
|
+
try:
|
|
285
|
+
hit_rates = json.loads(raw) if raw and raw.strip() else {}
|
|
286
|
+
except (ValueError, TypeError) as exc:
|
|
287
|
+
print(f"loop_pick_agent: bad hit-rates JSON: {exc}", file=sys.stderr)
|
|
288
|
+
return 1
|
|
289
|
+
candidates = [c.strip() for c in args.candidates.split(",") if c.strip()]
|
|
290
|
+
chosen, rationale = nudge_within_tier(
|
|
291
|
+
args.slot_agent, candidates, args.story_type, hit_rates,
|
|
292
|
+
sample_floor=args.sample_floor, enabled=not args.disabled)
|
|
293
|
+
# Tab-separated: field 1 = chosen agent, field 2 = audit rationale.
|
|
294
|
+
print(f"{chosen}\t{rationale}")
|
|
295
|
+
return 0
|
|
296
|
+
|
|
297
|
+
if args.est_min is not None:
|
|
298
|
+
tier = _classify_complexity(args.est_min)
|
|
299
|
+
print(f"{tier} est_min={args.est_min} → tier={tier}")
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
if not args.story_id:
|
|
303
|
+
print("loop_pick_agent: --story-id or --est-min required", file=sys.stderr)
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
result = classify_story(args.story_id, Path(args.backlog))
|
|
307
|
+
if result is None:
|
|
308
|
+
print(f"loop_pick_agent: cannot classify {args.story_id}", file=sys.stderr)
|
|
309
|
+
return 1
|
|
310
|
+
tier, rationale = result
|
|
311
|
+
print(f"{tier} {rationale}")
|
|
312
|
+
return 0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
sys.exit(main())
|