@seanyao/roll 2.602.1 → 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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.602.2
4
+
5
+ ### 自动化流水线
6
+
7
+ - **自家 PR 跑红了会自动后台修(US-LOOP-062a)** — loop 开的 PR 一旦 CI 变红,会在后台 checkout 交给 agent 修(有次数上限、不重复触发),修不好或关了自愈才告警,不再烂成没人管的僵尸 PR
8
+ - **你批准过的绿 PR 会被自动合并(US-LOOP-062b)** — 你 review 通过、CI 又绿的 PR,loop 直接帮你合并删分支,不用再等仓库级 auto-merge、也不用手点
9
+
10
+ ### 可见性
11
+
12
+ - **kimi / deepseek 成本按人民币 ¥ 显示(FIX-162)** — 之前 kimi 的成本被误标成美元 $,现在和 deepseek 一样按 ¥ 显示,成本总账不再混币种
13
+
14
+ ### 稳定性
15
+
16
+ - **升级提示不再反向叫你装回旧版本(FIX-163)** — 换上更短的新版本号后,`roll loop on` 等命令一度提示"升级"回旧的年份版本号;现在按 GitHub 最新发布判断,装的是最新就不再误报,发版遇到新号比线上"看起来小"也不再卡住
17
+
18
+ ### 工程和测试
19
+
20
+ - **roll-design 中等复杂度也过一道 peer(US-SKILL-018)** — 以前只有大改或跨边界的设计才自动触发 peer 评审,现在中等复杂度也会过一道,方向隐患能在拆故事前被独立挑一次;10 秒内可跳过
21
+
3
22
  ## v2.602.1
4
23
 
5
24
  ### 新功能
package/bin/roll CHANGED
@@ -4,7 +4,7 @@ set -euo pipefail
4
4
  # Roll — AI Agent Convention Manager
5
5
  # Single source of truth for how all AI coding agents behave.
6
6
 
7
- VERSION="2.602.1"
7
+ VERSION="2.602.2"
8
8
  ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
9
9
  ROLL_CONFIG="${ROLL_HOME}/config.yaml"
10
10
  ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
@@ -11295,6 +11295,142 @@ _loop_heal_dir() {
11295
11295
  printf '%s\n' "${ROLL_LOOP_DIR:-${HOME}/.shared/roll/loop}/heal"
11296
11296
  }
11297
11297
 
11298
+ # US-LOOP-062a: deduped [TYPE:loop-pr-ci-red] ALERT for a red loop/* PR. One
11299
+ # line per PR until the alert file is consumed — never silently drops.
11300
+ _loop_pr_ci_red_alert() {
11301
+ local num="$1" head_ref="$2" msg="${3:-own loop PR CI red — needs heal}"
11302
+ local alert="${_LOOP_ALERT}"
11303
+ [ -n "$alert" ] || return 0
11304
+ mkdir -p "$(dirname "$alert")" 2>/dev/null || true
11305
+ grep -qF "[TYPE:loop-pr-ci-red] PR #${num} " "$alert" 2>/dev/null && return 0
11306
+ printf '[%s] [error] [TYPE:loop-pr-ci-red] PR #%s %s: %s\n' \
11307
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$num" "$head_ref" "$msg" >> "$alert"
11308
+ }
11309
+
11310
+ # US-LOOP-062a: upsert "key: value" in the per-slug loop state file (same flat
11311
+ # format the HEAD-CI heal counter uses).
11312
+ _loop_state_set() {
11313
+ local key="$1" val="$2" state="${_LOOP_STATE}"
11314
+ [ -n "$state" ] || return 0
11315
+ mkdir -p "$(dirname "$state")" 2>/dev/null || true
11316
+ if [ -f "$state" ] && grep -q "^${key}:" "$state" 2>/dev/null; then
11317
+ local _tmp; _tmp=$(mktemp)
11318
+ grep -v "^${key}:" "$state" > "$_tmp" 2>/dev/null || true
11319
+ printf '%s: %s\n' "$key" "$val" >> "$_tmp"
11320
+ mv "$_tmp" "$state"
11321
+ else
11322
+ printf '%s: %s\n' "$key" "$val" >> "$state"
11323
+ fi
11324
+ }
11325
+
11326
+ # US-LOOP-062a: background-heal a red loop/* PR (loop_self_ci_red verdict).
11327
+ # Bounded by heal_count.pr:<num> (max ROLL_LOOP_HEAL_MAX, default 2);
11328
+ # ROLL_LOOP_NO_HEAL=1 disables. A per-PR lock (pid marker) prevents duplicate
11329
+ # concurrent heals; a stale lock from a dead pid is reclaimed. On disabled /
11330
+ # budget-exhausted / nothing-to-do → a deduped ALERT (never silent). The heal
11331
+ # agent is chosen via _project_agent() (no bare `claude -p`). Non-blocking: the
11332
+ # actual checkout→fix→push runs in a background subshell so the PR tick returns
11333
+ # immediately.
11334
+ _loop_pr_heal_self() {
11335
+ local num="$1" head_ref="$2" slug="${3:-}"
11336
+ [ -n "$num" ] || return 0
11337
+
11338
+ local heal_max="${ROLL_LOOP_HEAL_MAX:-2}"
11339
+ if [ "${ROLL_LOOP_NO_HEAL:-}" = "1" ] || [ "${heal_max:-0}" -le 0 ]; then
11340
+ _loop_pr_ci_red_alert "$num" "$head_ref" "auto-heal off (ROLL_LOOP_NO_HEAL) — fix manually"
11341
+ return 0
11342
+ fi
11343
+
11344
+ local heal_dir; heal_dir="$(_loop_heal_dir)"
11345
+ mkdir -p "$heal_dir" 2>/dev/null || true
11346
+ local lock="${heal_dir}/pr-${num}.lock"
11347
+ if [ -f "$lock" ]; then
11348
+ local lpid; lpid=$(cat "$lock" 2>/dev/null || echo "")
11349
+ if [ -n "$lpid" ] && kill -0 "$lpid" 2>/dev/null; then
11350
+ return 0 # heal already in flight for this PR
11351
+ fi
11352
+ rm -f "$lock" # stale lock (dead pid) — reclaim
11353
+ fi
11354
+
11355
+ local key="heal_count.pr:${num}"
11356
+ local count=0
11357
+ [ -f "${_LOOP_STATE}" ] && count=$(grep "^${key}:" "${_LOOP_STATE}" 2>/dev/null | awk '{print $2}' | head -1)
11358
+ count=$(( ${count:-0} + 0 ))
11359
+ if [ "$count" -ge "$heal_max" ]; then
11360
+ _loop_pr_ci_red_alert "$num" "$head_ref" "auto-heal budget exhausted (${count}/${heal_max}) — fix manually"
11361
+ return 0
11362
+ fi
11363
+ _loop_state_set "$key" "$(( count + 1 ))"
11364
+
11365
+ local agent; agent="$(_project_agent 2>/dev/null)"; agent="${agent:-claude}"
11366
+
11367
+ ( echo "$BASHPID" > "$lock"
11368
+ _loop_pr_do_heal "$num" "$head_ref" "$slug" "$agent" >/dev/null 2>&1
11369
+ rm -f "$lock"
11370
+ ) &
11371
+ disown 2>/dev/null || true
11372
+ info "PR #${num}: background heal $(( count + 1 ))/${heal_max} dispatched (agent=${agent})"
11373
+ return 0
11374
+ }
11375
+
11376
+ # US-LOOP-062a: the actual heal work (runs in the background subshell). Gathers
11377
+ # the failing-CI context, checks out the PR branch in a throwaway worktree, hands
11378
+ # the fix to the dynamically-selected agent via _agent_argv (no bare claude -p),
11379
+ # and pushes back to the SAME PR branch. Best-effort: any failure leaves the PR
11380
+ # untouched for the next tick (the heal budget caps retries). Overridable in
11381
+ # tests.
11382
+ _loop_pr_do_heal() {
11383
+ local num="$1" head_ref="$2" slug="${3:-}" agent="${4:-claude}"
11384
+ [ -n "$num" ] && [ -n "$head_ref" ] || return 1
11385
+ command -v gh >/dev/null 2>&1 || return 1
11386
+ [ -n "$slug" ] || _gh_resolve slug || return 1
11387
+
11388
+ # Capture failing-run context for the fix prompt.
11389
+ local ctx="/tmp/roll-heal-pr-${num}.log"
11390
+ {
11391
+ printf '=== CI heal context: PR #%s (%s) ===\n\n' "$num" "$head_ref"
11392
+ gh -R "$slug" pr checks "$num" 2>/dev/null || true
11393
+ local _run
11394
+ _run=$(gh -R "$slug" pr checks "$num" --json link --jq '.[]|select(.state=="FAILURE")|.link' 2>/dev/null \
11395
+ | grep -oE 'runs/[0-9]+' | head -1 | cut -d/ -f2)
11396
+ if [ -n "$_run" ]; then
11397
+ printf '\n--- failing run log (tail) ---\n'
11398
+ gh -R "$slug" run view "$_run" --log-failed 2>/dev/null | tail -200 || true
11399
+ fi
11400
+ } > "$ctx" 2>&1
11401
+
11402
+ # Isolated worktree on the PR branch.
11403
+ local wt; wt="$(mktemp -d)/pr-${num}"
11404
+ git fetch origin "$head_ref" >/dev/null 2>&1 || return 1
11405
+ git worktree add "$wt" "origin/${head_ref}" >/dev/null 2>&1 || { rm -rf "$(dirname "$wt")"; return 1; }
11406
+
11407
+ local prompt="[roll PR 自愈] PR #${num} (${head_ref}) 的 CI 红了。失败上下文见 ${ctx}。请只修使 CI 转绿所需的最小改动,保持 TCR 微提交节奏,改完直接 commit。不要改无关代码,不要反问。"
11408
+ _agent_argv "$agent" text "$prompt"
11409
+ ( cd "$wt" && "${_AGENT_ARGV[@]}" ) >/dev/null 2>&1 || true
11410
+
11411
+ # Push back to the same PR branch if the agent produced commits.
11412
+ if [ -n "$(cd "$wt" && git rev-list "origin/${head_ref}..HEAD" 2>/dev/null)" ]; then
11413
+ ( cd "$wt" && git push origin "HEAD:${head_ref}" ) >/dev/null 2>&1 || true
11414
+ fi
11415
+ git worktree remove --force "$wt" >/dev/null 2>&1 || true
11416
+ rm -rf "$(dirname "$wt")" 2>/dev/null || true
11417
+ }
11418
+
11419
+ # US-LOOP-062b: merge a human-approved PR directly when CI is green and the PR
11420
+ # is conflict-free, instead of waiting for repo-level auto-merge (which may be
11421
+ # disabled). Mirrors the bot-approved eager-merge. Merge failure is NON-fatal:
11422
+ # the PR is left open and the next PR-loop tick retries.
11423
+ _loop_pr_merge_approved() {
11424
+ local num="$1" ci_state="$2" mergeable="$3" slug="$4"
11425
+ [ -n "$num" ] && [ -n "$slug" ] || return 0
11426
+ [ "$ci_state" = "success" ] && [ "$mergeable" = "MERGEABLE" ] || return 0
11427
+ if gh -R "$slug" pr merge "$num" --squash --delete-branch >/dev/null 2>&1; then
11428
+ info "PR #${num}: human-approved + CI green — merged"
11429
+ else
11430
+ warn "PR #${num}: merge failed (human-approved + CI green) — left open, will retry"
11431
+ fi
11432
+ }
11433
+
11298
11434
  # REFACTOR-030: removed `_loop_self_heal_ci` and `_loop_clear_heal_state`.
11299
11435
  # REFACTOR-023 merged the CI self-heal counter into the main state.yaml flow,
11300
11436
  # but the two helpers themselves were left behind as dead code. Their job
@@ -11717,22 +11853,19 @@ _loop_pr_inbox() {
11717
11853
  _loop_pr_merge_self_eager "$num" "$ci_state" "$mergeable" "$slug"
11718
11854
  ;;
11719
11855
  loop_self_ci_red)
11720
- # FIX-158: _loop_pr_classify (US-LOOP-049) labels a red loop/* PR
11721
- # loop_self_ci_red, but US-LOOP-050's heal handler was lost in
11722
- # REFACTOR-030 this verdict had no branch, so a red self-PR fell
11723
- # through silently: GitHub auto-merge can't merge it (CI red) and
11724
- # nothing healed it, leaving a permanent zombie PR. Until the full
11725
- # checkout→fix→push auto-heal is re-wired, surface a deduped ALERT so
11726
- # the red self-PR is visible instead of dropped (full fix: FIX-158).
11727
- local _ci_red_alert="$_LOOP_ALERT"
11728
- mkdir -p "$(dirname "$_ci_red_alert")" 2>/dev/null || true
11729
- if ! grep -qF "[TYPE:loop-pr-ci-red] PR #${num} " "$_ci_red_alert" 2>/dev/null; then
11730
- printf '[%s] [error] [TYPE:loop-pr-ci-red] PR #%s %s: own loop PR CI red — needs heal (FIX-158)\n' \
11731
- "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$num" "$head_ref" >> "$_ci_red_alert"
11732
- fi
11856
+ # US-LOOP-062a: a red loop/* PR (classified by US-LOOP-049) is now
11857
+ # background-healed: bounded retries via heal budget + dynamic agent,
11858
+ # falling back to the deduped [TYPE:loop-pr-ci-red] ALERT (FIX-158's
11859
+ # surfacing) when heal is disabled/exhausted. Re-wires US-LOOP-050.
11860
+ _loop_pr_heal_self "$num" "$head_ref" "$slug" || true
11861
+ ;;
11862
+ blocked_human_request_changes)
11863
+ : # skip — last human review requested changes; wait for the author
11733
11864
  ;;
11734
- blocked_human_request_changes|blocked_human_approved)
11735
- : # skipexplained by verdict; nothing to do this cycle
11865
+ blocked_human_approved)
11866
+ # US-LOOP-062b: human approvedmerge directly when green + mergeable
11867
+ # (don't wait for repo auto-merge, which may be off).
11868
+ _loop_pr_merge_approved "$num" "$ci_state" "$mergeable" "$slug" || true
11736
11869
  ;;
11737
11870
  stale)
11738
11871
  _loop_pr_rebase_circuit "$num" || true
@@ -15440,12 +15573,13 @@ _notify_update() {
15440
15573
  [[ -f "$cache" ]] || return 0
15441
15574
  local latest; latest=$(awk '{print $2}' "$cache" 2>/dev/null || true)
15442
15575
  [[ -z "$latest" || "$latest" == "$VERSION" ]] && return
15443
- local newer; newer=$(printf '%s\n%s\n' "$VERSION" "$latest" | sort -V | tail -1)
15444
- if [[ "$newer" == "$VERSION" ]]; then
15445
- # Running version is newer than cached stale cache, clear it
15446
- rm -f "$cache"
15447
- return
15448
- fi
15576
+ # FIX-163: the cached `latest` is GitHub's releases/latest the newest
15577
+ # release by created_at, NOT by semver. Under the MAJOR.MMDD scheme a plain
15578
+ # `sort -V` mis-ranks versions across the year-based→MAJOR.MMDD transition
15579
+ # (2026.601.4 > 2.602.1) and the Jan-1 MMDD wrap (2.1231.N > 2.101.1), which
15580
+ # previously (a) reverse-nagged users to "upgrade" to an older release and
15581
+ # (b) silently suppressed real updates after the wrap. Trust GitHub's
15582
+ # chronological latest: if it differs from what's running, surface it.
15449
15583
  echo ""
15450
15584
  warn "$(msg update.available "$latest")"
15451
15585
  }
@@ -152,12 +152,20 @@ def _resolve_name(model: Optional[str],
152
152
  return fallback
153
153
 
154
154
 
155
+ _NO_CURRENCY_MATCH = "\x00__no_currency_match__\x00"
156
+
157
+
155
158
  def currency_for(model: Optional[str]) -> str:
156
159
  """Return the native currency code (USD/CNY) for a model.
157
160
 
158
- Falls back to 'USD' when the model isn't in any snapshot.
161
+ Falls back to 'USD' when the model isn't in any snapshot. FIX-162: resolve
162
+ with a sentinel default so a *genuinely unknown* model returns USD instead
163
+ of inheriting the global DEFAULT model's currency (which is a CNY kimi
164
+ entry — that would mislabel unrelated unknown models as CNY).
159
165
  """
160
- name = _resolve_name(model)
166
+ name = _resolve_name(model, default=_NO_CURRENCY_MATCH)
167
+ if name == _NO_CURRENCY_MATCH:
168
+ return "USD"
161
169
  return _CURRENCY.get(name, "USD")
162
170
 
163
171
 
@@ -3,12 +3,13 @@
3
3
  "effective_at": "2026-05-23",
4
4
  "source_url": "https://platform.kimi.com/docs/pricing/chat",
5
5
  "vendor": "kimi",
6
- "currency": "USD",
6
+ "currency": "CNY",
7
7
  "default_model": "kimi-k2.5",
8
- "notes": "Rates per million tokens (USD). cache_create estimated at 1.25x input. Prices from public Kimi API platform docs — verify with `roll prices refresh` if page layout changes. Model names: kimi-k2 (prior gen), kimi-k2.5 (current), kimi-k2.6 (latest).",
8
+ "notes": "Rates per million tokens (CNY). cache_create estimated at 1.25x input. Prices from public Kimi API platform docs — verify with `roll prices refresh` if page layout changes. Model names: kimi-k2 (prior gen), kimi-k2.5 (current), kimi-k2.6 (latest). kimi-for-coding is the kimi-code CLI's model id (alias of the current K2 line) — FIX-162 so usage events tagged `kimi-code/kimi-for-coding` resolve to a real CNY entry instead of falling back to USD.",
9
9
  "prices": {
10
10
  "kimi-k2": {"in": 1.00, "out": 4.00, "cache_create": 1.25, "cache_read": 0.25},
11
11
  "kimi-k2.5": {"in": 1.00, "out": 4.00, "cache_create": 1.25, "cache_read": 0.25},
12
- "kimi-k2.6": {"in": 1.00, "out": 4.00, "cache_create": 1.25, "cache_read": 0.25}
12
+ "kimi-k2.6": {"in": 1.00, "out": 4.00, "cache_create": 1.25, "cache_read": 0.25},
13
+ "kimi-for-coding": {"in": 1.00, "out": 4.00, "cache_create": 1.25, "cache_read": 0.25}
13
14
  }
14
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "2.602.1",
3
+ "version": "2.602.2",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "scripts": {
6
6
  "test": "bash tests/run.sh"
@@ -227,7 +227,7 @@ User Input
227
227
  │ Approach confirmed
228
228
 
229
229
  ┌─────────────────────────────┐
230
- │ [peer] Direction Review │ ← if complexity=large or cross-context; 10s opt-out
230
+ │ [peer] Direction Review │ ← if complexity=medium/large or cross-context; 10s opt-out
231
231
  │ Skill("roll-peer", │
232
232
  │ tag="architecture") │
233
233
  └─────────────┬───────────────┘
@@ -298,7 +298,7 @@ User Input
298
298
 
299
299
 
300
300
  ┌─────────────────────────────────────────┐
301
- │ [peer] Plan Review │ ← if complexity=large; 10s opt-out
301
+ │ [peer] Plan Review │ ← if complexity=medium/large; 10s opt-out
302
302
  │ Skill("roll-peer", tag="architecture")│
303
303
  └──────────────────┬──────────────────────┘
304
304
  │ AGREE / skipped
@@ -840,13 +840,19 @@ Two checkpoints, both with 10s opt-out:
840
840
  ```
841
841
  1. After Discuss — Direction Review
842
842
  Approach confirmed → [peer, tag=architecture] → challenge the direction before DDD
843
- Trigger: complexity=large OR requirement touches multiple Bounded Contexts
843
+ Trigger: complexity=medium OR complexity=large OR requirement touches multiple Bounded Contexts
844
844
 
845
845
  2. After Solution Design — Plan Review
846
846
  Plan written → [peer, tag=architecture] → full plan review before story split
847
- Trigger: complexity=large (greenfield always qualifies)
847
+ Trigger: complexity=medium OR complexity=large (greenfield always qualifies)
848
848
  ```
849
849
 
850
+ Rationale (US-SKILL-018): medium-complexity designs also routinely carry
851
+ direction/plan risks worth one independent challenge before story split — the
852
+ cost of one bounded peer pass is small next to reworking a misaimed design after
853
+ it ships. So peer now triggers at medium as well as large; the 10s opt-out
854
+ stays, so you can always skip when you're confident.
855
+
850
856
  On AGREE or user skip → continue to the next step normally.
851
857
  On REFINE/OBJECT → incorporate feedback, regenerate the relevant output, re-trigger peer.
852
858
  On ESCALATE → present both proposals to user for final call.
@@ -157,9 +157,10 @@ Call `_loop_pr_inbox` after the pre-run CI check passes. It walks
157
157
 
158
158
  | Classification | Action |
159
159
  |---|---|
160
- | `loop_self` (head ref starts with `loop/`) | Skip — let GitHub auto-merge handle it; never AI-review your own commit |
160
+ | `loop_self` (head ref starts with `loop/`, CI not red) | Skip — let GitHub auto-merge handle it; never AI-review your own commit |
161
+ | `loop_self_ci_red` (loop/* PR whose CI went red) | **US-LOOP-062a**: `_loop_pr_heal_self` — background-heal (per-PR lock + heal budget `ROLL_LOOP_HEAL_MAX`, default 2, via `_project_agent`); on `ROLL_LOOP_NO_HEAL=1` / budget exhausted → deduped `[TYPE:loop-pr-ci-red]` ALERT (never silently dropped) |
161
162
  | `blocked_human_request_changes` | Skip — last human review requested changes; wait for the author to push fixes |
162
- | `blocked_human_approved` | Skiplet GitHub auto-merge after CI is green |
163
+ | `blocked_human_approved` | **US-LOOP-062b**: `_loop_pr_merge_approved` merge directly (`gh pr merge --squash`) when CI green + mergeable, instead of relying on repo auto-merge (which may be off); merge failure is non-fatal (retried next tick) |
163
164
  | `stale` (CI failed or branch behind/conflicting) | Try `_loop_pr_rebase_stale` after the circuit breaker allows it |
164
165
  | `eligible` (clean external PR, no blocking review) | Invoke `_loop_pr_review_external` — the actual decision is provided by US-AUTO-035's GitHub Action |
165
166