@seanyao/roll 2026.519.1 → 2026.519.3
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 +45 -0
- package/README.md +41 -10
- package/bin/roll +178 -42
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/roll-backlog.py +1 -1
- package/lib/roll-help.py +1 -1
- package/lib/roll-home.py +23 -3
- package/lib/roll-init.py +101 -0
- package/lib/roll-loop-status.py +19 -3
- package/package.json +1 -1
- package/skills/roll-build/SKILL.md +2 -2
- package/skills/roll-loop/SKILL.md +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2026.519.3
|
|
4
|
+
|
|
5
|
+
### Major
|
|
6
|
+
|
|
7
|
+
- **`.roll/` 拆为独立私有仓库** — 过程文件(backlog / brief / features / dream / domain / briefs)整体迁入嵌套私有 repo `roll-meta`,主仓只留产品文档。AGENTS.md 新增 §9 说明双 repo 协作约定 `[meta]`
|
|
8
|
+
|
|
9
|
+
### Improved
|
|
10
|
+
|
|
11
|
+
- **`.roll/features/` 按 Epic 分组** — features 目录由扁平改为 Epic 子目录,附带 loop 架构经验文档 `[docs]`
|
|
12
|
+
- **站点登陆页对齐 v2.0 架构** — `docs/site/` 文案与 2.0 实际产物对齐,去掉过期描述 `[docs]`
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **loop 异常退出** — dashboard 不再卡在"运行中",崩溃 / 被 kill / 超时退出都会写下结束标记 `[loop]`
|
|
17
|
+
- **`FIX-065` loop 共享状态隔离** — 测试运行的 loop 不再污染生产 backlog/brief,sandbox 化共享路径 `[loop]`
|
|
18
|
+
|
|
19
|
+
## v2026.519.2
|
|
20
|
+
|
|
21
|
+
### Improved
|
|
22
|
+
|
|
23
|
+
- **`roll init`** — 初始化流程现在显示 6 步编号进度,新建文件用绿色 `+`、合并已有用琥珀 `~`,结尾给三步上手指南 `[loop]`
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`roll --help` 中 `init` 描述** — 文案从 `+ docs/` 改为 `+ .roll/features/`,对齐 2.0 实际产物和 README;v2 (`lib/roll-help.py`) 与 legacy (`bin/roll`) 两份 help 同步修正 `[FIX-064]`
|
|
28
|
+
|
|
3
29
|
## v2026.519.1
|
|
4
30
|
|
|
31
|
+
### Major(大版本重构)
|
|
32
|
+
|
|
33
|
+
- **项目结构重组 — 过程文件迁入 `.roll/`** — Phase 1 of Legacy Onboard Epic。`BACKLOG.md`、`PROPOSALS.md`、`docs/{features,briefs,dream,design,domain}/` 全部搬入 `.roll/`;`docs/guide/`、`docs/site/`、`docs/intro/` 上移到根级。一次性 breaking change,迁移指南见 `guide/{en,zh}/migration-2.0.md` `[legacy-onboard]`
|
|
34
|
+
- **新命令 `roll migrate`** — 老项目一键迁到新结构:dry-run 预览 + `git mv` 保留历史 + 单原子 commit + 三态幂等(仅老 / 仅新 / 并存) `[legacy-onboard]`
|
|
35
|
+
- **新版 `roll init` 识别 Legacy 项目** — 检测到现有源码无 `AGENTS.md` 时引导用户进入 onboard 流程:列出本机 AI agent、显式告知 token 消耗、引导运行 `$roll-onboard` `[legacy-onboard]`
|
|
36
|
+
- **新技能 `$roll-onboard`** — 三组九问 ≤ 3 分钟,AI 读懂项目后生成 `.roll/onboard-plan.yaml`,bash 侧 `roll init --apply` 执行 `[legacy-onboard]`
|
|
37
|
+
- **新命令 `roll init --apply`** — 消费 onboard plan 创建 `.roll/` 结构,按用户选择写 `.gitignore`、同步 AI 工具约定 `[legacy-onboard]`
|
|
38
|
+
- **结构强制检测** — 新版 Roll 在老结构项目上拒绝运行 + 引导 `roll migrate`(`setup` / `update` / `version` / `help` / `init` 豁免;`ROLL_SKIP_STRUCTURE_CHECK=1` 旁路) `[legacy-onboard]`
|
|
39
|
+
|
|
40
|
+
### Improved
|
|
41
|
+
|
|
42
|
+
- `AGENTS.md` §8 Documentation Conventions 重写匹配新目录结构,明确"过程默认对内、产品默认对外"原则 `[docs]`
|
|
43
|
+
- `guide/{en,zh}/practices/` 收入工程规范文档(原 `docs/practices/engineering-common-sense.md`) `[docs]`
|
|
44
|
+
- 新增 Python 校验器 `lib/roll-plan-validate.py`,验证 onboard plan 完整性、`generated_at` 24h 时效、版本兼容 `[legacy-onboard]`
|
|
45
|
+
|
|
5
46
|
### Fixed
|
|
6
47
|
|
|
7
48
|
- **`roll setup` 后从未开启 loop 的项目** — 不再被 macOS 自动激活、每小时弹出终端窗口 `[loop]`
|
|
49
|
+
- **`_write_backlog` 缺 `mkdir -p` 导致 `cmd_init` 在 `.roll/` 不存在时崩** `[legacy-onboard]`
|
|
50
|
+
- **`release.sh` 多 feature 时 awk `newline in string` 错误** — macOS BSD awk 不容忍 `-v var=多行`;改用 `ENVIRON` 读取 `[release]`
|
|
51
|
+
- **GitHub 仓库改名 Roll → roll** — 内部代码、测试 fixture、文档引用全部同步小写命名 `[chore]`
|
|
52
|
+
- **`.roll/backlog.md` 和 `guide/*` 中残留 `docs/features` 等老路径引用** — Story 5 sed 漏覆盖 `.roll/` 和 `guide/` 文件,dream 巡检发现后补齐 `[legacy-onboard]`
|
|
8
53
|
|
|
9
54
|
## v2026.518.4
|
|
10
55
|
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
**[中文版 README](README_CN.md)**
|
|
13
13
|
|
|
14
|
-
[](https://seanyao.github.io/roll/)
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
[](https://www.npmjs.com/package/@seanyao/roll)
|
|
17
17
|
[](https://github.com/seanyao/roll/actions/workflows/ci.yml)
|
|
@@ -28,6 +28,15 @@ Roll is an autonomous delivery system for software teams — AI agents pick stor
|
|
|
28
28
|
|
|
29
29
|
_Works with Claude, Cursor, Codex, or your own agent._
|
|
30
30
|
|
|
31
|
+
## What's New in 2.0 (May 2026)
|
|
32
|
+
|
|
33
|
+
Roll 2.0 introduces a **process/product split** to keep open-source projects clean:
|
|
34
|
+
|
|
35
|
+
- **`.roll/` directory convention** — all process artifacts (backlog, features, briefs, dream logs) move out of root into `.roll/`. User-facing `guide/` and `site/` move up to root. Old `docs/` directory disappears.
|
|
36
|
+
- **`roll migrate`** — one-command migration for projects on pre-2.0 layout. `git mv` preserves history, single atomic commit, three-state idempotent.
|
|
37
|
+
- **`$roll-onboard`** — interactive skill for adopting Roll on legacy codebases without rewrites. AI reads your code, asks 9 questions in ≤ 3 minutes, produces a plan; `roll init --apply` executes it.
|
|
38
|
+
- **Three adoption patterns** — `seed` (new project), `graft` (legacy, non-invasive), `replant` (legacy, clean rebuild). See [guide/en/patterns/](guide/en/patterns/).
|
|
39
|
+
|
|
31
40
|
## Evolution
|
|
32
41
|
|
|
33
42
|
Roll didn't start as a framework. It started as a question: *what if the AI didn't just write code, but actually shipped it?*
|
|
@@ -54,6 +63,20 @@ roll loop on # optional: let the agent work unattended
|
|
|
54
63
|
|
|
55
64
|
---
|
|
56
65
|
|
|
66
|
+
## Adoption Paths
|
|
67
|
+
|
|
68
|
+
Three ways to bring Roll into a project. Not sure which fits? Just run `roll init` — it detects legacy code and routes you to `$roll-onboard` automatically.
|
|
69
|
+
|
|
70
|
+
| Path | When to use | How to start |
|
|
71
|
+
|------|-------------|--------------|
|
|
72
|
+
| **Seed** | Brand-new project, starting from zero | `roll init` (the Quick Start path above) |
|
|
73
|
+
| **Graft** | Existing codebase, keep current workflow intact | `roll init` → AI prompts `$roll-onboard` → 9 questions in ≤ 3 min → `roll init --apply` |
|
|
74
|
+
| **Replant** | Existing codebase, ready to realign on Roll conventions | Same as Graft, choose "clean rebuild" during onboarding |
|
|
75
|
+
|
|
76
|
+
Details: [seed](guide/en/patterns/seed-pattern.md) · [graft](guide/en/patterns/graft-pattern.md) · [replant](guide/en/patterns/replant-pattern.md)
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
57
80
|
## Documentation Index
|
|
58
81
|
|
|
59
82
|
| Topic | English | 中文 |
|
|
@@ -66,7 +89,7 @@ roll loop on # optional: let the agent work unattended
|
|
|
66
89
|
| Configuration (env vars) | [guide/en/configuration.md](guide/en/configuration.md) | [guide/zh/configuration.md](guide/zh/configuration.md) |
|
|
67
90
|
| Skill selection guide | [guide/en/skills.md](guide/en/skills.md) | [guide/zh/skills.md](guide/zh/skills.md) |
|
|
68
91
|
| FAQ (troubleshooting) | [guide/en/faq.md](guide/en/faq.md) | [guide/zh/faq.md](guide/zh/faq.md) |
|
|
69
|
-
|
|
|
92
|
+
| Adoption patterns | [guide/en/patterns/](guide/en/patterns/) | [guide/zh/patterns/](guide/zh/patterns/) |
|
|
70
93
|
| Engineering common sense | [practices/engineering-common-sense.md](guide/en/practices/engineering-common-sense.md) | — |
|
|
71
94
|
|
|
72
95
|
---
|
|
@@ -75,15 +98,23 @@ roll loop on # optional: let the agent work unattended
|
|
|
75
98
|
|
|
76
99
|
| Command | Description |
|
|
77
100
|
|---------|-------------|
|
|
101
|
+
| **Autonomy · daily use** | |
|
|
102
|
+
| `roll loop <on\|off\|now\|status\|monitor>` | 🤖 Manage the autonomous BACKLOG executor |
|
|
103
|
+
| `roll brief` | 🤖 Show latest owner brief |
|
|
104
|
+
| `roll backlog [block\|defer\|…]` | View and manage pending tasks |
|
|
105
|
+
| `roll peer` | 🤖 Cross-agent negotiation & review |
|
|
106
|
+
| `roll alert` | View / clear loop alerts |
|
|
107
|
+
| **Project · per repo** | |
|
|
108
|
+
| `roll init` | Create AGENTS.md + .roll/backlog.md + .roll/features/ |
|
|
109
|
+
| `roll status` | Show current state and drift |
|
|
110
|
+
| `roll agent [use <name>]` | Per-project agent selection (Claude / Cursor / Codex / Kimi / …) |
|
|
111
|
+
| `roll ci [--wait]` | Show or wait for current commit's CI status |
|
|
112
|
+
| `roll release` | 🤖 Run the release script (human-only) |
|
|
113
|
+
| `roll review-pr <number>` | 🤖 AI-powered code review for a PR |
|
|
114
|
+
| **Machine · global** | |
|
|
78
115
|
| `roll setup [-f]` | First-time install or re-sync conventions to all AI clients |
|
|
79
|
-
| `roll update` | Upgrade to latest
|
|
80
|
-
| `roll
|
|
81
|
-
| `roll status` | Show sync state, skill links, detected AI tools |
|
|
82
|
-
| `roll backlog` | Show pending tasks from .roll/backlog.md |
|
|
83
|
-
| `roll loop <on\|off\|now\|status\|monitor>` | 🤖 Manage autonomous executor |
|
|
84
|
-
| `roll brief` | 🤖 Show latest owner digest |
|
|
85
|
-
| `roll peer` | 🤖 Cross-agent code review |
|
|
86
|
-
| `roll release` | 🤖 Version + tag + npm publish + GitHub Release |
|
|
116
|
+
| `roll update` | Upgrade to latest + re-sync |
|
|
117
|
+
| `roll version` | Print installed roll version |
|
|
87
118
|
|
|
88
119
|
---
|
|
89
120
|
|
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="2026.519.
|
|
7
|
+
VERSION="2026.519.3"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -917,6 +917,23 @@ _merge_claude_to_project() {
|
|
|
917
917
|
# Existing AGENTS.md: re-merges global conventions (section-level, non-destructive)
|
|
918
918
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
919
919
|
cmd_init() {
|
|
920
|
+
# US-VIEW-008: parse --demo before any side effects so the v2 renderer can
|
|
921
|
+
# run standalone (no templates, no project mutation).
|
|
922
|
+
local demo=false
|
|
923
|
+
local args=()
|
|
924
|
+
while [[ $# -gt 0 ]]; do
|
|
925
|
+
case "$1" in
|
|
926
|
+
--demo) demo=true; shift ;;
|
|
927
|
+
*) args+=("$1"); shift ;;
|
|
928
|
+
esac
|
|
929
|
+
done
|
|
930
|
+
set -- "${args[@]:-}"
|
|
931
|
+
|
|
932
|
+
if [[ "${ROLL_UI:-v2}" == "v2" ]] || [[ "$demo" == "true" ]]; then
|
|
933
|
+
python3 "${ROLL_PKG_DIR}/lib/roll-init.py" --demo
|
|
934
|
+
if [[ "$demo" == "true" ]]; then return; fi
|
|
935
|
+
fi
|
|
936
|
+
|
|
920
937
|
if [[ ! -d "$ROLL_TEMPLATES" ]]; then
|
|
921
938
|
err "No templates found. Run 'roll setup' first. 未找到模板,请先运行 'roll setup'。"
|
|
922
939
|
exit 1
|
|
@@ -2326,7 +2343,13 @@ cmd_review_pr() {
|
|
|
2326
2343
|
local output
|
|
2327
2344
|
info "Reviewing PR #${pr_number} with ${agent}..."
|
|
2328
2345
|
_agent_argv "$agent" text "$prompt" || { err "Unknown agent '${agent}'"; return 1; }
|
|
2329
|
-
|
|
2346
|
+
local _stderr_log; _stderr_log=$(mktemp)
|
|
2347
|
+
output=$("${_AGENT_ARGV[@]}" 2>"$_stderr_log")
|
|
2348
|
+
if [[ -z "$output" && -s "$_stderr_log" ]]; then
|
|
2349
|
+
err "agent ${agent} produced no output. stderr (first 5 lines):"
|
|
2350
|
+
head -5 "$_stderr_log" | sed 's/^/ /' >&2
|
|
2351
|
+
fi
|
|
2352
|
+
rm -f "$_stderr_log"
|
|
2330
2353
|
|
|
2331
2354
|
echo "$output"
|
|
2332
2355
|
|
|
@@ -2409,6 +2432,13 @@ cmd_agent() {
|
|
|
2409
2432
|
# Returns a filesystem-safe slug combining the project basename and a 6-char
|
|
2410
2433
|
# hash of the full path, ensuring uniqueness across sibling dirs with same name.
|
|
2411
2434
|
_project_slug() {
|
|
2435
|
+
# US-LOOP-006: cycle wrapper exports ROLL_MAIN_SLUG so any subshell — worktree,
|
|
2436
|
+
# tmp cwd, or unrelated path — writes events / runs.jsonl under the main project
|
|
2437
|
+
# identity instead of fragmenting into tmp-* / cycle-* phantom slugs.
|
|
2438
|
+
if [[ -n "${ROLL_MAIN_SLUG:-}" ]]; then
|
|
2439
|
+
printf '%s' "$ROLL_MAIN_SLUG"
|
|
2440
|
+
return 0
|
|
2441
|
+
fi
|
|
2412
2442
|
local path="${1:-$(pwd -P 2>/dev/null || pwd)}"
|
|
2413
2443
|
# FIX-056: normalize path to canonical case on macOS case-insensitive filesystem.
|
|
2414
2444
|
# Two paths differing only in case point to the same directory; realpath
|
|
@@ -2521,6 +2551,35 @@ PYEOF
|
|
|
2521
2551
|
}
|
|
2522
2552
|
|
|
2523
2553
|
_LOOP_TAG="# roll-loop"
|
|
2554
|
+
# FIX-065: when sourced in a test context with no explicit override, route
|
|
2555
|
+
# shared state into a per-process /tmp path instead of falling back to
|
|
2556
|
+
# production ~/.shared/roll/. Without this safety net, tests that source
|
|
2557
|
+
# bin/roll (directly or via a generated inner runner under /var/folders/)
|
|
2558
|
+
# would write ALERT / state / events / runs.jsonl into the live loop
|
|
2559
|
+
# daemon's monitored directory and trigger false aborts.
|
|
2560
|
+
#
|
|
2561
|
+
# Test context is detected via three signals (any one is enough):
|
|
2562
|
+
# 1. BATS_TEST_FILENAME is set (works for direct test invocations)
|
|
2563
|
+
# 2. The caller's file path lives under /tmp or /var/folders (catches the
|
|
2564
|
+
# generated runner-inner.sh path that bats subprocesses spawn —
|
|
2565
|
+
# BATS_* env can be lost across `bash -l` + nested forks)
|
|
2566
|
+
# 3. PWD is under /tmp or /var/folders (catches helpers that cd'd in)
|
|
2567
|
+
if [ -z "${_SHARED_ROOT:-}" ]; then
|
|
2568
|
+
_roll_in_test_ctx=0
|
|
2569
|
+
if [ -n "${BATS_TEST_FILENAME:-}" ]; then
|
|
2570
|
+
_roll_in_test_ctx=1
|
|
2571
|
+
else
|
|
2572
|
+
_roll_caller="${BASH_SOURCE[1]:-}"
|
|
2573
|
+
case "$_roll_caller" in /tmp/*|/private/tmp/*|/var/folders/*) _roll_in_test_ctx=1 ;; esac
|
|
2574
|
+
case "$PWD" in /tmp/*|/private/tmp/*|/var/folders/*) _roll_in_test_ctx=1 ;; esac
|
|
2575
|
+
fi
|
|
2576
|
+
if [ "$_roll_in_test_ctx" = 1 ]; then
|
|
2577
|
+
_SHARED_ROOT="${TMPDIR:-/tmp}/roll-test-shared.$$"
|
|
2578
|
+
mkdir -p "${_SHARED_ROOT}/loop"
|
|
2579
|
+
export _SHARED_ROOT
|
|
2580
|
+
fi
|
|
2581
|
+
unset _roll_in_test_ctx _roll_caller
|
|
2582
|
+
fi
|
|
2524
2583
|
: "${_SHARED_ROOT:=${HOME}/.shared/roll}"
|
|
2525
2584
|
# FIX-052: per-project loop state — ALERT/state/mute were globally shared,
|
|
2526
2585
|
# causing one project's alerts to surface in another project's session and
|
|
@@ -2529,7 +2588,9 @@ _LOOP_TAG="# roll-loop"
|
|
|
2529
2588
|
: "${_LOOP_PROJ_SLUG:=$(_project_slug 2>/dev/null || echo default)}"
|
|
2530
2589
|
: "${_LOOP_STATE:=${_SHARED_ROOT}/loop/state-${_LOOP_PROJ_SLUG}.yaml}"
|
|
2531
2590
|
: "${_LOOP_ALERT:=${_SHARED_ROOT}/loop/ALERT-${_LOOP_PROJ_SLUG}.md}"
|
|
2532
|
-
|
|
2591
|
+
# FIX-065: was hardcoded to ${HOME}/.shared/roll/loop/runs.jsonl which ignored
|
|
2592
|
+
# _SHARED_ROOT overrides and silently leaked test runs.jsonl writes into prod.
|
|
2593
|
+
_LOOP_RUNS="${_SHARED_ROOT}/loop/runs.jsonl"
|
|
2533
2594
|
: "${_LOOP_MUTE_FILE:=${_SHARED_ROOT}/loop/mute-${_LOOP_PROJ_SLUG}}"
|
|
2534
2595
|
_LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
|
2535
2596
|
|
|
@@ -2540,6 +2601,13 @@ _config_read_int() {
|
|
|
2540
2601
|
if [[ "$val" =~ ^[0-9]+$ ]]; then echo "$val"; else echo "$default"; fi
|
|
2541
2602
|
}
|
|
2542
2603
|
|
|
2604
|
+
# REFACTOR-031: cross-platform file mtime in epoch seconds.
|
|
2605
|
+
# Replaces the `stat -c %Y ... || stat -f %m ... || echo 0` pattern that was
|
|
2606
|
+
# copy-pasted in four places (dashboard age widgets, briefs, dream, peer).
|
|
2607
|
+
_file_mtime() {
|
|
2608
|
+
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2543
2611
|
# Derive a minute in [1,55] from project path hash + offset so different projects
|
|
2544
2612
|
# and different services within a project don't fire at the same time.
|
|
2545
2613
|
# Offsets used: loop=0, dream=2, brief=4 → always three distinct values (2<55).
|
|
@@ -2565,6 +2633,24 @@ _loop_event() {
|
|
|
2565
2633
|
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
2566
2634
|
slug=$(_project_slug 2>/dev/null || basename "$PWD")
|
|
2567
2635
|
evfile="${_SHARED_ROOT:-$HOME/.shared/roll}/loop/events-${slug}.ndjson"
|
|
2636
|
+
# FIX-065 tripwire: in a test context (BATS or temp cwd), refuse to write
|
|
2637
|
+
# into production ~/.shared/roll/. Catching this in code is the last line
|
|
2638
|
+
# of defense if some unusual path bypassed the auto-sandbox at source-time.
|
|
2639
|
+
# Skipped when HOME itself has been redirected to a sandbox dir — then
|
|
2640
|
+
# $HOME/.shared/roll IS the sandbox, not prod.
|
|
2641
|
+
case "${HOME:-}" in
|
|
2642
|
+
/tmp/*|/private/tmp/*|*/var/folders/*|*/tmp.*) ;;
|
|
2643
|
+
*)
|
|
2644
|
+
if [ -n "${HOME:-}" ] && [ "${evfile#${HOME}/.shared/roll/}" != "$evfile" ]; then
|
|
2645
|
+
case "${BATS_TEST_FILENAME:-}${PWD}" in
|
|
2646
|
+
*/tmp.*|*/var/folders/*|/tmp/*|/private/tmp/*|*.bats)
|
|
2647
|
+
echo "[FIX-065] refusing prod _loop_event write: $evfile (test context)" >&2
|
|
2648
|
+
return 1
|
|
2649
|
+
;;
|
|
2650
|
+
esac
|
|
2651
|
+
fi
|
|
2652
|
+
;;
|
|
2653
|
+
esac
|
|
2568
2654
|
mkdir -p "$(dirname "$evfile")"
|
|
2569
2655
|
|
|
2570
2656
|
# stdout: tab-separated for tmux display
|
|
@@ -2755,7 +2841,7 @@ fi
|
|
|
2755
2841
|
printf '%s:%s\n' "\$\$" "\$(date -u +%s)" > "\$INNER_LOCK"
|
|
2756
2842
|
# FIX-038: background heartbeat writer — outer script uses this as primary liveness signal
|
|
2757
2843
|
# to detect stale execution without relying on PID reuse heuristics.
|
|
2758
|
-
HEARTBEAT_FILE="
|
|
2844
|
+
HEARTBEAT_FILE="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/.heartbeat-${slug}"
|
|
2759
2845
|
_heartbeat_writer() {
|
|
2760
2846
|
while true; do echo "\$(date -u +%s)" > "\$HEARTBEAT_FILE"; sleep 60; done
|
|
2761
2847
|
}
|
|
@@ -2767,8 +2853,45 @@ _HEARTBEAT_PID=\$!
|
|
|
2767
2853
|
# next cron tick can proceed. Overridable via env for tests.
|
|
2768
2854
|
LOOP_CYCLE_TIMEOUT_SEC="\${ROLL_LOOP_CYCLE_TIMEOUT_SEC:-2700}"
|
|
2769
2855
|
_CYCLE_TIMED_OUT=0
|
|
2856
|
+
# IDEA-028 / FIX-066: track whether cycle_end has been emitted via any of the
|
|
2857
|
+
# explicit completion paths (publish/merge_back/orphan-push/claude-failed).
|
|
2858
|
+
# When zero, the EXIT trap emits a fallback so cycle_start never orphans the
|
|
2859
|
+
# dashboard into a phantom "still running" row.
|
|
2860
|
+
_CYCLE_END_WRITTEN=0
|
|
2770
2861
|
_on_sigterm() { _CYCLE_TIMED_OUT=1; }
|
|
2771
2862
|
trap '_on_sigterm' TERM
|
|
2863
|
+
# US-LOOP-005: idempotent runs.jsonl writer shared by normal exit, timeout
|
|
2864
|
+
# trap, and worktree-setup-failure early exit. Guards on jq + run_id dedupe so
|
|
2865
|
+
# multiple callers in the same cycle are safe.
|
|
2866
|
+
_runs_append() {
|
|
2867
|
+
local _status="\$1"; local _tcr="\${2:-0}"; local _built="\${3:-[]}"
|
|
2868
|
+
local _runs_dst="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/runs.jsonl"
|
|
2869
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
2870
|
+
local _cid="\${CYCLE_ID:-pre-cycle-\$\$}"
|
|
2871
|
+
local _rid="loop-\${_cid%-*}"
|
|
2872
|
+
grep -qF "\"run_id\":\"\$_rid\"" "\$_runs_dst" 2>/dev/null && return 0
|
|
2873
|
+
mkdir -p "\$(dirname "\$_runs_dst")"
|
|
2874
|
+
local _ts_now; _ts_now=\$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
2875
|
+
local _start="\${CYCLE_START:-\$(date -u +%s)}"
|
|
2876
|
+
local _dur=\$(( \$(date -u +%s) - _start ))
|
|
2877
|
+
[ "\$_dur" -lt 0 ] && _dur=0
|
|
2878
|
+
jq -nc \\
|
|
2879
|
+
--arg ts "\$_ts_now" \\
|
|
2880
|
+
--arg project "${slug}" \\
|
|
2881
|
+
--arg run_id "\$_rid" \\
|
|
2882
|
+
--arg status "\$_status" \\
|
|
2883
|
+
--arg cycle_id "\$_cid" \\
|
|
2884
|
+
--argjson built "\$_built" \\
|
|
2885
|
+
--argjson skipped '[]' \\
|
|
2886
|
+
--argjson alerts '[]' \\
|
|
2887
|
+
--argjson tcr_count "\$_tcr" \\
|
|
2888
|
+
--argjson duration_sec "\$_dur" \\
|
|
2889
|
+
'{ts:\$ts, project:\$project, run_id:\$run_id, status:\$status,
|
|
2890
|
+
cycle_id:\$cycle_id,
|
|
2891
|
+
built:\$built, skipped:\$skipped, alerts:\$alerts,
|
|
2892
|
+
tcr_count:\$tcr_count, duration_sec:\$duration_sec}' \\
|
|
2893
|
+
>> "\$_runs_dst" 2>/dev/null || true
|
|
2894
|
+
}
|
|
2772
2895
|
_inner_cleanup() {
|
|
2773
2896
|
local _rc=\$?
|
|
2774
2897
|
# Kill heartbeat + every remaining background job (watchdog, orphan
|
|
@@ -2778,8 +2901,21 @@ _inner_cleanup() {
|
|
|
2778
2901
|
for _pid in \$(jobs -p); do kill "\$_pid" 2>/dev/null; done
|
|
2779
2902
|
if [ "\${_CYCLE_TIMED_OUT:-0}" -eq 1 ]; then
|
|
2780
2903
|
_loop_event cycle_end "\${CYCLE_ID:-unknown}" "\${BRANCH:-}" "blocked" 2>/dev/null || true
|
|
2904
|
+
_CYCLE_END_WRITTEN=1
|
|
2905
|
+
# US-LOOP-005 T9: timeout path must also write runs.jsonl row so dashboard
|
|
2906
|
+
# has a terminal record (cycle_end alone is insufficient — runs.jsonl is
|
|
2907
|
+
# the canonical history feed for 'roll loop runs').
|
|
2908
|
+
_runs_append "failed" 0 "[]" 2>/dev/null || true
|
|
2781
2909
|
_worktree_alert "cycle \${CYCLE_ID:-unknown}: \${LOOP_CYCLE_TIMEOUT_SEC}s timeout — claude/python killed; in-progress story marked Blocked" 2>/dev/null || true
|
|
2782
2910
|
fi
|
|
2911
|
+
# IDEA-028 / FIX-066: catch every other abort (SIGKILL, set -e fire, ALERT
|
|
2912
|
+
# poisoning that bypasses the retry budget, etc.). Without this, cycle_start
|
|
2913
|
+
# is emitted but cycle_end never is, and dashboard renders the cycle as
|
|
2914
|
+
# "still running" until the next successful cycle rolls past it.
|
|
2915
|
+
if [ "\${_CYCLE_END_WRITTEN:-0}" -eq 0 ] && [ -n "\${CYCLE_ID:-}" ]; then
|
|
2916
|
+
_loop_event cycle_end "\${CYCLE_ID}" "\${BRANCH:-}" "aborted" 2>/dev/null || true
|
|
2917
|
+
_runs_append "aborted" 0 "[]" 2>/dev/null || true
|
|
2918
|
+
fi
|
|
2783
2919
|
rm -f "\$INNER_LOCK" "\$HEARTBEAT_FILE"
|
|
2784
2920
|
exit "\$_rc"
|
|
2785
2921
|
}
|
|
@@ -2800,12 +2936,16 @@ _LOOP_PROJ_SLUG="${slug}"
|
|
|
2800
2936
|
_LOOP_ALERT="\${_SHARED_ROOT}/loop/ALERT-${slug}.md"
|
|
2801
2937
|
_LOOP_STATE="\${_SHARED_ROOT}/loop/state-${slug}.yaml"
|
|
2802
2938
|
_LOOP_MUTE_FILE="\${_SHARED_ROOT}/loop/mute-${slug}"
|
|
2939
|
+
# US-LOOP-006: ROLL_MAIN_SLUG is the canonical identity for any subprocess —
|
|
2940
|
+
# claude, loop-fmt.py, _loop_event in arbitrary cwd. _project_slug honors this
|
|
2941
|
+
# env var first, so writes never fragment into tmp-* / cycle-* phantom slugs.
|
|
2942
|
+
export ROLL_MAIN_SLUG="${slug}"
|
|
2803
2943
|
|
|
2804
2944
|
# Pre-claude: try to create a per-cycle isolated worktree on origin/main.
|
|
2805
2945
|
# On any failure (no remote, no main, etc.) fall back to running in the
|
|
2806
2946
|
# project's main tree (degraded — no isolation, like pre-037 behavior).
|
|
2807
|
-
CYCLE_ID="\$(date
|
|
2808
|
-
CYCLE_START=\$(date
|
|
2947
|
+
CYCLE_ID="\$(date +%Y%m%d-%H%M%S)-\$\$"
|
|
2948
|
+
CYCLE_START=\$(date +%s)
|
|
2809
2949
|
WT="\$(_worktree_path "${slug}" "cycle-\${CYCLE_ID}")"
|
|
2810
2950
|
BRANCH="loop/cycle-\${CYCLE_ID}"
|
|
2811
2951
|
_USE_WORKTREE=0
|
|
@@ -2859,6 +2999,10 @@ else
|
|
|
2859
2999
|
# falling back to the main tree without isolation is unacceptable.
|
|
2860
3000
|
_worktree_alert "cycle \${CYCLE_ID}: worktree setup failed — skipping cycle to avoid running without isolation"
|
|
2861
3001
|
echo "[loop] cycle \${CYCLE_ID}: worktree setup failed; skipping cycle (no isolation)"
|
|
3002
|
+
# US-LOOP-005 T10: worktree-setup-failed path leaves no commits and never
|
|
3003
|
+
# emits cycle_start, but dashboard still needs a runs.jsonl row marking the
|
|
3004
|
+
# cycle as failed (otherwise the scheduled tick appears to have vanished).
|
|
3005
|
+
_runs_append "failed" 0 "[]" 2>/dev/null || true
|
|
2862
3006
|
exit 0
|
|
2863
3007
|
fi
|
|
2864
3008
|
|
|
@@ -2955,11 +3099,13 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2955
3099
|
fi
|
|
2956
3100
|
fi
|
|
2957
3101
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
2958
|
-
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true
|
|
3102
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true; _CYCLE_END_WRITTEN=1
|
|
2959
3103
|
echo "[loop] cycle \${CYCLE_ID}: published; worktree cleaned"
|
|
2960
3104
|
elif [ "\$_publish_status" -eq 2 ]; then
|
|
2961
3105
|
if ( cd "${project_path}" && _worktree_merge_back "\$BRANCH" ); then
|
|
2962
3106
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
3107
|
+
# US-LOOP-005 T3: gh unavailable + ff merge_back OK → cycle_end done
|
|
3108
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "done" || true; _CYCLE_END_WRITTEN=1
|
|
2963
3109
|
echo "[loop] cycle \${CYCLE_ID}: gh unavailable; merged via ff and cleaned up"
|
|
2964
3110
|
else
|
|
2965
3111
|
# FIX-039: gh unavailable + merge_back failed — push orphan branch+tag to origin
|
|
@@ -2969,9 +3115,13 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2969
3115
|
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
2970
3116
|
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
2971
3117
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
3118
|
+
# US-LOOP-005 T4: gh unavailable + orphan push OK → cycle_end orphan
|
|
3119
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true; _CYCLE_END_WRITTEN=1
|
|
2972
3120
|
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
2973
3121
|
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
2974
3122
|
else
|
|
3123
|
+
# US-LOOP-005 T5: gh unavailable + all failed → cycle_end failed
|
|
3124
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
|
|
2975
3125
|
_worktree_alert "cycle \${CYCLE_ID}: gh+merge_back+push all failed; worktree preserved at \$WT"
|
|
2976
3126
|
echo "[loop] cycle \${CYCLE_ID}: all publish paths failed; worktree preserved at \$WT"
|
|
2977
3127
|
fi
|
|
@@ -2984,15 +3134,21 @@ if [ "\$_USE_WORKTREE" = "1" ]; then
|
|
|
2984
3134
|
&& git tag "\$_orphan_tag" 2>/dev/null \
|
|
2985
3135
|
&& git push origin "\$_orphan_tag" 2>/dev/null ); then
|
|
2986
3136
|
_worktree_cleanup "\$WT" "\$BRANCH"
|
|
3137
|
+
# US-LOOP-005 T6: PR publish failed + orphan push OK → cycle_end orphan
|
|
3138
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "orphan" || true; _CYCLE_END_WRITTEN=1
|
|
2987
3139
|
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; FIX-039 pushed orphan+tag \${_orphan_tag}; worktree cleaned"
|
|
2988
3140
|
echo "[loop] cycle \${CYCLE_ID}: FIX-039: orphan branch+tag \${_orphan_tag} pushed; worktree cleaned"
|
|
2989
3141
|
else
|
|
3142
|
+
# US-LOOP-005 T7: PR publish failed + orphan push failed → cycle_end failed
|
|
3143
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
|
|
2990
3144
|
_worktree_alert "cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2991
3145
|
echo "[loop] cycle \${CYCLE_ID}: PR publish failed; worktree preserved at \$WT"
|
|
2992
3146
|
fi
|
|
2993
3147
|
fi
|
|
2994
3148
|
fi
|
|
2995
3149
|
else
|
|
3150
|
+
# US-LOOP-005 T8: claude session failed after retry budget → cycle_end failed
|
|
3151
|
+
_loop_event cycle_end "\${CYCLE_ID}" "" "failed" || true; _CYCLE_END_WRITTEN=1
|
|
2996
3152
|
_worktree_alert "cycle \${CYCLE_ID}: claude exited \$_exit; worktree preserved at \$WT (branch \$BRANCH)"
|
|
2997
3153
|
echo "[loop] cycle \${CYCLE_ID}: claude failed (exit \$_exit); worktree preserved at \$WT"
|
|
2998
3154
|
fi
|
|
@@ -3003,31 +3159,9 @@ _loop_cleanup_stale_cycle_branches "${project_path}" || true
|
|
|
3003
3159
|
|
|
3004
3160
|
# FIX-044 / Step 5: Write loop cycle run summary to runs.jsonl
|
|
3005
3161
|
# Deterministic — runs in shell regardless of whether agent executes SKILL.md Step 5.
|
|
3006
|
-
#
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
_cycle_end=\$(date -u +%s)
|
|
3010
|
-
_cycle_dur=\$(( _cycle_end - CYCLE_START ))
|
|
3011
|
-
_ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
3012
|
-
_run_id="loop-\${CYCLE_ID%-*}"
|
|
3013
|
-
if command -v jq >/dev/null 2>&1 && ! grep -qF "\"run_id\":\"\$_run_id\"" "\$_runs_dst" 2>/dev/null; then
|
|
3014
|
-
jq -nc \\
|
|
3015
|
-
--arg ts "\$_ts" \\
|
|
3016
|
-
--arg project "${slug}" \\
|
|
3017
|
-
--arg run_id "\$_run_id" \\
|
|
3018
|
-
--arg status "\$_cycle_status" \\
|
|
3019
|
-
--arg cycle_id "\$CYCLE_ID" \\
|
|
3020
|
-
--argjson built "\$_cycle_built" \\
|
|
3021
|
-
--argjson skipped '[]' \\
|
|
3022
|
-
--argjson alerts '[]' \\
|
|
3023
|
-
--argjson tcr_count "\$_cycle_tcr" \\
|
|
3024
|
-
--argjson duration_sec "\$_cycle_dur" \\
|
|
3025
|
-
'{ts:\$ts, project:\$project, run_id:\$run_id, status:\$status,
|
|
3026
|
-
cycle_id:\$cycle_id,
|
|
3027
|
-
built:\$built, skipped:\$skipped, alerts:\$alerts,
|
|
3028
|
-
tcr_count:\$tcr_count, duration_sec:\$duration_sec}' \\
|
|
3029
|
-
>> "\$_runs_dst" 2>/dev/null || true
|
|
3030
|
-
fi
|
|
3162
|
+
# US-LOOP-005: now routed through _runs_append so timeout/worktree-setup-fail
|
|
3163
|
+
# share the same write logic. _runs_append is idempotent on run_id.
|
|
3164
|
+
_runs_append "\$_cycle_status" "\$_cycle_tcr" "\$_cycle_built" 2>/dev/null || true
|
|
3031
3165
|
INNER
|
|
3032
3166
|
chmod +x "$inner_path"
|
|
3033
3167
|
|
|
@@ -3059,13 +3193,13 @@ if [ -z "\$ROLL_LOOP_FORCE" ] && [ -f "\$PAUSE" ]; then exit 0; fi
|
|
|
3059
3193
|
HEARTBEAT_TIMEOUT="\${ROLL_HEARTBEAT_TIMEOUT:-1800}"
|
|
3060
3194
|
# FIX-052: per-project STATE_FILE (was global state.yaml — caused two projects
|
|
3061
3195
|
# to clobber each other's cycle state).
|
|
3062
|
-
STATE_FILE="
|
|
3196
|
+
STATE_FILE="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/state-${slug}.yaml"
|
|
3063
3197
|
if [ -f "\$STATE_FILE" ]; then
|
|
3064
3198
|
_state=\$(grep '^status:' "\$STATE_FILE" | awk '{print \$2}' 2>/dev/null || echo "")
|
|
3065
3199
|
if [ "\$_state" = "running" ]; then
|
|
3066
3200
|
_still_active=false
|
|
3067
3201
|
# FIX-038: heartbeat is primary signal
|
|
3068
|
-
_heartbeat_file="
|
|
3202
|
+
_heartbeat_file="\${_SHARED_ROOT:-\${HOME}/.shared/roll}/loop/.heartbeat-${slug}"
|
|
3069
3203
|
if [ -f "\$_heartbeat_file" ]; then
|
|
3070
3204
|
_hb_ts=\$(cat "\$_heartbeat_file" 2>/dev/null || echo "0")
|
|
3071
3205
|
_now=\$(date -u +%s)
|
|
@@ -4543,7 +4677,7 @@ _changelog_lint_bullet() {
|
|
|
4543
4677
|
if [ "${len:-0}" -gt 50 ]; then
|
|
4544
4678
|
echo "over-length"
|
|
4545
4679
|
fi
|
|
4546
|
-
if printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(docs|bin|tests|scripts)/'; then
|
|
4680
|
+
if printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(\.roll|docs|bin|tests|scripts)/'; then
|
|
4547
4681
|
echo "path-fragment"
|
|
4548
4682
|
fi
|
|
4549
4683
|
return 0
|
|
@@ -4600,7 +4734,7 @@ _changelog_audit_bullet() {
|
|
|
4600
4734
|
|
|
4601
4735
|
# Rule 3: file suffix / path fragment outside backticks.
|
|
4602
4736
|
if printf '%s' "$stripped" | grep -qE '\.(md|sh|yml|ts|bats)([^A-Za-z0-9]|$)' \
|
|
4603
|
-
|| printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(docs|bin|tests|scripts)/'; then
|
|
4737
|
+
|| printf '%s' "$stripped" | grep -qE '(^|[^A-Za-z0-9_])(\.roll|docs|bin|tests|scripts)/'; then
|
|
4604
4738
|
echo "path-or-suffix"
|
|
4605
4739
|
fi
|
|
4606
4740
|
|
|
@@ -4936,7 +5070,9 @@ _loop_is_doc_only_change() {
|
|
|
4936
5070
|
local changed
|
|
4937
5071
|
changed=$(git diff --name-only origin/main HEAD 2>/dev/null) || return 1
|
|
4938
5072
|
[ -z "$changed" ] && return 1
|
|
4939
|
-
|
|
5073
|
+
# Post-Phase-1: process artifacts moved into .roll/; user-facing docs at guide/ + site/.
|
|
5074
|
+
# Legacy paths (BACKLOG.md, PROPOSALS.md, docs/) kept as fallback for pre-2.0 projects.
|
|
5075
|
+
echo "$changed" | grep -qvE '^(\.roll/|CHANGELOG\.md|guide/|site/|\.claude/|BACKLOG\.md|PROPOSALS\.md|docs/)' && return 1
|
|
4940
5076
|
return 0
|
|
4941
5077
|
}
|
|
4942
5078
|
|
|
@@ -5229,7 +5365,7 @@ cmd_brief() {
|
|
|
5229
5365
|
latest=$(ls "${briefs_dir}"/*.md 2>/dev/null | sort | tail -1 || true)
|
|
5230
5366
|
else
|
|
5231
5367
|
local mod_time now age
|
|
5232
|
-
mod_time=$(
|
|
5368
|
+
mod_time=$(_file_mtime "$latest")
|
|
5233
5369
|
now=$(date +%s); age=$(( now - mod_time ))
|
|
5234
5370
|
if (( age > 86400 )); then
|
|
5235
5371
|
info "Brief is $(( age / 3600 ))h old — regenerating... 简报已 $(( age / 3600 )) 小时未更新,重新生成..."
|
|
@@ -5606,7 +5742,7 @@ _dash_last_dream_hours() {
|
|
|
5606
5742
|
local dream_log="${HOME}/.shared/roll/dream/log.md"
|
|
5607
5743
|
[[ -f "$dream_log" ]] || return 0
|
|
5608
5744
|
local mod_time now
|
|
5609
|
-
mod_time=$(
|
|
5745
|
+
mod_time=$(_file_mtime "$dream_log")
|
|
5610
5746
|
now=$(date +%s)
|
|
5611
5747
|
echo $(( (now - mod_time) / 3600 ))
|
|
5612
5748
|
}
|
|
@@ -5626,7 +5762,7 @@ _dash_last_peer() {
|
|
|
5626
5762
|
local result
|
|
5627
5763
|
result=$(grep -oE '(AGREE|REFINE|OBJECT|ESCALATE)' "$latest" 2>/dev/null | tail -1 || true)
|
|
5628
5764
|
local mod_time now days
|
|
5629
|
-
mod_time=$(
|
|
5765
|
+
mod_time=$(_file_mtime "$latest")
|
|
5630
5766
|
now=$(date +%s)
|
|
5631
5767
|
days=$(( (now - mod_time) / 86400 ))
|
|
5632
5768
|
printf '%s|%s' "${result:-—}" "${days}"
|
|
@@ -5919,7 +6055,7 @@ _legacy_home() {
|
|
|
5919
6055
|
local latest_brief; latest_brief=$(ls .roll/briefs/*.md 2>/dev/null | sort | tail -1 || true)
|
|
5920
6056
|
if [[ -n "$latest_brief" ]]; then
|
|
5921
6057
|
local mod_time now age summary
|
|
5922
|
-
mod_time=$(
|
|
6058
|
+
mod_time=$(_file_mtime "$latest_brief")
|
|
5923
6059
|
now=$(date +%s); age=$(( (now - mod_time) / 3600 ))
|
|
5924
6060
|
summary=$(_dash_brief_summary "$latest_brief")
|
|
5925
6061
|
printf " Brief ${CYAN}%sh${NC} ago — %s\n" "$age" "${summary:-—}"
|
|
@@ -5956,7 +6092,7 @@ _legacy_help() {
|
|
|
5956
6092
|
echo "Commands:"
|
|
5957
6093
|
echo " setup [-f] [Machine] First-time install or re-sync 首次安装或重新同步"
|
|
5958
6094
|
echo " update [Upgrade] npm install latest + re-sync 一键升级到最新版"
|
|
5959
|
-
echo " init [Project] Create AGENTS.md + .roll/backlog.md +
|
|
6095
|
+
echo " init [Project] Create AGENTS.md + .roll/backlog.md + .roll/features/ 初始化项目工作流文件"
|
|
5960
6096
|
echo " status [Diagnostic] Show current state 显示当前状态"
|
|
5961
6097
|
echo " peer [Peer Review] Cross-agent negotiation 跨 Agent 协商对审"
|
|
5962
6098
|
echo " loop <on|off|now|status|monitor|resume|reset> [Autonomous] Manage scheduled BACKLOG executor 管理自主执行循环"
|
|
Binary file
|
package/lib/roll-backlog.py
CHANGED
|
@@ -244,7 +244,7 @@ def _write_demo(path: str) -> None:
|
|
|
244
244
|
### Feature: autonomous-evolution
|
|
245
245
|
| Story | Description | Status |
|
|
246
246
|
|-------|-------------|--------|
|
|
247
|
-
| [US-AUTO-042](.roll/features/autonomous-evolution.md) | Loop cost telemetry — write model and token data per cycle | 📋 Todo |
|
|
247
|
+
| [US-AUTO-042](.roll/features/autonomous-evolution/autonomous-evolution.md) | Loop cost telemetry — write model and token data per cycle | 📋 Todo |
|
|
248
248
|
|
|
249
249
|
## ♻️ Refactor
|
|
250
250
|
| ID | Description | Status |
|
package/lib/roll-help.py
CHANGED
|
@@ -45,7 +45,7 @@ AUTONOMY = [
|
|
|
45
45
|
]
|
|
46
46
|
|
|
47
47
|
PROJECT = [
|
|
48
|
-
("init", "", "create AGENTS.md + .roll/backlog.md +
|
|
48
|
+
("init", "", "create AGENTS.md + .roll/backlog.md + .roll/features/", "初始化项目工作流文件", False),
|
|
49
49
|
("status", "", "show current state and drift", "显示当前状态和漂移项", False),
|
|
50
50
|
("agent", "[use <name>]", "per-project agent selection", "切换项目 agent", False),
|
|
51
51
|
("ci", "[--wait]", "show or wait for current commit's CI status", "查看 / 等待 CI 状态", False),
|
package/lib/roll-home.py
CHANGED
|
@@ -116,6 +116,26 @@ def _launchd_svc_state(service: str, slug: str) -> str:
|
|
|
116
116
|
except Exception:
|
|
117
117
|
return "installed-off"
|
|
118
118
|
|
|
119
|
+
def _read_plist_schedule(service: str, slug: str) -> Optional[Dict[str, int]]:
|
|
120
|
+
"""FIX-063: read actual Minute/Hour from launchd plist (truth source).
|
|
121
|
+
Returns {'minute': N, 'hour': N|None} or None if plist missing.
|
|
122
|
+
Dashboard must reflect what launchd actually fires, not a hardcoded default.
|
|
123
|
+
"""
|
|
124
|
+
label = f"com.roll.{service}.{slug}"
|
|
125
|
+
plist = Path(os.path.expanduser("~/Library/LaunchAgents")) / f"{label}.plist"
|
|
126
|
+
if not plist.exists():
|
|
127
|
+
return None
|
|
128
|
+
try:
|
|
129
|
+
text = plist.read_text(errors="ignore")
|
|
130
|
+
except Exception:
|
|
131
|
+
return None
|
|
132
|
+
# Parse <key>Minute</key><integer>N</integer> (and Hour)
|
|
133
|
+
m = re.search(r"<key>Minute</key>\s*<integer>(\d+)</integer>", text)
|
|
134
|
+
h = re.search(r"<key>Hour</key>\s*<integer>(\d+)</integer>", text)
|
|
135
|
+
if not m:
|
|
136
|
+
return None
|
|
137
|
+
return {"minute": int(m.group(1)), "hour": int(h.group(1)) if h else None}
|
|
138
|
+
|
|
119
139
|
def _dream_last_hours() -> Optional[int]:
|
|
120
140
|
log = _shared_root() / "dream" / "log.md"
|
|
121
141
|
if not log.exists():
|
|
@@ -469,12 +489,12 @@ def main() -> None:
|
|
|
469
489
|
timestamp = datetime.now().strftime("%H:%M"),
|
|
470
490
|
state = state,
|
|
471
491
|
loop_state = _launchd_svc_state("loop", slug),
|
|
472
|
-
loop_minute = _ci("loop_minute", 38),
|
|
492
|
+
loop_minute = (_read_plist_schedule("loop", slug) or {}).get("minute") or _ci("loop_minute", 38),
|
|
473
493
|
loop_active_start = _ci("loop_active_start", 10),
|
|
474
494
|
loop_active_end = _ci("loop_active_end", 18),
|
|
475
495
|
dream_state = _launchd_svc_state("dream", slug),
|
|
476
|
-
dream_hour = _ci("loop_dream_hour", 3),
|
|
477
|
-
dream_minute = _ci("loop_dream_minute", 12),
|
|
496
|
+
dream_hour = (_read_plist_schedule("dream", slug) or {}).get("hour") or _ci("loop_dream_hour", 3),
|
|
497
|
+
dream_minute = (_read_plist_schedule("dream", slug) or {}).get("minute") or _ci("loop_dream_minute", 12),
|
|
478
498
|
dream_last_hours = _dream_last_hours(),
|
|
479
499
|
refactor_pending = refactor_pending,
|
|
480
500
|
peer_last = _peer_last(),
|
package/lib/roll-init.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""roll-init — v2 terminal view for `roll init` (US-VIEW-008)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
_LIB_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
10
|
+
if _LIB_DIR not in sys.path:
|
|
11
|
+
sys.path.insert(0, _LIB_DIR)
|
|
12
|
+
import roll_render
|
|
13
|
+
from roll_render import c, row, COLS
|
|
14
|
+
|
|
15
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
# Demo data — 6 steps mirror cmd_init's actual phases.
|
|
17
|
+
# AC text: detect → AGENTS.md → BACKLOG.md → docs/features/ → merge CLAUDE.md → link skills
|
|
18
|
+
# Each entry: (label, [(op, filename)]) where op ∈ {"+", "~"}.
|
|
19
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
_DEMO_STEPS = [
|
|
22
|
+
("Detect project type", []),
|
|
23
|
+
("Create AGENTS.md", [("+", "AGENTS.md")]),
|
|
24
|
+
("Create .roll/backlog.md", [("+", ".roll/backlog.md")]),
|
|
25
|
+
("Create .roll/features/", [("+", ".roll/features/")]),
|
|
26
|
+
("Merge existing CLAUDE.md", [("~", "CLAUDE.md")]),
|
|
27
|
+
("Link skills to AI clients", [("+", "~/.claude/skills/roll-build"),
|
|
28
|
+
("+", "~/.claude/skills/roll-fix")]),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_NEXT_STEPS = [
|
|
32
|
+
("Edit .roll/backlog.md", "open the backlog and add your first US"),
|
|
33
|
+
("Run roll loop now", "execute one cycle manually to test the flow"),
|
|
34
|
+
("Enable loop scheduling", "roll loop on — let it run hourly"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
39
|
+
# Render
|
|
40
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
def _divider(char: str = "─") -> None:
|
|
43
|
+
print(c("dim", char * min(COLS, 80)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _op_marker(op: str) -> str:
|
|
47
|
+
if op == "+":
|
|
48
|
+
return c("green", "+", bold=True)
|
|
49
|
+
if op == "~":
|
|
50
|
+
return c("amber", "~", bold=True)
|
|
51
|
+
return c("dim", op)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def render_demo(project_path: str = "~/myproject") -> None:
|
|
55
|
+
left = " " + c("blue", "INIT", bold=True) + c("dim", " · ") + c("dim", "项目初始化")
|
|
56
|
+
right = c("dim", project_path) + " "
|
|
57
|
+
print(row(left, right))
|
|
58
|
+
_divider()
|
|
59
|
+
print()
|
|
60
|
+
|
|
61
|
+
for i, (label, files) in enumerate(_DEMO_STEPS, start=1):
|
|
62
|
+
num = c("dim", f" {i}.")
|
|
63
|
+
icon = c("green", "✓")
|
|
64
|
+
print(f"{num} {icon} {label}")
|
|
65
|
+
for op, fname in files:
|
|
66
|
+
mark = _op_marker(op)
|
|
67
|
+
color = "green" if op == "+" else "amber"
|
|
68
|
+
print(" " + mark + " " + c(color, fname))
|
|
69
|
+
|
|
70
|
+
print()
|
|
71
|
+
_divider()
|
|
72
|
+
print(" " + c("green", "✓") + " " + c("green", "Project ready", bold=True))
|
|
73
|
+
print()
|
|
74
|
+
print(" " + c("pink", "NEXT", bold=True) + c("dim", " · 下一步"))
|
|
75
|
+
for i, (label, hint) in enumerate(_NEXT_STEPS, start=1):
|
|
76
|
+
num = c("dim", f" {i}.")
|
|
77
|
+
print(f"{num} {c('fg', label, bold=True)}")
|
|
78
|
+
print(" " + c("dim", hint))
|
|
79
|
+
_divider("═")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
83
|
+
# Entry point
|
|
84
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
85
|
+
|
|
86
|
+
def main() -> None:
|
|
87
|
+
ap = argparse.ArgumentParser(add_help=False)
|
|
88
|
+
ap.add_argument("--demo", action="store_true")
|
|
89
|
+
ap.add_argument("--no-color", dest="no_color", action="store_true")
|
|
90
|
+
ap.add_argument("--en", action="store_true")
|
|
91
|
+
ap.add_argument("--zh", action="store_true")
|
|
92
|
+
args, _ = ap.parse_known_args()
|
|
93
|
+
|
|
94
|
+
if args.no_color or os.environ.get("NO_COLOR") or not sys.stdout.isatty():
|
|
95
|
+
roll_render.USE_COLOR = False
|
|
96
|
+
|
|
97
|
+
render_demo(project_path=os.getcwd())
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
main()
|
package/lib/roll-loop-status.py
CHANGED
|
@@ -701,11 +701,27 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
|
|
|
701
701
|
c("muted", " ") +
|
|
702
702
|
c("dim", "more ") + c("blue", "roll loop --days 7"))
|
|
703
703
|
|
|
704
|
+
def _read_plist_loop_minute() -> int:
|
|
705
|
+
"""FIX-063: read actual loop Minute from launchd plist (truth source).
|
|
706
|
+
Falls back to 48 only when plist missing/unparseable.
|
|
707
|
+
"""
|
|
708
|
+
import re as _re
|
|
709
|
+
slug = project_slug()
|
|
710
|
+
plist = Path(os.path.expanduser("~/Library/LaunchAgents")) / f"com.roll.loop.{slug}.plist"
|
|
711
|
+
if not plist.exists():
|
|
712
|
+
return 48
|
|
713
|
+
try:
|
|
714
|
+
text = plist.read_text(errors="ignore")
|
|
715
|
+
except Exception:
|
|
716
|
+
return 48
|
|
717
|
+
m = _re.search(r"<key>Minute</key>\s*<integer>(\d+)</integer>", text)
|
|
718
|
+
return int(m.group(1)) if m else 48
|
|
719
|
+
|
|
720
|
+
|
|
704
721
|
def _next_cron_hint(state: Dict[str, str], zh: bool = False) -> str:
|
|
705
|
-
"""
|
|
706
|
-
we only have access to last_run here, so we approximate to the next :48."""
|
|
722
|
+
"""Compute next cron fire time from the actual launchd plist Minute (FIX-063)."""
|
|
707
723
|
now = datetime.now().astimezone()
|
|
708
|
-
minute_target =
|
|
724
|
+
minute_target = _read_plist_loop_minute()
|
|
709
725
|
nxt = now.replace(minute=minute_target, second=0, microsecond=0)
|
|
710
726
|
if nxt <= now:
|
|
711
727
|
nxt += timedelta(hours=1)
|
package/package.json
CHANGED
|
@@ -278,7 +278,7 @@ When any signal appears, **do not stop — flag it**:
|
|
|
278
278
|
# 1. Append to .roll/backlog.md under ## ♻️ Refactor
|
|
279
279
|
# REFACTOR-XXX | <one-line description> | 📋 Todo
|
|
280
280
|
|
|
281
|
-
# 2. Append a brief entry to .roll/features/refactor-log.md
|
|
281
|
+
# 2. Append a brief entry to .roll/features/autonomous-evolution/refactor-log.md
|
|
282
282
|
```
|
|
283
283
|
|
|
284
284
|
**REFACTOR entry format in .roll/backlog.md:**
|
|
@@ -287,7 +287,7 @@ When any signal appears, **do not stop — flag it**:
|
|
|
287
287
|
| REFACTOR-001 | {one-line plain-language description} | 📋 Todo |
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
描述写法:参见 AGENTS.md "Backlog descriptions" 规则。说清楚"什么需要改"以及"不改会怎样",技术细节写在 `.roll/features/refactor-log.md`。
|
|
290
|
+
描述写法:参见 AGENTS.md "Backlog descriptions" 规则。说清楚"什么需要改"以及"不改会怎样",技术细节写在 `.roll/features/autonomous-evolution/refactor-log.md`。
|
|
291
291
|
|
|
292
292
|
**refactor-log.md entry format:**
|
|
293
293
|
|
|
@@ -208,11 +208,14 @@ For each item, **before invoking the executor skill**, mark the story 🔨 In Pr
|
|
|
208
208
|
|
|
209
209
|
This commit is what makes the work visible — without it, tcr micro-commits during execution are invisible to `roll-brief`.
|
|
210
210
|
|
|
211
|
-
选定故事后,调用 `_loop_event` 发出
|
|
211
|
+
选定故事后,调用 `_loop_event` 发出 pick_todo 事件,让 dashboard / monitor / attach 都能把"这个 cycle 选了哪个 story"正确归类:
|
|
212
212
|
|
|
213
213
|
```bash
|
|
214
214
|
# 选定故事后立即 emit(在调用 executor skill 之前)
|
|
215
|
-
|
|
215
|
+
# label 必须是 cycle_id(来自 bin/roll 注入的 LOOP_CYCLE_ID 环境变量),
|
|
216
|
+
# 不是 US_ID — dashboard 按 label 聚类,US_ID 当 label 会让事件分到错的桶
|
|
217
|
+
# 里,cycle 看起来"有 token 没 ID"。
|
|
218
|
+
_loop_event pick_todo "$LOOP_CYCLE_ID" "$US_ID" ""
|
|
216
219
|
```
|
|
217
220
|
|
|
218
221
|
Then invoke the executor:
|