@seanyao/roll 3.617.1 → 3.617.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 +17 -0
- package/dist/roll.mjs +480 -67
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v3.617.2 — 2026-06-17
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- **各类 agent 的展示、用量和 smoke 行为改走统一注册表**:模型名、实时观察窗、usage recovery、dashboard 回填、pairing 成本和 loop smoke 不再散落写 claude/pi/kimi/codex 特判;新增 agent 只需补 AgentSpec,非 claude 不再显示成 `?` 或被 mock smoke 降级。(FIX-313) `[loop]`
|
|
10
|
+
<!-- evidence: .roll/features/loop-engine/FIX-313/latest/FIX-313-report.html -->
|
|
11
|
+
|
|
12
|
+
- **Loop 页的"已合并显示"这次真生效了**:上一版加的"PR 合了 → 显示绿色"其实是哑的——一个缓冲区上限太小的 bug 让它读不到 git 记录、静默失效;现在已合并的交付会正确显示为已交付(绿),且只按这个循环自己开的 PR 判定,不会被别的 PR 误判成绿。(FIX-349, FIX-348, FIX-350) `[loop-observability]`
|
|
13
|
+
|
|
14
|
+
- **重复的故事编号当场拦住**:同一个编号被两个地方使用时,不再静默读到错的卡;新增一个 CI 检查,扫到重复编号就报错。(FIX-340) `[loop-engine]`
|
|
15
|
+
|
|
16
|
+
- **发布没成功的循环不再误标"失败"**:循环把活干完、过了质量闸,但因为临时的 GitHub 故障没能开 PR 时,现在显示"未发布"(中性灰),不再误显示成"失败"(红);`roll loop run-once --help` 也改为显示帮助,不再误跑一个循环。(FIX-351) `[loop-engine]`
|
|
17
|
+
|
|
18
|
+
### 性能(可选,默认关闭)
|
|
19
|
+
|
|
20
|
+
- **两个 execute 提速开关(默认关,需手动开启)**:`prebuild_dist`(循环工作区一建好就预构建,省 agent 找入口的往返)和 `project_map`(spawn 时给 agent 注入精简的仓库结构 + 本卡相关文件,省它摸索)。对所有 agent 通用、不破循环隔离;不开就完全没影响。(FIX-338) `[loop-engine]`
|
|
21
|
+
|
|
5
22
|
## v3.617.1 — 2026-06-17
|
|
6
23
|
|
|
7
24
|
### 质量与可信
|
package/dist/roll.mjs
CHANGED
|
@@ -2382,6 +2382,14 @@ var init_terminal = __esm({
|
|
|
2382
2382
|
"orphan_timeout",
|
|
2383
2383
|
"idle_no_work",
|
|
2384
2384
|
"gave_up",
|
|
2385
|
+
// FIX-351 — a cycle that reached the end with its gates PASSED (attest
|
|
2386
|
+
// produced + peer ok/consulted, real TCR commits, exit 0 → a `built` capture)
|
|
2387
|
+
// but whose publish could NOT complete (push / `gh pr create` failed before
|
|
2388
|
+
// any orphan branch was pushed). The WORK is sound and locally committed —
|
|
2389
|
+
// this is NOT a gate failure. A neutral "ran locally, never published" state,
|
|
2390
|
+
// distinct from `failed` (a gate genuinely failed / errored) and from
|
|
2391
|
+
// `aborted_with_delivery` (publish failed but the branch WAS pushed for audit).
|
|
2392
|
+
"unpublished",
|
|
2385
2393
|
"unknown"
|
|
2386
2394
|
];
|
|
2387
2395
|
}
|
|
@@ -3076,6 +3084,131 @@ var init_registry = __esm({
|
|
|
3076
3084
|
}
|
|
3077
3085
|
});
|
|
3078
3086
|
|
|
3087
|
+
// packages/core/dist/agent/specs.js
|
|
3088
|
+
function canonicalSpecKey(name) {
|
|
3089
|
+
const raw = name.trim().toLowerCase();
|
|
3090
|
+
if (raw === "antigravity" || raw === "gemini")
|
|
3091
|
+
return "agy";
|
|
3092
|
+
if (raw === "openai")
|
|
3093
|
+
return "codex";
|
|
3094
|
+
if (raw === "deepseek")
|
|
3095
|
+
return "pi";
|
|
3096
|
+
return raw;
|
|
3097
|
+
}
|
|
3098
|
+
function withAgentSpecs(extra = []) {
|
|
3099
|
+
const out3 = {};
|
|
3100
|
+
for (const spec of [...BASE_AGENT_SPECS, ...extra]) {
|
|
3101
|
+
out3[canonicalSpecKey(spec.name)] = spec;
|
|
3102
|
+
for (const alias of spec.aliases ?? [])
|
|
3103
|
+
out3[canonicalSpecKey(alias)] = spec;
|
|
3104
|
+
}
|
|
3105
|
+
return out3;
|
|
3106
|
+
}
|
|
3107
|
+
function getAgentSpec(name, registry2 = AGENT_SPECS) {
|
|
3108
|
+
return registry2[canonicalSpecKey(name)];
|
|
3109
|
+
}
|
|
3110
|
+
function agentDefaultModel(name, registry2 = AGENT_SPECS) {
|
|
3111
|
+
return getAgentSpec(name, registry2)?.defaultModel ?? name;
|
|
3112
|
+
}
|
|
3113
|
+
function agentNormalizerKind(name, registry2 = AGENT_SPECS) {
|
|
3114
|
+
return getAgentSpec(name, registry2)?.normalizer ?? "generic";
|
|
3115
|
+
}
|
|
3116
|
+
function agentSmokeCommand(name, registry2 = AGENT_SPECS) {
|
|
3117
|
+
return getAgentSpec(name, registry2)?.smokeCommand ?? `${name} --version`;
|
|
3118
|
+
}
|
|
3119
|
+
var BASE_AGENT_SPECS, AGENT_SPECS;
|
|
3120
|
+
var init_specs = __esm({
|
|
3121
|
+
"packages/core/dist/agent/specs.js"() {
|
|
3122
|
+
"use strict";
|
|
3123
|
+
BASE_AGENT_SPECS = [
|
|
3124
|
+
{
|
|
3125
|
+
name: "claude",
|
|
3126
|
+
displayName: "claude",
|
|
3127
|
+
defaultModel: "claude-sonnet-4",
|
|
3128
|
+
normalizer: "claude",
|
|
3129
|
+
usage: { stdoutExtractor: "claude-stream", sessionBackfill: "claude-projects" },
|
|
3130
|
+
smokeCommand: 'claude -p "Reply with a single word: hello"'
|
|
3131
|
+
},
|
|
3132
|
+
{
|
|
3133
|
+
name: "codex",
|
|
3134
|
+
aliases: ["openai"],
|
|
3135
|
+
displayName: "codex",
|
|
3136
|
+
defaultModel: "gpt-5.5",
|
|
3137
|
+
normalizer: "codex",
|
|
3138
|
+
usage: { stdoutExtractor: "openai", sessionRecovery: "codex" },
|
|
3139
|
+
smokeCommand: 'codex exec "Reply with a single word: hello"'
|
|
3140
|
+
},
|
|
3141
|
+
{
|
|
3142
|
+
name: "kimi",
|
|
3143
|
+
displayName: "kimi",
|
|
3144
|
+
defaultModel: "kimi-k2",
|
|
3145
|
+
normalizer: "generic",
|
|
3146
|
+
usage: { stdoutExtractor: "kimi", sessionRecovery: "kimi" },
|
|
3147
|
+
smokeCommand: 'kimi-code -p "Reply with a single word: hello"'
|
|
3148
|
+
},
|
|
3149
|
+
{
|
|
3150
|
+
name: "qwen",
|
|
3151
|
+
displayName: "qwen",
|
|
3152
|
+
defaultModel: "qwen-coder-plus",
|
|
3153
|
+
normalizer: "generic",
|
|
3154
|
+
usage: { stdoutExtractor: "qwen" },
|
|
3155
|
+
smokeCommand: 'qwen -p "Reply with a single word: hello"'
|
|
3156
|
+
},
|
|
3157
|
+
{
|
|
3158
|
+
name: "agy",
|
|
3159
|
+
aliases: ["antigravity", "gemini"],
|
|
3160
|
+
displayName: "antigravity (agy)",
|
|
3161
|
+
defaultModel: "gemini-2.5-pro",
|
|
3162
|
+
normalizer: "generic",
|
|
3163
|
+
usage: { stdoutExtractor: "gemini" },
|
|
3164
|
+
smokeCommand: 'agy -p "Reply with a single word: hello"'
|
|
3165
|
+
},
|
|
3166
|
+
{
|
|
3167
|
+
name: "pi",
|
|
3168
|
+
aliases: ["deepseek"],
|
|
3169
|
+
displayName: "pi",
|
|
3170
|
+
defaultModel: "deepseek-v4-pro",
|
|
3171
|
+
normalizer: "generic",
|
|
3172
|
+
usage: { stdoutExtractor: "pi", sessionRecovery: "pi" },
|
|
3173
|
+
smokeCommand: 'pi -p "Reply with a single word: hello"'
|
|
3174
|
+
},
|
|
3175
|
+
{
|
|
3176
|
+
name: "cursor",
|
|
3177
|
+
displayName: "cursor",
|
|
3178
|
+
defaultModel: "cursor",
|
|
3179
|
+
normalizer: "generic",
|
|
3180
|
+
usage: { stdoutExtractor: "generic" },
|
|
3181
|
+
smokeCommand: "cursor --version"
|
|
3182
|
+
},
|
|
3183
|
+
{
|
|
3184
|
+
name: "opencode",
|
|
3185
|
+
displayName: "opencode",
|
|
3186
|
+
defaultModel: "opencode",
|
|
3187
|
+
normalizer: "generic",
|
|
3188
|
+
usage: { stdoutExtractor: "generic" },
|
|
3189
|
+
smokeCommand: "opencode --version"
|
|
3190
|
+
},
|
|
3191
|
+
{
|
|
3192
|
+
name: "trae",
|
|
3193
|
+
displayName: "trae",
|
|
3194
|
+
defaultModel: "trae",
|
|
3195
|
+
normalizer: "generic",
|
|
3196
|
+
usage: { stdoutExtractor: "generic" },
|
|
3197
|
+
smokeCommand: "trae --version"
|
|
3198
|
+
},
|
|
3199
|
+
{
|
|
3200
|
+
name: "openclaw",
|
|
3201
|
+
displayName: "openclaw",
|
|
3202
|
+
defaultModel: "openclaw",
|
|
3203
|
+
normalizer: "generic",
|
|
3204
|
+
usage: { stdoutExtractor: "generic" },
|
|
3205
|
+
smokeCommand: "openclaw --version"
|
|
3206
|
+
}
|
|
3207
|
+
];
|
|
3208
|
+
AGENT_SPECS = withAgentSpecs();
|
|
3209
|
+
}
|
|
3210
|
+
});
|
|
3211
|
+
|
|
3079
3212
|
// packages/core/dist/agent/router.js
|
|
3080
3213
|
function classifyComplexity(estMin) {
|
|
3081
3214
|
if (estMin === null || estMin === void 0)
|
|
@@ -3380,7 +3513,8 @@ function makeStdoutExtractor(spec) {
|
|
|
3380
3513
|
};
|
|
3381
3514
|
}
|
|
3382
3515
|
function extractUsage(agent, lines2) {
|
|
3383
|
-
const
|
|
3516
|
+
const key = getAgentSpec(agent)?.usage.stdoutExtractor ?? agent;
|
|
3517
|
+
const fn = REGISTRY[key];
|
|
3384
3518
|
if (fn === void 0)
|
|
3385
3519
|
return null;
|
|
3386
3520
|
let result;
|
|
@@ -3391,8 +3525,9 @@ function extractUsage(agent, lines2) {
|
|
|
3391
3525
|
}
|
|
3392
3526
|
if (result === null)
|
|
3393
3527
|
return null;
|
|
3394
|
-
|
|
3395
|
-
|
|
3528
|
+
const required2 = key === "claude-stream" ? REQUIRED_FIELDS.filter((field2) => field2 !== "cost_list_usd") : REQUIRED_FIELDS;
|
|
3529
|
+
for (const field2 of required2) {
|
|
3530
|
+
if (result[field2] === void 0 || result[field2] === null)
|
|
3396
3531
|
return null;
|
|
3397
3532
|
}
|
|
3398
3533
|
return result;
|
|
@@ -3639,6 +3774,7 @@ var MODEL_RE, INPUT_RE, OUTPUT_RE, COST_RE, TOTAL_RE_OPENAI, TOTAL_RE_GENERIC, S
|
|
|
3639
3774
|
var init_tracker = __esm({
|
|
3640
3775
|
"packages/core/dist/cost/tracker.js"() {
|
|
3641
3776
|
"use strict";
|
|
3777
|
+
init_specs();
|
|
3642
3778
|
init_prices();
|
|
3643
3779
|
MODEL_RE = /^\s*model\s*[:=]\s*([A-Za-z0-9][\w.\-]*)/i;
|
|
3644
3780
|
INPUT_RE = /input(?:\s+tokens)?\s*[:=]\s*([\d,]+)/i;
|
|
@@ -3658,11 +3794,13 @@ var init_tracker = __esm({
|
|
|
3658
3794
|
qwenExtract = makeStdoutExtractor(STDOUT_AGENTS["qwen"]);
|
|
3659
3795
|
piExtract = () => null;
|
|
3660
3796
|
REGISTRY = {
|
|
3797
|
+
"claude-stream": sumClaudeStream,
|
|
3661
3798
|
pi: piExtract,
|
|
3662
3799
|
openai: openaiExtract,
|
|
3663
3800
|
gemini: geminiExtract,
|
|
3664
3801
|
kimi: kimiExtract,
|
|
3665
|
-
qwen: qwenExtract
|
|
3802
|
+
qwen: qwenExtract,
|
|
3803
|
+
generic: makeStdoutExtractor({ defaultModel: "generic", totalKind: "generic" })
|
|
3666
3804
|
};
|
|
3667
3805
|
REQUIRED_FIELDS = [
|
|
3668
3806
|
"model",
|
|
@@ -3800,7 +3938,7 @@ function peerReviewCost(peer, stdout) {
|
|
|
3800
3938
|
try {
|
|
3801
3939
|
const canon = canonicalAgentName(peer);
|
|
3802
3940
|
const lines2 = stdout.split("\n");
|
|
3803
|
-
const usage2 =
|
|
3941
|
+
const usage2 = extractUsage(canon, lines2);
|
|
3804
3942
|
if (usage2 === null)
|
|
3805
3943
|
return 0;
|
|
3806
3944
|
if ((usage2.input_tokens ?? 0) === 0 && (usage2.output_tokens ?? 0) === 0)
|
|
@@ -5442,10 +5580,10 @@ function codexJsonl(line) {
|
|
|
5442
5580
|
}
|
|
5443
5581
|
}
|
|
5444
5582
|
function normalizerFor(agent) {
|
|
5445
|
-
const
|
|
5446
|
-
if (
|
|
5583
|
+
const kind = agentNormalizerKind(agent);
|
|
5584
|
+
if (kind === "claude")
|
|
5447
5585
|
return claudeNormalizer;
|
|
5448
|
-
if (
|
|
5586
|
+
if (kind === "codex")
|
|
5449
5587
|
return codexNormalizer;
|
|
5450
5588
|
return genericNormalizer;
|
|
5451
5589
|
}
|
|
@@ -5453,6 +5591,7 @@ var KIND_TIER, DEFAULT_HEARTBEAT_GAP_MS, SUPPRESS_TOOLS, PEER_VERDICTS, claudeNo
|
|
|
5453
5591
|
var init_activity_signal = __esm({
|
|
5454
5592
|
"packages/core/dist/loop/activity-signal.js"() {
|
|
5455
5593
|
"use strict";
|
|
5594
|
+
init_specs();
|
|
5456
5595
|
init_signals();
|
|
5457
5596
|
KIND_TIER = {
|
|
5458
5597
|
lifecycle: "A",
|
|
@@ -6298,6 +6437,8 @@ function mapV2Status(status2) {
|
|
|
6298
6437
|
return "gave_up";
|
|
6299
6438
|
case "orphan":
|
|
6300
6439
|
return "aborted_with_delivery";
|
|
6440
|
+
case "local":
|
|
6441
|
+
return "unpublished";
|
|
6301
6442
|
case "failed":
|
|
6302
6443
|
return "failed";
|
|
6303
6444
|
case "aborted":
|
|
@@ -6327,17 +6468,18 @@ function classifyPublish(pub) {
|
|
|
6327
6468
|
if (pub.status === 0)
|
|
6328
6469
|
return "published";
|
|
6329
6470
|
if (pub.status === 2) {
|
|
6330
|
-
if (pub.manualMerge === true)
|
|
6331
|
-
return "
|
|
6471
|
+
if (pub.manualMerge === true) {
|
|
6472
|
+
return pub.orphanPushed === true ? "orphan" : "local";
|
|
6473
|
+
}
|
|
6332
6474
|
if (pub.mergedBack === true)
|
|
6333
6475
|
return "done";
|
|
6334
6476
|
if (pub.orphanPushed === true)
|
|
6335
6477
|
return "orphan";
|
|
6336
|
-
return "
|
|
6478
|
+
return "local";
|
|
6337
6479
|
}
|
|
6338
6480
|
if (pub.orphanPushed === true)
|
|
6339
6481
|
return "orphan";
|
|
6340
|
-
return "
|
|
6482
|
+
return "local";
|
|
6341
6483
|
}
|
|
6342
6484
|
function watchdogVerdict(elapsedSec, limitSec = CYCLE_TIMEOUT_SEC) {
|
|
6343
6485
|
if (elapsedSec >= limitSec)
|
|
@@ -6542,8 +6684,8 @@ function cycleStep(state, event) {
|
|
|
6542
6684
|
{ kind: "append_alert", message: `cycle ${state.ctx.cycleId}: publish failed; orphan branch+tag pushed; worktree cleaned` }
|
|
6543
6685
|
]);
|
|
6544
6686
|
}
|
|
6545
|
-
return terminate({ ...state, phase: "publish" }, "
|
|
6546
|
-
{ kind: "append_alert", message: `cycle ${state.ctx.cycleId}: publish
|
|
6687
|
+
return terminate({ ...state, phase: "publish" }, "local", [
|
|
6688
|
+
{ kind: "append_alert", message: `cycle ${state.ctx.cycleId}: gates passed but publish did not complete \u2014 work committed locally, worktree preserved at branch ${state.ctx.branch} (unpublished, not a failure)` }
|
|
6547
6689
|
]);
|
|
6548
6690
|
}
|
|
6549
6691
|
case "merge_polled": {
|
|
@@ -6915,7 +7057,15 @@ function parseLoopSafety(lines2, start) {
|
|
|
6915
7057
|
...flat["peer_gate"] === "hard" || flat["peer_gate"] === "soft" ? { peerGate: flat["peer_gate"] } : {},
|
|
6916
7058
|
// FIX-298: the network-guard recovery hook. A non-empty string is the shell
|
|
6917
7059
|
// command the guard runs to enable connectivity before re-checking.
|
|
6918
|
-
...flat["proxy_enable_cmd"] !== void 0 && flat["proxy_enable_cmd"] !== "" ? { proxyEnableCmd: flat["proxy_enable_cmd"] } : {}
|
|
7060
|
+
...flat["proxy_enable_cmd"] !== void 0 && flat["proxy_enable_cmd"] !== "" ? { proxyEnableCmd: flat["proxy_enable_cmd"] } : {},
|
|
7061
|
+
// FIX-338: the prebuild-dist execute-speed lever. DEFAULT-OFF — only an
|
|
7062
|
+
// explicit `prebuild_dist: true` turns it on; anything else (absent / false /
|
|
7063
|
+
// garbage) leaves it false so deploy stays a NO-OP (稳字纪律).
|
|
7064
|
+
...flat["prebuild_dist"] === "true" ? { prebuildDist: true } : {},
|
|
7065
|
+
// FIX-338 (杠杆2): the project-map injection execute-speed lever. DEFAULT-OFF —
|
|
7066
|
+
// only an explicit `project_map: true` turns it on; anything else (absent /
|
|
7067
|
+
// false / garbage) leaves it false so deploy stays a NO-OP (稳字纪律).
|
|
7068
|
+
...flat["project_map"] === "true" ? { projectMap: true } : {}
|
|
6919
7069
|
};
|
|
6920
7070
|
return [i, cfg];
|
|
6921
7071
|
}
|
|
@@ -8441,7 +8591,10 @@ var init_selectors = __esm({
|
|
|
8441
8591
|
// was unmapped, fell to outcome "" → "unknown" panel, and the failure was
|
|
8442
8592
|
// silently swallowed from every failed count (the FIX-248 class).
|
|
8443
8593
|
reverted: "failed",
|
|
8444
|
-
orphan: "aborted_with_delivery"
|
|
8594
|
+
orphan: "aborted_with_delivery",
|
|
8595
|
+
// FIX-351: gates passed but publish did not land (work committed locally) — a
|
|
8596
|
+
// neutral non-failure terminal, NOT mapped into the failed cluster.
|
|
8597
|
+
local: "unpublished"
|
|
8445
8598
|
};
|
|
8446
8599
|
}
|
|
8447
8600
|
});
|
|
@@ -8451,6 +8604,7 @@ var init_dist2 = __esm({
|
|
|
8451
8604
|
"packages/core/dist/index.js"() {
|
|
8452
8605
|
"use strict";
|
|
8453
8606
|
init_registry();
|
|
8607
|
+
init_specs();
|
|
8454
8608
|
init_router();
|
|
8455
8609
|
init_pairing();
|
|
8456
8610
|
init_peer_review();
|
|
@@ -8575,11 +8729,15 @@ function fmtDur(s) {
|
|
|
8575
8729
|
function fmtModel(model) {
|
|
8576
8730
|
if (model === null || model === void 0 || model === "")
|
|
8577
8731
|
return "\u2014";
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8732
|
+
const prefixed = /^([a-z][a-z0-9_]*)-(.+)$/i.exec(model);
|
|
8733
|
+
if (prefixed === null)
|
|
8734
|
+
return model;
|
|
8735
|
+
const vendor = prefixed[1] ?? "";
|
|
8736
|
+
let s = prefixed[2] ?? "";
|
|
8737
|
+
if (vendor !== "claude")
|
|
8738
|
+
return model;
|
|
8581
8739
|
s = s.replace(/-\d{6,8}$/, "");
|
|
8582
|
-
return s !== "" ? s :
|
|
8740
|
+
return s !== "" ? s : model;
|
|
8583
8741
|
}
|
|
8584
8742
|
function pyFixed1(x) {
|
|
8585
8743
|
const scaled = x * 10;
|
|
@@ -8781,8 +8939,8 @@ function cycleRow(cy) {
|
|
|
8781
8939
|
const timeC = outcome === "fail" ? "red" : "fg";
|
|
8782
8940
|
const sidC = outcome === "fail" ? "red" : "blue";
|
|
8783
8941
|
let modelLabel = fmtModel(cy.model);
|
|
8784
|
-
if (
|
|
8785
|
-
modelLabel =
|
|
8942
|
+
if (modelLabel === "\u2014" && cy.agent) {
|
|
8943
|
+
modelLabel = agentDefaultModel(cy.agent);
|
|
8786
8944
|
}
|
|
8787
8945
|
const showModel = COLS >= 100;
|
|
8788
8946
|
const modelSeg = showModel ? c("muted", pad(modelLabel, 11)) + " " : "";
|
|
@@ -8818,10 +8976,11 @@ function cycleRow(cy) {
|
|
|
8818
8976
|
}
|
|
8819
8977
|
return lines2;
|
|
8820
8978
|
}
|
|
8821
|
-
var COLS, renderState, PAL, BOLD, RESET, ANSI_RE2, RESET_RAW,
|
|
8979
|
+
var COLS, renderState, PAL, BOLD, RESET, ANSI_RE2, RESET_RAW, BG_FAIL, WEEKDAY_EN, WEEKDAY_ZH;
|
|
8822
8980
|
var init_render = __esm({
|
|
8823
8981
|
"packages/cli/dist/render.js"() {
|
|
8824
8982
|
"use strict";
|
|
8983
|
+
init_dist2();
|
|
8825
8984
|
COLS = 100;
|
|
8826
8985
|
renderState = { useColor: true };
|
|
8827
8986
|
PAL = {
|
|
@@ -8841,11 +9000,6 @@ var init_render = __esm({
|
|
|
8841
9000
|
RESET = "\x1B[0m";
|
|
8842
9001
|
ANSI_RE2 = /\x1b\[[\d;]*m/g;
|
|
8843
9002
|
RESET_RAW = "\x1B[0m";
|
|
8844
|
-
AGENT_PRIMARY_MODEL = {
|
|
8845
|
-
pi: "deepseek-v4-pro",
|
|
8846
|
-
deepseek: "deepseek-v4-pro",
|
|
8847
|
-
kimi: "kimi-k2-0905"
|
|
8848
|
-
};
|
|
8849
9003
|
BG_FAIL = "\x1B[48;2;55;15;15m";
|
|
8850
9004
|
WEEKDAY_EN = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
8851
9005
|
WEEKDAY_ZH = ["\u5468\u4E00", "\u5468\u4E8C", "\u5468\u4E09", "\u5468\u56DB", "\u5468\u4E94", "\u5468\u516D", "\u5468\u65E5"];
|
|
@@ -12183,6 +12337,11 @@ function outcomeToPanel(outcome, state) {
|
|
|
12183
12337
|
return "done";
|
|
12184
12338
|
case "idle_no_work":
|
|
12185
12339
|
return "idle";
|
|
12340
|
+
// FIX-351: gates passed but publish did not land (work committed locally) —
|
|
12341
|
+
// a NEUTRAL state, NOT a failure. Classified `idle`-side so the panel/tally
|
|
12342
|
+
// never counts a sound-but-unpublished cycle as a fail (it is not red).
|
|
12343
|
+
case "unpublished":
|
|
12344
|
+
return "idle";
|
|
12186
12345
|
case "failed":
|
|
12187
12346
|
case "blocked":
|
|
12188
12347
|
case "aborted_no_delivery":
|
|
@@ -14049,6 +14208,7 @@ duration: ${facts.durationMs}ms
|
|
|
14049
14208
|
// packages/cli/dist/commands/dashboard.js
|
|
14050
14209
|
init_render();
|
|
14051
14210
|
init_dist3();
|
|
14211
|
+
init_dist2();
|
|
14052
14212
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
14053
14213
|
import { createHash as createHash3 } from "node:crypto";
|
|
14054
14214
|
import { existsSync as existsSync18, readFileSync as readFileSync17, readdirSync as readdirSync7, realpathSync as realpathSync2, statSync as statSync8 } from "node:fs";
|
|
@@ -14731,7 +14891,9 @@ function repairOrphanCyclesFromGit(cycles, gitMerges) {
|
|
|
14731
14891
|
}
|
|
14732
14892
|
}
|
|
14733
14893
|
}
|
|
14734
|
-
function
|
|
14894
|
+
function loadStreamJsonSessionUsage(label4, slug, agent) {
|
|
14895
|
+
if (getAgentSpec(agent)?.usage.sessionBackfill !== "claude-projects")
|
|
14896
|
+
return null;
|
|
14735
14897
|
const user = process.env["USER"] ?? "seanyao";
|
|
14736
14898
|
const worktreePath = `/Users/${user}/.shared/roll/worktrees/${slug}-cycle-${label4}`;
|
|
14737
14899
|
const projName = "-" + worktreePath.replaceAll("/", "-").replaceAll(".", "-").replace(/^-+/, "");
|
|
@@ -14796,7 +14958,7 @@ function loadClaudeSessionUsage(label4, slug) {
|
|
|
14796
14958
|
return null;
|
|
14797
14959
|
return { model, ...sums, cost_reported_usd: cost, duration_ms: durationMs };
|
|
14798
14960
|
}
|
|
14799
|
-
function
|
|
14961
|
+
function backfillUsageFromAgentSessions(cycles, slug) {
|
|
14800
14962
|
for (const cy of cycles) {
|
|
14801
14963
|
const ue = cy.usage_event;
|
|
14802
14964
|
if (ue !== void 0 && (toInt2(ue["input_tokens"]) || toInt2(ue["output_tokens"]))) {
|
|
@@ -14827,7 +14989,7 @@ function backfillUsageFromClaudeSessions(cycles, slug) {
|
|
|
14827
14989
|
}
|
|
14828
14990
|
if (cy.input_tokens || cy.output_tokens)
|
|
14829
14991
|
continue;
|
|
14830
|
-
const u =
|
|
14992
|
+
const u = loadStreamJsonSessionUsage(cy.label, slug, cy.agent ?? "");
|
|
14831
14993
|
if (!u)
|
|
14832
14994
|
continue;
|
|
14833
14995
|
cy.input_tokens = toInt2(u.input_tokens);
|
|
@@ -15414,7 +15576,7 @@ function isoLabel(d) {
|
|
|
15414
15576
|
return `${d.getUTCFullYear()}${pad2(d.getUTCMonth() + 1)}${pad2(d.getUTCDate())}-${pad2(d.getUTCHours())}${pad2(d.getUTCMinutes())}${pad2(d.getUTCSeconds())}-30585`;
|
|
15415
15577
|
}
|
|
15416
15578
|
function render(out3, events, cron, state, backlog, args) {
|
|
15417
|
-
const { days, lang: lang9, runs, gitMerges,
|
|
15579
|
+
const { days, lang: lang9, runs, gitMerges, projectSlug: projectSlug4, now } = args;
|
|
15418
15580
|
const cycles = aggregate(events, cron);
|
|
15419
15581
|
const matchedRunIds = Object.keys(runs).length > 0 ? mergeRunsIntoCycles(cycles, runs) : /* @__PURE__ */ new Set();
|
|
15420
15582
|
for (const cy of cycles) {
|
|
@@ -15430,7 +15592,7 @@ function render(out3, events, cron, state, backlog, args) {
|
|
|
15430
15592
|
}
|
|
15431
15593
|
if (Object.keys(gitMerges).length > 0)
|
|
15432
15594
|
repairOrphanCyclesFromGit(cycles, gitMerges);
|
|
15433
|
-
|
|
15595
|
+
backfillUsageFromAgentSessions(cycles, projectSlug4 ?? "");
|
|
15434
15596
|
const byDay = bucketByDay(cycles);
|
|
15435
15597
|
const daysKeys = [...byDay.keys()].sort().reverse().slice(0, days);
|
|
15436
15598
|
const bilingual2 = (enLine, zhLine) => {
|
|
@@ -15692,7 +15854,7 @@ function collectCycles(slug, days) {
|
|
|
15692
15854
|
mergeRunsIntoCycles(cycles, runs);
|
|
15693
15855
|
if (Object.keys(gitMerges).length > 0)
|
|
15694
15856
|
repairOrphanCyclesFromGit(cycles, gitMerges);
|
|
15695
|
-
|
|
15857
|
+
backfillUsageFromAgentSessions(cycles, slug);
|
|
15696
15858
|
return cycles;
|
|
15697
15859
|
}
|
|
15698
15860
|
function rollupForStory(cycles, storyId) {
|
|
@@ -15993,7 +16155,7 @@ roll-loop-status.py: error: unrecognized arguments: ${args.unknown.join(" ")}
|
|
|
15993
16155
|
lang: lang9,
|
|
15994
16156
|
runs,
|
|
15995
16157
|
gitMerges,
|
|
15996
|
-
|
|
16158
|
+
projectSlug: slug,
|
|
15997
16159
|
now,
|
|
15998
16160
|
rtDir: useFixture || slug === null ? null : loopRuntimeDir(slug)
|
|
15999
16161
|
});
|
|
@@ -16907,6 +17069,12 @@ function overviewTab(input) {
|
|
|
16907
17069
|
}
|
|
16908
17070
|
var VERDICT_COLORS = {
|
|
16909
17071
|
delivered: C.green,
|
|
17072
|
+
pending_merge: C.amber,
|
|
17073
|
+
// PR open, merge pending — in-flight (amber), not red
|
|
17074
|
+
// FIX-351: gates passed but publish did not land (work committed locally) —
|
|
17075
|
+
// a NEUTRAL blue, clearly distinct from `failed` (red). The dashboard reads
|
|
17076
|
+
// it as "ran locally, not published", never as a failure.
|
|
17077
|
+
unpublished: C.blue,
|
|
16910
17078
|
reverted: C.amber,
|
|
16911
17079
|
failed: C.red,
|
|
16912
17080
|
blocked: C.purple,
|
|
@@ -16915,6 +17083,9 @@ var VERDICT_COLORS = {
|
|
|
16915
17083
|
};
|
|
16916
17084
|
var VERDICT_ZH = {
|
|
16917
17085
|
delivered: "\u5DF2\u4EA4\u4ED8",
|
|
17086
|
+
pending_merge: "\u5F85\u5408\u5E76",
|
|
17087
|
+
unpublished: "\u672A\u53D1\u5E03",
|
|
17088
|
+
// FIX-351: 闸通过但未发布(本地已提交)——中性,非失败
|
|
16918
17089
|
reverted: "\u5DF2\u56DE\u6EDA",
|
|
16919
17090
|
failed: "\u5931\u8D25",
|
|
16920
17091
|
blocked: "\u88AB\u963B\u585E",
|
|
@@ -18091,6 +18262,8 @@ function ledgerVerdict(status2, outcome) {
|
|
|
18091
18262
|
if (outcome === "published_pending_merge" || status2 === "published" || status2 === "built" || status2 === "done") {
|
|
18092
18263
|
return "pending_merge";
|
|
18093
18264
|
}
|
|
18265
|
+
if (outcome === "unpublished" || status2 === "local")
|
|
18266
|
+
return "unpublished";
|
|
18094
18267
|
if (outcome === "blocked" || status2 === "blocked")
|
|
18095
18268
|
return "blocked";
|
|
18096
18269
|
if (outcome === "idle_no_work" || status2 === "idle")
|
|
@@ -18112,7 +18285,9 @@ function reconcilePendingMergeVerdicts(rows, isMerged) {
|
|
|
18112
18285
|
return rows.map((r) => {
|
|
18113
18286
|
if (r.verdict !== "pending_merge")
|
|
18114
18287
|
return r;
|
|
18115
|
-
if (r.storyId === ""
|
|
18288
|
+
if (r.storyId === "" && r.prNumber === void 0)
|
|
18289
|
+
return r;
|
|
18290
|
+
if (!isMerged(r.storyId, r.prNumber))
|
|
18116
18291
|
return r;
|
|
18117
18292
|
return {
|
|
18118
18293
|
...r,
|
|
@@ -18164,14 +18339,15 @@ function readEventFacts(projectPath3) {
|
|
|
18164
18339
|
const byCycle = /* @__PURE__ */ new Map();
|
|
18165
18340
|
const prMergedBy = /* @__PURE__ */ new Map();
|
|
18166
18341
|
const prOpenBy = /* @__PURE__ */ new Map();
|
|
18342
|
+
const prByCycle = /* @__PURE__ */ new Map();
|
|
18167
18343
|
const path = join23(projectPath3, ".roll", "loop", "events.ndjson");
|
|
18168
18344
|
if (!existsSync25(path))
|
|
18169
|
-
return { byCycle, prMergedBy, prOpenBy };
|
|
18345
|
+
return { byCycle, prMergedBy, prOpenBy, prByCycle };
|
|
18170
18346
|
let content = "";
|
|
18171
18347
|
try {
|
|
18172
18348
|
content = readFileSync23(path, "utf8");
|
|
18173
18349
|
} catch {
|
|
18174
|
-
return { byCycle, prMergedBy, prOpenBy };
|
|
18350
|
+
return { byCycle, prMergedBy, prOpenBy, prByCycle };
|
|
18175
18351
|
}
|
|
18176
18352
|
const facts = (id) => {
|
|
18177
18353
|
let f = byCycle.get(id);
|
|
@@ -18195,14 +18371,28 @@ function readEventFacts(projectPath3) {
|
|
|
18195
18371
|
prMergedBy.set(e.storyId, e.prNumber);
|
|
18196
18372
|
else if (e.type === "pr:open")
|
|
18197
18373
|
prOpenBy.set(e.storyId, e.prNumber);
|
|
18374
|
+
else if (e.type === "cycle:terminal" && e.pr.present) {
|
|
18375
|
+
const n = terminalPrNumber(e.pr.value);
|
|
18376
|
+
if (n !== void 0)
|
|
18377
|
+
prByCycle.set(e.cycleId, n);
|
|
18378
|
+
}
|
|
18198
18379
|
}
|
|
18199
|
-
return { byCycle, prMergedBy, prOpenBy };
|
|
18380
|
+
return { byCycle, prMergedBy, prOpenBy, prByCycle };
|
|
18381
|
+
}
|
|
18382
|
+
function terminalPrNumber(pr) {
|
|
18383
|
+
if (typeof pr.number === "number" && Number.isInteger(pr.number) && pr.number > 0)
|
|
18384
|
+
return pr.number;
|
|
18385
|
+
const m7 = /\/pull\/(\d+)/.exec(pr.url);
|
|
18386
|
+
if (m7 === null)
|
|
18387
|
+
return void 0;
|
|
18388
|
+
const n = Number(m7[1]);
|
|
18389
|
+
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
18200
18390
|
}
|
|
18201
18391
|
function rowTape(row2, verdict, ev, prNumber, prOpen) {
|
|
18202
18392
|
const storyId = typeof row2["story_id"] === "string" ? row2["story_id"] : "";
|
|
18203
18393
|
const tcr = typeof row2["tcr_count"] === "number" ? row2["tcr_count"] : 0;
|
|
18204
18394
|
const seg = (key, detail, state) => ({ key, detail, state });
|
|
18205
|
-
const endState = verdict === "delivered" ? "pass" : verdict === "idle" ? "idle" : verdict === "unknown" ? "unknown" : "fail";
|
|
18395
|
+
const endState = verdict === "delivered" ? "pass" : verdict === "idle" || verdict === "unpublished" ? "idle" : verdict === "unknown" ? "unknown" : "fail";
|
|
18206
18396
|
return [
|
|
18207
18397
|
seg("cycle", typeof row2["ts"] === "string" ? row2["ts"].replace("T", " ").replace(/:\d{2}Z$/, "Z") : "\u2014", "pass"),
|
|
18208
18398
|
seg("story", storyId !== "" ? storyId : "\u2014", storyId !== "" ? "pass" : "idle"),
|
|
@@ -18228,7 +18418,7 @@ function collectCycleLedger(projectPath3) {
|
|
|
18228
18418
|
} catch {
|
|
18229
18419
|
return [];
|
|
18230
18420
|
}
|
|
18231
|
-
const { byCycle, prMergedBy, prOpenBy } = readEventFacts(projectPath3);
|
|
18421
|
+
const { byCycle, prMergedBy, prOpenBy, prByCycle } = readEventFacts(projectPath3);
|
|
18232
18422
|
const rows = [];
|
|
18233
18423
|
for (const line of content.split("\n")) {
|
|
18234
18424
|
if (line.trim() === "")
|
|
@@ -18269,7 +18459,10 @@ function collectCycleLedger(projectPath3) {
|
|
|
18269
18459
|
cost: cost !== void 0 ? `$${cost.toFixed(2)}` : usageUnknown ? "?" : "\u2014",
|
|
18270
18460
|
duration: fmtDuration(row2["duration_sec"]),
|
|
18271
18461
|
tape: rowTape(row2, verdict, ev, prNumber, prOpen),
|
|
18272
|
-
evidence
|
|
18462
|
+
evidence,
|
|
18463
|
+
// FIX-348: the cycle's own PR number (cycle:terminal twin), falling back to
|
|
18464
|
+
// the merged/open PR event keyed by story when the terminal twin is absent.
|
|
18465
|
+
prNumber: prByCycle.get(cycleId) ?? prNumber ?? prOpen
|
|
18273
18466
|
});
|
|
18274
18467
|
}
|
|
18275
18468
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -21924,6 +22117,20 @@ function storyHasMergeEvidence(facts, storyId) {
|
|
|
21924
22117
|
return true;
|
|
21925
22118
|
return factsPrNumbers(facts, storyId).length > 0;
|
|
21926
22119
|
}
|
|
22120
|
+
function gitHasPrMergeCommit(facts, prNumber) {
|
|
22121
|
+
if (facts === null || !Number.isInteger(prNumber) || prNumber <= 0)
|
|
22122
|
+
return false;
|
|
22123
|
+
for (const c2 of facts.commits) {
|
|
22124
|
+
for (const m7 of c2.subject.matchAll(/(?:\(#|PR\s*#?)(\d+)\)?/gi)) {
|
|
22125
|
+
if (numberFromString(m7[1]) === prNumber)
|
|
22126
|
+
return true;
|
|
22127
|
+
}
|
|
22128
|
+
}
|
|
22129
|
+
return false;
|
|
22130
|
+
}
|
|
22131
|
+
function cycleMergeTruth(facts) {
|
|
22132
|
+
return (storyId, prNumber) => prNumber !== void 0 && prNumber !== null ? gitHasPrMergeCommit(facts, prNumber) : storyId !== "" && storyHasMergeEvidence(facts, storyId);
|
|
22133
|
+
}
|
|
21927
22134
|
function factsPrNumbers(facts, storyId) {
|
|
21928
22135
|
const nums = /* @__PURE__ */ new Set();
|
|
21929
22136
|
for (const subject of factsSubjects(facts, storyId)) {
|
|
@@ -21995,7 +22202,13 @@ function statDir(p) {
|
|
|
21995
22202
|
return statSync13(p).isFile();
|
|
21996
22203
|
}
|
|
21997
22204
|
function execGit(cwd, args) {
|
|
21998
|
-
return execFileSync6("git", args, {
|
|
22205
|
+
return execFileSync6("git", args, {
|
|
22206
|
+
cwd,
|
|
22207
|
+
encoding: "utf8",
|
|
22208
|
+
timeout: 1e4,
|
|
22209
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
22210
|
+
maxBuffer: 256 * 1024 * 1024
|
|
22211
|
+
});
|
|
21999
22212
|
}
|
|
22000
22213
|
|
|
22001
22214
|
// packages/cli/dist/commands/index-gen.js
|
|
@@ -22335,7 +22548,10 @@ function generateDossierPages(cwd, rebuild) {
|
|
|
22335
22548
|
// rewrites its runs row. Reuses the SAME offline git facts the dossier
|
|
22336
22549
|
// already built (storyHasMergeEvidence — a `git log` check, no gh call;
|
|
22337
22550
|
// refreshDossierMergeBaseline fetched origin/main just above).
|
|
22338
|
-
|
|
22551
|
+
// FIX-348: cycleMergeTruth ALSO matches the row's recorded PR number, so
|
|
22552
|
+
// a merged delivery whose squash commit carries `(#N)` but does NOT name
|
|
22553
|
+
// the story-id (e.g. FIX-287 / PR #773) still reconciles to delivered.
|
|
22554
|
+
cycles: reconcilePendingMergeVerdicts(collectCycleLedger(cwd), cycleMergeTruth(runCache.git)),
|
|
22339
22555
|
agents: agentRows,
|
|
22340
22556
|
releasePanel: collectReleasePanel(cwd),
|
|
22341
22557
|
skills: collectSkillsPanel(cwd),
|
|
@@ -24919,7 +25135,7 @@ import { join as join42 } from "node:path";
|
|
|
24919
25135
|
init_render();
|
|
24920
25136
|
function reconciledLedger(cwd) {
|
|
24921
25137
|
const git4 = collectGitDossierFacts(cwd);
|
|
24922
|
-
return reconcilePendingMergeVerdicts(collectCycleLedger(cwd), (
|
|
25138
|
+
return reconcilePendingMergeVerdicts(collectCycleLedger(cwd), cycleMergeTruth(git4));
|
|
24923
25139
|
}
|
|
24924
25140
|
var CYCLES_USAGE = "Usage: roll cycles [--since 1d|3d|7d|all] [--detail <id>]\n The cycle ledger: one line per cycle, failures never swallowed (default --since 3d).\n --detail <id> the per-cycle build-phase timeline (per-commit / heartbeat timing).\n\u5468\u671F\u8D26\u672C\uFF1A\u6BCF\u884C\u4E00\u4E2A cycle\uFF0C\u5931\u8D25\u4E0D\u88AB\u541E\uFF08\u9ED8\u8BA4 --since 3d\uFF09\u3002\n --detail <id> \u5355\u4E2A cycle \u7684 build \u9636\u6BB5\u65F6\u95F4\u7EBF\uFF08\u6BCF\u63D0\u4EA4/\u5FC3\u8DF3\u8BA1\u65F6\uFF09\u3002";
|
|
24925
25141
|
var WINDOWS = { "1d": 1, "3d": 3, "7d": 7 };
|
|
@@ -24931,6 +25147,8 @@ var VERDICT_COLOR3 = {
|
|
|
24931
25147
|
delivered: "green",
|
|
24932
25148
|
pending_merge: "yellow",
|
|
24933
25149
|
// FIX-322: opened a PR, merge pending — in-flight, NOT delivered (amber)
|
|
25150
|
+
unpublished: "blue",
|
|
25151
|
+
// FIX-351: gates passed, work local, publish didn't land — neutral, NOT red
|
|
24934
25152
|
reverted: "yellow",
|
|
24935
25153
|
failed: "red",
|
|
24936
25154
|
blocked: "purple",
|
|
@@ -25185,7 +25403,7 @@ function findCycle(rows, raw) {
|
|
|
25185
25403
|
}
|
|
25186
25404
|
function renderCycleTrace(row2, lang9, slug) {
|
|
25187
25405
|
const lines2 = [];
|
|
25188
|
-
lines2.push(`#${cycleNo(row2.cycleId)} \xB7 ${c(row2.verdict === "delivered" ? "green" : row2.verdict === "idle" ? "muted" : "red", row2.verdict)} \xB7 ${row2.model} \xB7 ${row2.tokens} \xB7 ${row2.cost} \xB7 ${row2.duration}`);
|
|
25406
|
+
lines2.push(`#${cycleNo(row2.cycleId)} \xB7 ${c(row2.verdict === "delivered" ? "green" : row2.verdict === "idle" || row2.verdict === "unpublished" ? "muted" : "red", row2.verdict)} \xB7 ${row2.model} \xB7 ${row2.tokens} \xB7 ${row2.cost} \xB7 ${row2.duration}`);
|
|
25189
25407
|
lines2.push(lang9 === "zh" ? `story ${row2.storyId === "" ? "\u2014\uFF08\u65E0\u6545\u4E8B\uFF09" : row2.storyId}` : `story ${row2.storyId === "" ? "\u2014 (no story)" : row2.storyId}`);
|
|
25190
25408
|
lines2.push("");
|
|
25191
25409
|
const reached = new Set(row2.tape.filter((s) => s.detail !== "\u2014" || s.state !== "unknown").map((s) => s.key));
|
|
@@ -28600,23 +28818,47 @@ function reportCandidates(worktreeCwd, storyId) {
|
|
|
28600
28818
|
function acMapCandidates(worktreeCwd, storyId) {
|
|
28601
28819
|
return [join57(cardArchiveDir(worktreeCwd, storyId), "ac-map.json")];
|
|
28602
28820
|
}
|
|
28603
|
-
function
|
|
28821
|
+
function storySpecMatches(worktreeCwd, storyId) {
|
|
28604
28822
|
const featuresDir = join57(worktreeCwd, ".roll", "features");
|
|
28823
|
+
const matches = [];
|
|
28824
|
+
let entries;
|
|
28605
28825
|
try {
|
|
28606
|
-
|
|
28607
|
-
if (!epic.isDirectory())
|
|
28608
|
-
continue;
|
|
28609
|
-
const spec = join57(featuresDir, epic.name, storyId, "spec.md");
|
|
28610
|
-
if (existsSync58(spec))
|
|
28611
|
-
return spec;
|
|
28612
|
-
const legacy = join57(featuresDir, epic.name, `${storyId}.md`);
|
|
28613
|
-
if (existsSync58(legacy))
|
|
28614
|
-
return legacy;
|
|
28615
|
-
}
|
|
28826
|
+
entries = readdirSync23(featuresDir, { withFileTypes: true });
|
|
28616
28827
|
} catch {
|
|
28617
|
-
return
|
|
28828
|
+
return matches;
|
|
28618
28829
|
}
|
|
28619
|
-
|
|
28830
|
+
for (const epic of entries) {
|
|
28831
|
+
if (!epic.isDirectory())
|
|
28832
|
+
continue;
|
|
28833
|
+
const spec = join57(featuresDir, epic.name, storyId, "spec.md");
|
|
28834
|
+
if (existsSync58(spec)) {
|
|
28835
|
+
matches.push(spec);
|
|
28836
|
+
continue;
|
|
28837
|
+
}
|
|
28838
|
+
const legacy = join57(featuresDir, epic.name, `${storyId}.md`);
|
|
28839
|
+
if (existsSync58(legacy))
|
|
28840
|
+
matches.push(legacy);
|
|
28841
|
+
}
|
|
28842
|
+
return matches;
|
|
28843
|
+
}
|
|
28844
|
+
var DuplicateStoryIdError = class extends Error {
|
|
28845
|
+
storyId;
|
|
28846
|
+
matches;
|
|
28847
|
+
constructor(storyId, matches) {
|
|
28848
|
+
super(`duplicate story id ${storyId}: resolves to ${matches.length} specs \u2014 a story id MUST resolve uniquely (\u4E00\u4E2A ID \u4E00\u4EFD spec). Disambiguate (rename/archive) one of:
|
|
28849
|
+
` + matches.map((m7) => ` - ${m7}`).join("\n"));
|
|
28850
|
+
this.storyId = storyId;
|
|
28851
|
+
this.matches = matches;
|
|
28852
|
+
this.name = "DuplicateStoryIdError";
|
|
28853
|
+
}
|
|
28854
|
+
};
|
|
28855
|
+
function storySpecPath(worktreeCwd, storyId) {
|
|
28856
|
+
const matches = storySpecMatches(worktreeCwd, storyId);
|
|
28857
|
+
if (matches.length === 0)
|
|
28858
|
+
return null;
|
|
28859
|
+
if (matches.length > 1)
|
|
28860
|
+
throw new DuplicateStoryIdError(storyId, matches);
|
|
28861
|
+
return matches[0] ?? null;
|
|
28620
28862
|
}
|
|
28621
28863
|
function storyHasAcBlock(worktreeCwd, storyId) {
|
|
28622
28864
|
const spec = storySpecPath(worktreeCwd, storyId);
|
|
@@ -29367,7 +29609,18 @@ story validate: '${id}' \u4E0D\u662F\u5408\u6CD5\u6545\u4E8B ID
|
|
|
29367
29609
|
return 2;
|
|
29368
29610
|
}
|
|
29369
29611
|
const cwd = process.cwd();
|
|
29370
|
-
|
|
29612
|
+
let spec;
|
|
29613
|
+
try {
|
|
29614
|
+
spec = storySpecPath(cwd, id);
|
|
29615
|
+
} catch (e) {
|
|
29616
|
+
if (e instanceof DuplicateStoryIdError) {
|
|
29617
|
+
process.stderr.write(`story validate: ${e.message}
|
|
29618
|
+
story validate: ${id} \u89E3\u6790\u5230\u591A\u4EFD spec \u2014 \u5FC5\u987B\u552F\u4E00(\u6D88\u9664\u91CD\u590D ID \u540E\u91CD\u8BD5)
|
|
29619
|
+
`);
|
|
29620
|
+
return 2;
|
|
29621
|
+
}
|
|
29622
|
+
throw e;
|
|
29623
|
+
}
|
|
29371
29624
|
if (spec === null) {
|
|
29372
29625
|
process.stderr.write(`story validate: no spec found for ${id} (looked for features/<epic>/${id}/spec.md and features/<epic>/${id}.md)
|
|
29373
29626
|
story validate: \u627E\u4E0D\u5230 ${id} \u7684 spec
|
|
@@ -30835,6 +31088,7 @@ async function loopWatchCommand(args, deps = realDeps6()) {
|
|
|
30835
31088
|
|
|
30836
31089
|
// packages/cli/dist/commands/loop-maint.js
|
|
30837
31090
|
init_dist3();
|
|
31091
|
+
init_dist2();
|
|
30838
31092
|
init_dist();
|
|
30839
31093
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
30840
31094
|
import { existsSync as existsSync61, mkdirSync as mkdirSync28, readFileSync as readFileSync56, readdirSync as readdirSync25, renameSync as renameSync8, rmSync as rmSync10, statSync as statSync21, writeFileSync as writeFileSync28 } from "node:fs";
|
|
@@ -31097,7 +31351,7 @@ function realTestDeps() {
|
|
|
31097
31351
|
};
|
|
31098
31352
|
}
|
|
31099
31353
|
function defaultSmokeCmd(agent) {
|
|
31100
|
-
return agent
|
|
31354
|
+
return `${agentSmokeCommand(agent)}; sleep 10`;
|
|
31101
31355
|
}
|
|
31102
31356
|
function loopTestCommand(args = [], deps = realTestDeps()) {
|
|
31103
31357
|
const slug = deps.slug();
|
|
@@ -32353,6 +32607,7 @@ async function executeCommand(cmd, ports, ctx) {
|
|
|
32353
32607
|
const depsOk = await bootstrapWorktreeDeps(ports.paths.worktreePath, ports.paths.alertsPath, ports.events, ports.depsExec);
|
|
32354
32608
|
if (!depsOk)
|
|
32355
32609
|
return { event: { type: "worktree_failed" } };
|
|
32610
|
+
await bootstrapWorktreePrebuild(ports.paths.worktreePath, ports.paths.alertsPath, ports.events, readPrebuildDistEnabled(ports.repoCwd), ports.depsExec);
|
|
32356
32611
|
return { event: { type: "worktree_created" } };
|
|
32357
32612
|
}
|
|
32358
32613
|
// backlog/picker pickStory (read backlog INSIDE the worktree, bin/roll:8938).
|
|
@@ -32437,11 +32692,12 @@ async function executeCommand(cmd, ports, ctx) {
|
|
|
32437
32692
|
}
|
|
32438
32693
|
const captureSink = ctx.evidenceRunDir !== void 0 && ctx.evidenceRunDir !== "" ? createCaptureMarkerSink(ctx.evidenceRunDir, ports.capture) : void 0;
|
|
32439
32694
|
const observer = startCycleObserver(ports, ctx.cycleId ?? "");
|
|
32695
|
+
const skillBodyForSpawn = maybeInjectProjectMap(ports.skillBody, ports.paths.worktreePath, readProjectMapEnabled(ports.repoCwd), ctx.storyId);
|
|
32440
32696
|
let res;
|
|
32441
32697
|
try {
|
|
32442
32698
|
res = await ports.agentSpawn(cmd.agent, {
|
|
32443
32699
|
cwd: ports.paths.worktreePath,
|
|
32444
|
-
skillBody:
|
|
32700
|
+
skillBody: skillBodyForSpawn,
|
|
32445
32701
|
...ctx.evidenceRunDir !== void 0 ? { runDir: ctx.evidenceRunDir } : {},
|
|
32446
32702
|
writableRoots: agentWritableRoots(ports.repoCwd, ports.paths.alertsPath),
|
|
32447
32703
|
env: {
|
|
@@ -32485,16 +32741,17 @@ ${res.stderr}
|
|
|
32485
32741
|
try {
|
|
32486
32742
|
const agentName = ctx.agent ?? cmd.agent;
|
|
32487
32743
|
const lines2 = res.stdout.split("\n");
|
|
32488
|
-
|
|
32489
|
-
|
|
32744
|
+
const usageSpec = getAgentSpec(agentName)?.usage;
|
|
32745
|
+
let usage2 = usageSpec?.stdoutExtractor === "claude-stream" ? extractUsage(agentName, lines2) : null;
|
|
32746
|
+
if (usage2 === null && usageSpec?.sessionRecovery === "pi") {
|
|
32490
32747
|
const rootOverride = (process.env["ROLL_PI_SESSIONS_ROOT"] ?? "").trim();
|
|
32491
32748
|
usage2 = recoverPiUsage(ports.paths.worktreePath, ctx.startSec, ...rootOverride !== "" ? [rootOverride] : []);
|
|
32492
32749
|
}
|
|
32493
|
-
if (usage2 === null &&
|
|
32750
|
+
if (usage2 === null && usageSpec?.sessionRecovery === "kimi") {
|
|
32494
32751
|
const rootOverride = (process.env["ROLL_KIMI_SESSIONS_DIR"] ?? "").trim();
|
|
32495
32752
|
usage2 = recoverKimiUsage(ports.paths.worktreePath, ctx.startSec, ...rootOverride !== "" ? [rootOverride] : []);
|
|
32496
32753
|
}
|
|
32497
|
-
if (usage2 === null &&
|
|
32754
|
+
if (usage2 === null && usageSpec?.sessionRecovery === "codex") {
|
|
32498
32755
|
const rootOverride = (process.env["ROLL_CODEX_SESSIONS_DIR"] ?? "").trim();
|
|
32499
32756
|
usage2 = recoverCodexUsage(ports.paths.worktreePath, ctx.startSec, ...rootOverride !== "" ? [rootOverride] : []);
|
|
32500
32757
|
}
|
|
@@ -32998,7 +33255,10 @@ function buildTerminalRecord(cmd, ctx, attestCwd, nowSec2) {
|
|
|
32998
33255
|
failed: "failed",
|
|
32999
33256
|
blocked: "blocked",
|
|
33000
33257
|
aborted: "aborted_no_delivery",
|
|
33001
|
-
orphan: "aborted_with_delivery"
|
|
33258
|
+
orphan: "aborted_with_delivery",
|
|
33259
|
+
// FIX-351: gates passed but publish could not complete (work committed
|
|
33260
|
+
// locally, never published) — a neutral terminal, NOT a failure.
|
|
33261
|
+
local: "unpublished"
|
|
33002
33262
|
};
|
|
33003
33263
|
let attest;
|
|
33004
33264
|
if (storyId === "") {
|
|
@@ -33355,6 +33615,147 @@ async function bootstrapWorktreeDeps(worktreePath, alertsPath, events, exec = ex
|
|
|
33355
33615
|
return false;
|
|
33356
33616
|
}
|
|
33357
33617
|
}
|
|
33618
|
+
var PREBUILD_TIMEOUT_MS = 6e5;
|
|
33619
|
+
function readPrebuildDistEnabled(repoCwd) {
|
|
33620
|
+
try {
|
|
33621
|
+
const p = join66(repoCwd, ".roll", "policy.yaml");
|
|
33622
|
+
if (!existsSync66(p))
|
|
33623
|
+
return false;
|
|
33624
|
+
return parsePolicy(readFileSync62(p, "utf8")).loopSafety.prebuildDist === true;
|
|
33625
|
+
} catch {
|
|
33626
|
+
return false;
|
|
33627
|
+
}
|
|
33628
|
+
}
|
|
33629
|
+
async function bootstrapWorktreePrebuild(worktreePath, alertsPath, events, enabled, exec = execFileAsync9) {
|
|
33630
|
+
if (!enabled)
|
|
33631
|
+
return;
|
|
33632
|
+
if (!existsSync66(join66(worktreePath, "package.json")))
|
|
33633
|
+
return;
|
|
33634
|
+
if (!existsSync66(join66(worktreePath, "pnpm-lock.yaml")))
|
|
33635
|
+
return;
|
|
33636
|
+
try {
|
|
33637
|
+
await exec("pnpm", ["-r", "build"], {
|
|
33638
|
+
cwd: worktreePath,
|
|
33639
|
+
timeout: PREBUILD_TIMEOUT_MS,
|
|
33640
|
+
maxBuffer: 16 * 1024 * 1024
|
|
33641
|
+
});
|
|
33642
|
+
} catch (e) {
|
|
33643
|
+
const msg6 = e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
33644
|
+
events.appendAlert(alertsPath, `[WARN] worktree dist prebuild failed (pnpm -r build): ${msg6} \u2014 continuing; agent will build on demand`);
|
|
33645
|
+
}
|
|
33646
|
+
}
|
|
33647
|
+
function readProjectMapEnabled(repoCwd) {
|
|
33648
|
+
try {
|
|
33649
|
+
const p = join66(repoCwd, ".roll", "policy.yaml");
|
|
33650
|
+
if (!existsSync66(p))
|
|
33651
|
+
return false;
|
|
33652
|
+
return parsePolicy(readFileSync62(p, "utf8")).loopSafety.projectMap === true;
|
|
33653
|
+
} catch {
|
|
33654
|
+
return false;
|
|
33655
|
+
}
|
|
33656
|
+
}
|
|
33657
|
+
var PROJECT_MAP_MAX_CHARS = 1800;
|
|
33658
|
+
var PROJECT_MAP_MAX_TOPLEVEL = 24;
|
|
33659
|
+
var PROJECT_MAP_MAX_RELEVANT = 12;
|
|
33660
|
+
var PROJECT_MAP_SKIP = /* @__PURE__ */ new Set([
|
|
33661
|
+
".git",
|
|
33662
|
+
"node_modules",
|
|
33663
|
+
".vite",
|
|
33664
|
+
"dist",
|
|
33665
|
+
"coverage",
|
|
33666
|
+
".turbo",
|
|
33667
|
+
".cache",
|
|
33668
|
+
".DS_Store"
|
|
33669
|
+
]);
|
|
33670
|
+
var PROJECT_MAP_CONTAINERS = /* @__PURE__ */ new Set(["packages", "apps", "skills"]);
|
|
33671
|
+
function shallowDirents(dir) {
|
|
33672
|
+
return readdirSync28(dir, { withFileTypes: true });
|
|
33673
|
+
}
|
|
33674
|
+
function shallowList(dir, limit) {
|
|
33675
|
+
try {
|
|
33676
|
+
const out3 = [];
|
|
33677
|
+
for (const ent of shallowDirents(dir)) {
|
|
33678
|
+
if (PROJECT_MAP_SKIP.has(ent.name))
|
|
33679
|
+
continue;
|
|
33680
|
+
out3.push(ent.isDirectory() ? `${ent.name}/` : ent.name);
|
|
33681
|
+
}
|
|
33682
|
+
out3.sort((a, b) => a.localeCompare(b));
|
|
33683
|
+
return out3.slice(0, limit);
|
|
33684
|
+
} catch {
|
|
33685
|
+
return [];
|
|
33686
|
+
}
|
|
33687
|
+
}
|
|
33688
|
+
function findRelevantFiles(root, token, limit) {
|
|
33689
|
+
const needle = token.toLowerCase();
|
|
33690
|
+
const hits = [];
|
|
33691
|
+
let scanned = 0;
|
|
33692
|
+
const maxScan = 4e3;
|
|
33693
|
+
const walk2 = (dir, rel) => {
|
|
33694
|
+
if (hits.length >= limit || scanned >= maxScan)
|
|
33695
|
+
return;
|
|
33696
|
+
let ents;
|
|
33697
|
+
try {
|
|
33698
|
+
ents = shallowDirents(dir);
|
|
33699
|
+
} catch {
|
|
33700
|
+
return;
|
|
33701
|
+
}
|
|
33702
|
+
for (const ent of ents) {
|
|
33703
|
+
if (hits.length >= limit || scanned >= maxScan)
|
|
33704
|
+
return;
|
|
33705
|
+
if (PROJECT_MAP_SKIP.has(ent.name))
|
|
33706
|
+
continue;
|
|
33707
|
+
scanned += 1;
|
|
33708
|
+
const childRel = rel === "" ? ent.name : `${rel}/${ent.name}`;
|
|
33709
|
+
if (ent.isDirectory()) {
|
|
33710
|
+
walk2(join66(dir, ent.name), childRel);
|
|
33711
|
+
} else if (childRel.toLowerCase().includes(needle)) {
|
|
33712
|
+
hits.push(childRel);
|
|
33713
|
+
}
|
|
33714
|
+
}
|
|
33715
|
+
};
|
|
33716
|
+
walk2(root, "");
|
|
33717
|
+
hits.sort((a, b) => a.localeCompare(b));
|
|
33718
|
+
return hits.slice(0, limit);
|
|
33719
|
+
}
|
|
33720
|
+
function buildProjectMap(worktreePath, storyId) {
|
|
33721
|
+
const top = shallowList(worktreePath, PROJECT_MAP_MAX_TOPLEVEL);
|
|
33722
|
+
if (top.length === 0)
|
|
33723
|
+
return "";
|
|
33724
|
+
const lines2 = ["[\u9879\u76EE\u5730\u56FE / project map]", "\u7ED3\u6784 / structure:"];
|
|
33725
|
+
for (const name of top) {
|
|
33726
|
+
lines2.push(` ${name}`);
|
|
33727
|
+
const bare = name.replace(/\/$/, "");
|
|
33728
|
+
if (name.endsWith("/") && PROJECT_MAP_CONTAINERS.has(bare)) {
|
|
33729
|
+
for (const child of shallowList(join66(worktreePath, bare), PROJECT_MAP_MAX_TOPLEVEL)) {
|
|
33730
|
+
lines2.push(` ${child}`);
|
|
33731
|
+
}
|
|
33732
|
+
}
|
|
33733
|
+
}
|
|
33734
|
+
const token = (storyId ?? "").trim();
|
|
33735
|
+
if (token.length >= 3) {
|
|
33736
|
+
const relevant = findRelevantFiles(worktreePath, token, PROJECT_MAP_MAX_RELEVANT);
|
|
33737
|
+
if (relevant.length > 0) {
|
|
33738
|
+
lines2.push(`\u672C\u5361\u76F8\u5173\u6587\u4EF6 / files matching ${token}:`);
|
|
33739
|
+
for (const f of relevant)
|
|
33740
|
+
lines2.push(` ${f}`);
|
|
33741
|
+
}
|
|
33742
|
+
}
|
|
33743
|
+
let map = lines2.join("\n");
|
|
33744
|
+
if (map.length > PROJECT_MAP_MAX_CHARS) {
|
|
33745
|
+
map = `${map.slice(0, PROJECT_MAP_MAX_CHARS - 3)}...`;
|
|
33746
|
+
}
|
|
33747
|
+
return map;
|
|
33748
|
+
}
|
|
33749
|
+
function maybeInjectProjectMap(skillBody, worktreePath, enabled, storyId) {
|
|
33750
|
+
if (!enabled)
|
|
33751
|
+
return skillBody;
|
|
33752
|
+
const map = buildProjectMap(worktreePath, storyId);
|
|
33753
|
+
if (map === "")
|
|
33754
|
+
return skillBody;
|
|
33755
|
+
return `${map}
|
|
33756
|
+
|
|
33757
|
+
${skillBody}`;
|
|
33758
|
+
}
|
|
33358
33759
|
function skillsEntryCount(worktreePath) {
|
|
33359
33760
|
const dir = join66(worktreePath, "skills");
|
|
33360
33761
|
if (!existsSync66(dir))
|
|
@@ -34219,7 +34620,13 @@ function readField(path, re) {
|
|
|
34219
34620
|
}
|
|
34220
34621
|
return void 0;
|
|
34221
34622
|
}
|
|
34623
|
+
var RUN_ONCE_USAGE = "Usage: roll loop run-once [--dry-run]\n Run ONE loop cycle now: pick a Todo card, build it through TCR, run the\n gates (attest + peer), and publish a PR. Exits when the cycle terminates.\n --dry-run Print the command plan only \u2014 no git / gh / agent side effects.\n\u7ACB\u5373\u8DD1\u4E00\u4E2A loop \u5468\u671F:\u9009\u4E00\u5F20 Todo \u5361,\u7ECF TCR \u5EFA\u9020,\u8FC7\u95F8(\u9A8C\u6536+\u540C\u884C\u8BC4\u5BA1),\u53D1 PR\u3002\n --dry-run \u53EA\u6253\u5370\u547D\u4EE4\u8BA1\u5212\u2014\u2014\u4E0D\u52A8 git / gh / agent\u3002";
|
|
34222
34624
|
async function loopRunOnceCommand(args) {
|
|
34625
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
34626
|
+
process.stdout.write(`${RUN_ONCE_USAGE}
|
|
34627
|
+
`);
|
|
34628
|
+
return 0;
|
|
34629
|
+
}
|
|
34223
34630
|
const dryRun = args.includes("--dry-run");
|
|
34224
34631
|
const id = await projectIdentity();
|
|
34225
34632
|
const cycleId = makeCycleId();
|
|
@@ -34339,6 +34746,12 @@ loop run-once: \u627E\u4E0D\u5230 roll-loop SKILL.md \u2014 \u62D2\u7EDD\u76F2\u
|
|
|
34339
34746
|
if (result.terminal === "published") {
|
|
34340
34747
|
process.stdout.write("loop run-once: delivery published \u2014 PR open, merge pending (PR loop merges; backfill credits on merge evidence)\nloop run-once: \u4EA4\u4ED8\u5DF2\u53D1\u5E03\u2014\u2014PR \u5DF2\u5F00,\u7B49\u5F85\u5408\u5E76(PR loop \u8D1F\u8D23\u5408\u5E76;\u5408\u5E76\u8BC1\u636E\u843D\u5730\u540E\u7531\u56DE\u586B\u8BB0\u8D26)\n");
|
|
34341
34748
|
}
|
|
34749
|
+
if (result.terminal === "local") {
|
|
34750
|
+
const storyId = (result.state?.ctx?.storyId ?? "").trim();
|
|
34751
|
+
announceReport(id.path, id.slug, storyId);
|
|
34752
|
+
resetConsecutiveFails(id.path);
|
|
34753
|
+
process.stdout.write("loop run-once: gates passed but publish did not complete \u2014 work committed locally on the branch, not published (unpublished, not a failure)\nloop run-once: \u95F8\u5DF2\u901A\u8FC7\u4F46\u672A\u5B8C\u6210\u53D1\u5E03\u2014\u2014\u5DE5\u4F5C\u5DF2\u5728\u5206\u652F\u4E0A\u672C\u5730\u63D0\u4EA4,\u5C1A\u672A\u53D1\u5E03(\u672A\u53D1\u5E03,\u975E\u5931\u8D25)\n");
|
|
34754
|
+
}
|
|
34342
34755
|
const isFail = result.terminal === "failed" || result.terminal === "blocked";
|
|
34343
34756
|
if (isFail) {
|
|
34344
34757
|
if (await isOffline()) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanyao/roll",
|
|
3
|
-
"version": "3.617.
|
|
3
|
+
"version": "3.617.2",
|
|
4
4
|
"description": "Roll — Roll out features with AI agents",
|
|
5
5
|
"packageManager": "pnpm@11.1.3",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "bash scripts/test-ts.sh",
|
|
8
|
+
"lint:story-ids": "node scripts/lint-story-ids.mjs",
|
|
8
9
|
"test:e2e": "pnpm -r build && pnpm --filter @roll/cli exec vitest run test/critical-flows.e2e.test.ts",
|
|
9
10
|
"test:cov": "pnpm -r exec vitest run --coverage.enabled --coverage.provider=v8 --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=text",
|
|
10
11
|
"bundle": "node scripts/bundle.mjs",
|