@seanyao/roll 3.614.1 → 3.614.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
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v3.614.3 — 2026-06-14
|
|
6
|
+
|
|
7
|
+
### 改进
|
|
8
|
+
|
|
9
|
+
- **设计技能更严**:`roll-design` 现在把"详细设计"做成一道硬步骤——拆任务之前,必须先拿出能照着写的方案(数据结构、**至少一个完整样例**、接口、映射规则、边界情况)并经确认。规矩一句话:拿不出一个完整样例,就还没设计完。(US-SKILL-029)
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
|
|
13
|
+
- 失败/空转的一轮循环现在也记下用了哪个模型:之前那条记录里模型是空的(只补过另一处、这条漏了),导致实时窗和循环账本看不到是哪个 AI 在跑;现在哪个都记上。(FIX-294)
|
|
14
|
+
|
|
15
|
+
## v3.614.2 — 2026-06-14
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- `roll loop go` 启动时会给清楚的反馈了:告诉你起的是哪个会话、在做哪些卡、第一轮起没起、怎么只读地看——之前只甩一句模糊的 "started" 就退出,你根本不知道开没开。会话复用时也会补上观察窗,attach 进去能直接看到实时流、而不是落在干活的窗口里。还能 `--attach` 直接在前台跟住实时流(此时 Ctrl-C 只停看、不停循环)。(FIX-289)
|
|
20
|
+
- 失败/空转的一轮循环现在也记下用了哪个模型、烧了多少 token、花了多少钱:之前账本那行只剩时间、其余全是 —;读不到用量时如实标"未知",不再假装成 0。而且每一轮结束(不管成没成)都会刷新项目网页,失败的循环不再在循环页里隐身。(FIX-290)
|
|
21
|
+
- 网页截图取证不再因为"本地没装 Chromium"就悄悄降级成纯文本:有图形界面时直接用系统截屏(打开浏览器截真窗口、真像素),没界面才用无头浏览器,都不行才如实记"没截成"——绝不拿文本冒充截图。(FIX-291)
|
|
22
|
+
|
|
5
23
|
## v3.614.1 — 2026-06-14
|
|
6
24
|
|
|
7
25
|
### 新功能
|
package/dist/roll.mjs
CHANGED
|
@@ -2324,6 +2324,7 @@ function buildTerminalEvent(input) {
|
|
|
2324
2324
|
cycleId: input.cycleId,
|
|
2325
2325
|
storyId: input.storyId,
|
|
2326
2326
|
agent: input.agent,
|
|
2327
|
+
model: input.model ?? "",
|
|
2327
2328
|
startedAt: input.startedAt,
|
|
2328
2329
|
endedAt: input.endedAt,
|
|
2329
2330
|
outcome: input.outcome,
|
|
@@ -2412,6 +2413,10 @@ var init_truth_registry = __esm({
|
|
|
2412
2413
|
{ field: "cycleId", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
|
|
2413
2414
|
{ field: "storyId", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
|
|
2414
2415
|
{ field: "agent", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
|
|
2416
|
+
// FIX-294: routed model is a dispatch-time fact (like agent) — authoritative,
|
|
2417
|
+
// ALWAYS present even when usage couldn't be parsed; the `usage` fact below
|
|
2418
|
+
// still owns the present-or-reasoned token/cost truth.
|
|
2419
|
+
{ field: "model", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
|
|
2415
2420
|
{ field: "startedAt", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
|
|
2416
2421
|
{ field: "endedAt", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
|
|
2417
2422
|
{ field: "outcome", surface: "event:cycle:terminal", anchor: "cycle_outcome", writer: "buildTerminalEvent", kind: "authoritative" },
|
|
@@ -9452,6 +9457,10 @@ function terminalBoundsScript(title) {
|
|
|
9452
9457
|
"end tell"
|
|
9453
9458
|
].join("\n");
|
|
9454
9459
|
}
|
|
9460
|
+
async function hasGuiSession(run) {
|
|
9461
|
+
const gui = await run("launchctl", ["managername"]);
|
|
9462
|
+
return gui.code === 0 && gui.stdout.includes("Aqua");
|
|
9463
|
+
}
|
|
9455
9464
|
async function resolveWindowRect(title, run) {
|
|
9456
9465
|
const r = await run("osascript", ["-e", terminalBoundsScript(title)]);
|
|
9457
9466
|
if (r.code !== 0)
|
|
@@ -9464,6 +9473,89 @@ async function resolveWindowRect(title, run) {
|
|
|
9464
9473
|
return null;
|
|
9465
9474
|
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
|
|
9466
9475
|
}
|
|
9476
|
+
function browserOpenScript(target, browserApp, r) {
|
|
9477
|
+
const app = appleScriptString(browserApp);
|
|
9478
|
+
const url = appleScriptString(target);
|
|
9479
|
+
return [
|
|
9480
|
+
`tell application "${app}"`,
|
|
9481
|
+
" activate",
|
|
9482
|
+
" make new window",
|
|
9483
|
+
" try",
|
|
9484
|
+
` set URL of active tab of front window to "${url}"`,
|
|
9485
|
+
" on error",
|
|
9486
|
+
" try",
|
|
9487
|
+
` set URL of front document to "${url}"`,
|
|
9488
|
+
" end try",
|
|
9489
|
+
" end try",
|
|
9490
|
+
` delay ${BROWSER_RENDER_DELAY_S}`,
|
|
9491
|
+
" try",
|
|
9492
|
+
` set bounds of front window to {${r.x}, ${r.y}, ${r.x + r.w}, ${r.y + r.h}}`,
|
|
9493
|
+
" end try",
|
|
9494
|
+
"end tell"
|
|
9495
|
+
].join("\n");
|
|
9496
|
+
}
|
|
9497
|
+
function browserBoundsScript(browserApp) {
|
|
9498
|
+
const app = appleScriptString(browserApp);
|
|
9499
|
+
return [
|
|
9500
|
+
`tell application "${app}"`,
|
|
9501
|
+
" try",
|
|
9502
|
+
" activate",
|
|
9503
|
+
" set index of front window to 1",
|
|
9504
|
+
" delay 0.3",
|
|
9505
|
+
" return bounds of front window",
|
|
9506
|
+
" on error",
|
|
9507
|
+
' return ""',
|
|
9508
|
+
" end try",
|
|
9509
|
+
"end tell"
|
|
9510
|
+
].join("\n");
|
|
9511
|
+
}
|
|
9512
|
+
function browserCloseScript(browserApp) {
|
|
9513
|
+
const app = appleScriptString(browserApp);
|
|
9514
|
+
return [
|
|
9515
|
+
`tell application "${app}"`,
|
|
9516
|
+
" try",
|
|
9517
|
+
" close front window",
|
|
9518
|
+
" end try",
|
|
9519
|
+
"end tell"
|
|
9520
|
+
].join("\n");
|
|
9521
|
+
}
|
|
9522
|
+
async function resolveBrowserRect(browserApp, run) {
|
|
9523
|
+
const r = await run("osascript", ["-e", browserBoundsScript(browserApp)]);
|
|
9524
|
+
if (r.code !== 0)
|
|
9525
|
+
return null;
|
|
9526
|
+
const parts = r.stdout.trim().split(",").map((part) => Number(part.trim()));
|
|
9527
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n)))
|
|
9528
|
+
return null;
|
|
9529
|
+
const [x1, y1, x2, y2] = parts;
|
|
9530
|
+
if (x2 <= x1 || y2 <= y1)
|
|
9531
|
+
return null;
|
|
9532
|
+
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
|
|
9533
|
+
}
|
|
9534
|
+
async function captureWebViaBrowser(req, run) {
|
|
9535
|
+
const browserApp = req.browser ?? DEFAULT_BROWSER;
|
|
9536
|
+
const rect = parseRegion(req.region ?? DEFAULT_REGION);
|
|
9537
|
+
if (rect === null)
|
|
9538
|
+
return "bad region";
|
|
9539
|
+
const target = req.url;
|
|
9540
|
+
const opened = await run("osascript", ["-e", browserOpenScript(target, browserApp, rect)]);
|
|
9541
|
+
if (opened.code !== 0)
|
|
9542
|
+
return `osascript ${browserApp} open failed`;
|
|
9543
|
+
const liveRect = await resolveBrowserRect(browserApp, run);
|
|
9544
|
+
if (liveRect === null) {
|
|
9545
|
+
await run("osascript", ["-e", browserCloseScript(browserApp)]);
|
|
9546
|
+
return "browser window not found \u2014 refusing a blind-region shot";
|
|
9547
|
+
}
|
|
9548
|
+
const front = await run("sh", ["-c", "lsappinfo info -only name $(lsappinfo front)"]);
|
|
9549
|
+
if (front.code !== 0 || !front.stdout.includes(browserApp)) {
|
|
9550
|
+
await run("osascript", ["-e", browserCloseScript(browserApp)]);
|
|
9551
|
+
return `${browserApp} not frontmost \u2014 refusing to shoot another app's pixels`;
|
|
9552
|
+
}
|
|
9553
|
+
const shot = await run("screencapture", ["-x", "-R", `${liveRect.x},${liveRect.y},${liveRect.w},${liveRect.h}`, req.out]);
|
|
9554
|
+
await run("osascript", ["-e", browserCloseScript(browserApp)]);
|
|
9555
|
+
if (shot.code !== 0)
|
|
9556
|
+
return "screencapture failed (screen-recording permission?)";
|
|
9557
|
+
return null;
|
|
9558
|
+
}
|
|
9467
9559
|
function terminalExitTabScript(title) {
|
|
9468
9560
|
const esc8 = appleScriptString(title);
|
|
9469
9561
|
return [
|
|
@@ -9550,9 +9642,17 @@ async function captureScreenshot(req, deps = {}) {
|
|
|
9550
9642
|
return skip("ROLL_ATTEST_NO_BROWSER=1");
|
|
9551
9643
|
if (req.url === void 0 || req.url === "")
|
|
9552
9644
|
return skip("no url");
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9645
|
+
if (platform3 === "darwin" && await hasGuiSession(run)) {
|
|
9646
|
+
const reason = await captureWebViaBrowser(req, run);
|
|
9647
|
+
if (reason !== null)
|
|
9648
|
+
return skip(`GUI browser capture: ${reason}`);
|
|
9649
|
+
} else {
|
|
9650
|
+
const r = await run("npx", ["-y", "playwright@latest", "screenshot", req.url, req.out]);
|
|
9651
|
+
if (r.code !== 0) {
|
|
9652
|
+
const why = platform3 === "darwin" ? "no GUI session" : "non-macOS host";
|
|
9653
|
+
return skip(`headless Chromium unavailable or capture failed (${why}; screencapture fallback not applicable)`);
|
|
9654
|
+
}
|
|
9655
|
+
}
|
|
9556
9656
|
} else if (req.kind === "mobile-ios") {
|
|
9557
9657
|
if (platform3 !== "darwin")
|
|
9558
9658
|
return skip("not macOS");
|
|
@@ -9578,8 +9678,7 @@ async function captureScreenshot(req, deps = {}) {
|
|
|
9578
9678
|
const rawLine = req.tmux !== void 0 && req.tmux !== "" ? `tmux attach -t ${req.tmux}` : req.command ?? "";
|
|
9579
9679
|
if (containsSecret(rawLine))
|
|
9580
9680
|
return skip("secret in capture command \u2014 redact & reshoot");
|
|
9581
|
-
|
|
9582
|
-
if (gui.code !== 0 || !gui.stdout.includes("Aqua"))
|
|
9681
|
+
if (!await hasGuiSession(run))
|
|
9583
9682
|
return skip("no GUI session");
|
|
9584
9683
|
const rect = parseRegion(req.region ?? DEFAULT_REGION);
|
|
9585
9684
|
if (rect === null)
|
|
@@ -9638,7 +9737,7 @@ async function captureAll(reqs, deps = {}) {
|
|
|
9638
9737
|
out3.push(await captureScreenshot(r, deps));
|
|
9639
9738
|
return out3;
|
|
9640
9739
|
}
|
|
9641
|
-
var execFileAsync7, defaultRun2, SAFE_STEM, DEFAULT_REGION, TERMINAL_DONE_TIMEOUT_MS, TERMINAL_DONE_POLL_MS;
|
|
9740
|
+
var execFileAsync7, defaultRun2, SAFE_STEM, DEFAULT_REGION, TERMINAL_DONE_TIMEOUT_MS, TERMINAL_DONE_POLL_MS, DEFAULT_BROWSER, BROWSER_RENDER_DELAY_S;
|
|
9642
9741
|
var init_screenshot = __esm({
|
|
9643
9742
|
"packages/infra/dist/screenshot.js"() {
|
|
9644
9743
|
"use strict";
|
|
@@ -9661,6 +9760,8 @@ var init_screenshot = __esm({
|
|
|
9661
9760
|
DEFAULT_REGION = "0,0,1280,800";
|
|
9662
9761
|
TERMINAL_DONE_TIMEOUT_MS = 12e4;
|
|
9663
9762
|
TERMINAL_DONE_POLL_MS = 100;
|
|
9763
|
+
DEFAULT_BROWSER = "Google Chrome";
|
|
9764
|
+
BROWSER_RENDER_DELAY_S = 2;
|
|
9664
9765
|
}
|
|
9665
9766
|
});
|
|
9666
9767
|
|
|
@@ -9684,6 +9785,9 @@ __export(dist_exports, {
|
|
|
9684
9785
|
attachArgv: () => attachArgv,
|
|
9685
9786
|
branchCreate: () => branchCreate,
|
|
9686
9787
|
branchDelete: () => branchDelete,
|
|
9788
|
+
browserBoundsScript: () => browserBoundsScript,
|
|
9789
|
+
browserCloseScript: () => browserCloseScript,
|
|
9790
|
+
browserOpenScript: () => browserOpenScript,
|
|
9687
9791
|
canonicalProjectPath: () => canonicalProjectPath,
|
|
9688
9792
|
captureAll: () => captureAll,
|
|
9689
9793
|
captureFromMarker: () => captureFromMarker,
|
|
@@ -17137,7 +17241,9 @@ function ledgerVerdict(status2, outcome) {
|
|
|
17137
17241
|
function ledgerFailedCount(rows) {
|
|
17138
17242
|
return rows.filter((r) => r.verdict === "failed" || r.verdict === "reverted" || r.verdict === "blocked").length;
|
|
17139
17243
|
}
|
|
17140
|
-
function fmtTokens2(tin, tout) {
|
|
17244
|
+
function fmtTokens2(tin, tout, usageUnknown) {
|
|
17245
|
+
if (usageUnknown)
|
|
17246
|
+
return "?";
|
|
17141
17247
|
const a = typeof tin === "number" ? tin : 0;
|
|
17142
17248
|
const b = typeof tout === "number" ? tout : 0;
|
|
17143
17249
|
if (a + b === 0)
|
|
@@ -17241,6 +17347,7 @@ function collectCycleLedger(projectPath3) {
|
|
|
17241
17347
|
const rawTs = row2["ts"];
|
|
17242
17348
|
const ts = typeof rawTs === "string" ? Date.parse(rawTs) : typeof rawTs === "number" ? rawTs > 1e10 ? rawTs : rawTs * 1e3 : Number.NaN;
|
|
17243
17349
|
const cost = typeof row2["cost_effective_usd"] === "number" ? row2["cost_effective_usd"] : typeof row2["cost_usd"] === "number" ? row2["cost_usd"] : void 0;
|
|
17350
|
+
const usageUnknown = row2["usage_unknown"] === true;
|
|
17244
17351
|
const ev = byCycle.get(cycleId);
|
|
17245
17352
|
const prNumber = storyId !== "" ? prMergedBy.get(storyId) : void 0;
|
|
17246
17353
|
const prOpen = storyId !== "" ? prOpenBy.get(storyId) : void 0;
|
|
@@ -17254,8 +17361,8 @@ function collectCycleLedger(projectPath3) {
|
|
|
17254
17361
|
storyId,
|
|
17255
17362
|
agent: String(row2["agent"] ?? ""),
|
|
17256
17363
|
model: typeof row2["model"] === "string" && row2["model"] !== "" ? row2["model"] : String(row2["agent"] ?? "\u2014") || "\u2014",
|
|
17257
|
-
tokens: fmtTokens2(row2["tokens_in"], row2["tokens_out"]),
|
|
17258
|
-
cost: cost !== void 0 ? `$${cost.toFixed(2)}` : "\u2014",
|
|
17364
|
+
tokens: fmtTokens2(row2["tokens_in"], row2["tokens_out"], usageUnknown),
|
|
17365
|
+
cost: cost !== void 0 ? `$${cost.toFixed(2)}` : usageUnknown ? "?" : "\u2014",
|
|
17259
17366
|
duration: fmtDuration(row2["duration_sec"]),
|
|
17260
17367
|
tape: rowTape(row2, verdict, ev, prNumber, prOpen),
|
|
17261
17368
|
evidence
|
|
@@ -23605,6 +23712,8 @@ function pad3(s, w) {
|
|
|
23605
23712
|
function tokensTotal(tokens) {
|
|
23606
23713
|
if (tokens === "\u2014")
|
|
23607
23714
|
return "\u2014";
|
|
23715
|
+
if (tokens === "?")
|
|
23716
|
+
return "?";
|
|
23608
23717
|
const parts = tokens.split("/");
|
|
23609
23718
|
const num4 = (p) => p.endsWith("k") ? Number(p.slice(0, -1)) * 1e3 : Number(p);
|
|
23610
23719
|
const total = parts.reduce((a, p) => a + (Number.isFinite(num4(p)) ? num4(p) : 0), 0);
|
|
@@ -24728,6 +24837,7 @@ function realDeps4() {
|
|
|
24728
24837
|
}
|
|
24729
24838
|
},
|
|
24730
24839
|
startTmux: startGoTmux,
|
|
24840
|
+
followFeed: followGoLiveFeed,
|
|
24731
24841
|
runOnce: realRunOnce,
|
|
24732
24842
|
readUsageLimits: readAnthropicUsageLimits,
|
|
24733
24843
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
@@ -24781,6 +24891,7 @@ function parseOptions2(args) {
|
|
|
24781
24891
|
let worker = false;
|
|
24782
24892
|
let noTmux = false;
|
|
24783
24893
|
let noWait = false;
|
|
24894
|
+
let attach = false;
|
|
24784
24895
|
let scopeSpecified = false;
|
|
24785
24896
|
let reviewMode = "auto";
|
|
24786
24897
|
let reviewModeSpecified = false;
|
|
@@ -24798,6 +24909,10 @@ function parseOptions2(args) {
|
|
|
24798
24909
|
noWait = true;
|
|
24799
24910
|
continue;
|
|
24800
24911
|
}
|
|
24912
|
+
if (arg === "--attach" || arg === "--follow") {
|
|
24913
|
+
attach = true;
|
|
24914
|
+
continue;
|
|
24915
|
+
}
|
|
24801
24916
|
if (arg === "--epic") {
|
|
24802
24917
|
const epic = args[i + 1]?.trim() ?? "";
|
|
24803
24918
|
if (epic !== "") {
|
|
@@ -24882,6 +24997,7 @@ function parseOptions2(args) {
|
|
|
24882
24997
|
worker,
|
|
24883
24998
|
noTmux,
|
|
24884
24999
|
noWait,
|
|
25000
|
+
attach,
|
|
24885
25001
|
scope,
|
|
24886
25002
|
scopeSpecified,
|
|
24887
25003
|
reviewMode,
|
|
@@ -24897,7 +25013,7 @@ function hasHelpArg2(args) {
|
|
|
24897
25013
|
}
|
|
24898
25014
|
function loopGoHelp() {
|
|
24899
25015
|
return [
|
|
24900
|
-
"Usage: roll loop go [--epic <name>|--cards <ids>] [--budget <usd>] [--for <duration>] [--max-cycles <n>] [--review <auto|hetero|self|off>] [--no-wait] [--no-tmux]",
|
|
25016
|
+
"Usage: roll loop go [--epic <name>|--cards <ids>] [--budget <usd>] [--for <duration>] [--max-cycles <n>] [--review <auto|hetero|self|off>] [--no-wait] [--attach] [--no-tmux]",
|
|
24901
25017
|
" Chain goal-mode cycles until the scoped backlog is complete, paused, budget-limited, or capped.",
|
|
24902
25018
|
" \u6309 goal \u8303\u56F4\u8FDE\u7EED\u6267\u884C cycle\uFF0C\u76F4\u5230\u5B8C\u6210\u3001\u6682\u505C\u3001\u9884\u7B97\u53D7\u9650\u6216\u8FBE\u5230\u4E0A\u9650\u3002",
|
|
24903
25019
|
"",
|
|
@@ -24910,6 +25026,7 @@ function loopGoHelp() {
|
|
|
24910
25026
|
" --usage-threshold <ratio> Pause when account usage reaches this ratio; default 0.85.",
|
|
24911
25027
|
" --no-wait Do not wait for usage windows to recover after a usage-limit pause.",
|
|
24912
25028
|
" --review <mode> Final review policy before completion: auto, hetero, self, or off.",
|
|
25029
|
+
" --attach, --follow Start the session, then follow the read-only live feed in the foreground (Ctrl-C stops the view, not the loop).",
|
|
24913
25030
|
" --no-tmux Run in the current process instead of starting a tmux session.",
|
|
24914
25031
|
"",
|
|
24915
25032
|
"Limits are explicit per run (FIX-279):",
|
|
@@ -25000,21 +25117,76 @@ function shellQuote3(value) {
|
|
|
25000
25117
|
function rollBin() {
|
|
25001
25118
|
return (process.env["ROLL_BIN"] ?? "").trim() || process.argv[1] || "roll";
|
|
25002
25119
|
}
|
|
25120
|
+
function goSessionName(slug) {
|
|
25121
|
+
return `roll-loop-${slug}`;
|
|
25122
|
+
}
|
|
25123
|
+
function watchCommand(projectPath3, slug, rollBin3) {
|
|
25124
|
+
const live = shellQuote3(join44(runtimeDir2(projectPath3), "live.log"));
|
|
25125
|
+
return `printf 'roll goal \xB7 ${slug}\\n'; tail -n +1 -F ${live} | ${shellQuote3(rollBin3)} loop fmt`;
|
|
25126
|
+
}
|
|
25127
|
+
function planGoTmuxCommands(input, state) {
|
|
25128
|
+
const session = goSessionName(input.slug);
|
|
25129
|
+
const watch = watchCommand(input.projectPath, input.slug, input.rollBin);
|
|
25130
|
+
const workerArgs = input.args.filter((arg) => arg !== "--worker" && arg !== "--no-tmux" && arg !== "--attach" && arg !== "--follow").concat("--worker").map(shellQuote3).join(" ");
|
|
25131
|
+
const command = `cd ${shellQuote3(input.projectPath)} && ROLL_LOOP_GO_WORKER=1 ROLL_LOOP_NO_TMUX=1 ROLL_BIN=${shellQuote3(input.rollBin)} ${shellQuote3(input.rollBin)} loop go ${workerArgs}`;
|
|
25132
|
+
const plan = [];
|
|
25133
|
+
if (!state.sessionExists) {
|
|
25134
|
+
plan.push(["new-session", "-d", "-s", session, "-x", "200", "-y", "50", "-n", "watch", watch]);
|
|
25135
|
+
} else if (!state.watchWindowExists) {
|
|
25136
|
+
plan.push(["new-window", "-d", "-t", session, "-n", "watch", watch]);
|
|
25137
|
+
}
|
|
25138
|
+
plan.push(["new-window", "-d", "-t", session, "-n", "go", command]);
|
|
25139
|
+
return plan;
|
|
25140
|
+
}
|
|
25141
|
+
function probeGoTmuxState(slug) {
|
|
25142
|
+
const session = goSessionName(slug);
|
|
25143
|
+
const sessionExists = spawnSync4("tmux", ["has-session", "-t", session], { stdio: "ignore" }).status === 0;
|
|
25144
|
+
if (!sessionExists)
|
|
25145
|
+
return { sessionExists: false, watchWindowExists: false };
|
|
25146
|
+
const listed = spawnSync4("tmux", ["list-windows", "-t", session, "-F", "#{window_name}"], { encoding: "utf8" });
|
|
25147
|
+
const windows = listed.status === 0 ? String(listed.stdout ?? "").split("\n").map((w) => w.trim()) : [];
|
|
25148
|
+
return { sessionExists: true, watchWindowExists: windows.includes("watch") };
|
|
25149
|
+
}
|
|
25003
25150
|
function startGoTmux(input) {
|
|
25004
|
-
const session =
|
|
25005
|
-
const rt = runtimeDir2(input.projectPath);
|
|
25006
|
-
const workerArgs = input.args.filter((arg) => arg !== "--worker" && arg !== "--no-tmux").concat("--worker").map(shellQuote3).join(" ");
|
|
25007
|
-
const watch = `printf 'roll goal \xB7 ${input.slug}\\n'; tail -n +1 -F ${shellQuote3(join44(rt, "live.log"))} | ${shellQuote3(input.rollBin)} loop fmt`;
|
|
25151
|
+
const session = goSessionName(input.slug);
|
|
25008
25152
|
try {
|
|
25009
|
-
|
|
25010
|
-
|
|
25153
|
+
const plan = planGoTmuxCommands(input, probeGoTmuxState(input.slug));
|
|
25154
|
+
let goStarted = false;
|
|
25155
|
+
for (const argv of plan) {
|
|
25156
|
+
const ok8 = spawnSync4("tmux", argv, { stdio: "ignore" }).status === 0;
|
|
25157
|
+
if (argv[0] === "new-window" && argv.includes("go") && argv[argv.length - 1]?.startsWith("cd "))
|
|
25158
|
+
goStarted = ok8;
|
|
25011
25159
|
}
|
|
25012
|
-
|
|
25013
|
-
return spawnSync4("tmux", ["new-window", "-d", "-t", session, "-n", "go", command], { stdio: "ignore" }).status === 0;
|
|
25160
|
+
return goStarted;
|
|
25014
25161
|
} catch {
|
|
25015
25162
|
return false;
|
|
25016
25163
|
}
|
|
25017
25164
|
}
|
|
25165
|
+
function followGoLiveFeed(projectPath3, rollBin3) {
|
|
25166
|
+
return new Promise((resolve5) => {
|
|
25167
|
+
const live = join44(runtimeDir2(projectPath3), "live.log");
|
|
25168
|
+
const tail3 = spawn3("tail", ["-n", "+1", "-F", live], { stdio: ["ignore", "pipe", "inherit"] });
|
|
25169
|
+
const fmt = spawn3(rollBin3.endsWith(".js") || rollBin3.endsWith(".mjs") ? process.execPath : rollBin3, rollBin3.endsWith(".js") || rollBin3.endsWith(".mjs") ? [rollBin3, "loop", "fmt"] : ["loop", "fmt"], { stdio: ["pipe", "inherit", "inherit"] });
|
|
25170
|
+
if (tail3.stdout !== null && fmt.stdin !== null)
|
|
25171
|
+
tail3.stdout.pipe(fmt.stdin);
|
|
25172
|
+
const finish = () => {
|
|
25173
|
+
try {
|
|
25174
|
+
tail3.kill("SIGTERM");
|
|
25175
|
+
} catch {
|
|
25176
|
+
}
|
|
25177
|
+
try {
|
|
25178
|
+
fmt.kill("SIGTERM");
|
|
25179
|
+
} catch {
|
|
25180
|
+
}
|
|
25181
|
+
process.removeListener("SIGINT", finish);
|
|
25182
|
+
resolve5();
|
|
25183
|
+
};
|
|
25184
|
+
process.on("SIGINT", finish);
|
|
25185
|
+
fmt.on("exit", finish);
|
|
25186
|
+
tail3.on("exit", finish);
|
|
25187
|
+
tail3.on("error", finish);
|
|
25188
|
+
});
|
|
25189
|
+
}
|
|
25018
25190
|
function realRunOnce(input) {
|
|
25019
25191
|
const bin = rollBin();
|
|
25020
25192
|
const cmd = bin.endsWith(".js") || bin.endsWith(".mjs") ? process.execPath : bin;
|
|
@@ -25756,6 +25928,28 @@ function installStopHandlers(onStop) {
|
|
|
25756
25928
|
process.off(signal2, handler);
|
|
25757
25929
|
};
|
|
25758
25930
|
}
|
|
25931
|
+
function describeScope(scope) {
|
|
25932
|
+
if (scope.kind === "cards")
|
|
25933
|
+
return `cards ${scope.cards.join(", ")}`;
|
|
25934
|
+
if (scope.kind === "epic")
|
|
25935
|
+
return `epic ${scope.epic}`;
|
|
25936
|
+
return "all Todo backlog cards";
|
|
25937
|
+
}
|
|
25938
|
+
function goStartupFeedback(slug, scope) {
|
|
25939
|
+
const session = goSessionName(slug);
|
|
25940
|
+
return [
|
|
25941
|
+
`Goal go session started: ${session}`,
|
|
25942
|
+
` scope: ${describeScope(scope)}`,
|
|
25943
|
+
" cycles: first cycle is running now; cycles chain until the scope is complete, paused, or capped.",
|
|
25944
|
+
` observe: tmux attach -t ${session} (read-only watch window; the 'go' window is the worker \u2014 do not Ctrl-C it)`,
|
|
25945
|
+
` or follow inline: roll loop go --attach`,
|
|
25946
|
+
"",
|
|
25947
|
+
`goal \u8FDE\u8DD1\u4F1A\u8BDD\u5DF2\u542F\u52A8: ${session}`,
|
|
25948
|
+
" \u7B2C\u4E00\u4E2A cycle \u6B63\u5728\u8FD0\u884C\uFF1B\u4F1A\u6301\u7EED\u8FDE\u8DD1\u76F4\u5230\u8303\u56F4\u5B8C\u6210\u3001\u6682\u505C\u6216\u8FBE\u4E0A\u9650\u3002",
|
|
25949
|
+
` \u89C2\u5BDF (\u53EA\u8BFB): tmux attach -t ${session} \u6216 roll loop go --attach`,
|
|
25950
|
+
""
|
|
25951
|
+
].join("\n");
|
|
25952
|
+
}
|
|
25759
25953
|
async function loopGoCommand(args, deps = realDeps4()) {
|
|
25760
25954
|
if (hasHelpArg2(args)) {
|
|
25761
25955
|
process.stdout.write(loopGoHelp());
|
|
@@ -25766,9 +25960,11 @@ async function loopGoCommand(args, deps = realDeps4()) {
|
|
|
25766
25960
|
if (!opts.worker && !opts.noTmux && deps.hasTmux()) {
|
|
25767
25961
|
const started = deps.startTmux({ projectPath: id.path, slug: id.slug, args, rollBin: rollBin() });
|
|
25768
25962
|
if (started) {
|
|
25769
|
-
process.stdout.write(
|
|
25770
|
-
|
|
25771
|
-
|
|
25963
|
+
process.stdout.write(goStartupFeedback(id.slug, opts.scope));
|
|
25964
|
+
if (opts.attach && deps.followFeed !== void 0) {
|
|
25965
|
+
process.stdout.write("Following live feed (Ctrl-C stops the view, not the loop) \u2026\n\u6B63\u5728\u8DDF\u968F\u5B9E\u65F6\u8F93\u51FA (Ctrl-C \u53EA\u505C\u6B62\u67E5\u770B\uFF0C\u4E0D\u4F1A\u505C\u6B62 loop) \u2026\n");
|
|
25966
|
+
await deps.followFeed(id.path, rollBin());
|
|
25967
|
+
}
|
|
25772
25968
|
return 0;
|
|
25773
25969
|
}
|
|
25774
25970
|
}
|
|
@@ -30414,6 +30610,7 @@ ${diffStat}`;
|
|
|
30414
30610
|
} else if (cmd.status === "idle" && (ctx.storyId ?? "") !== "") {
|
|
30415
30611
|
ports.backlog.markStatus?.(ports.repoCwd, ctx.storyId ?? "", STATUS_MARKER.todo);
|
|
30416
30612
|
}
|
|
30613
|
+
refreshAggregates(ports.repoCwd);
|
|
30417
30614
|
return {};
|
|
30418
30615
|
}
|
|
30419
30616
|
// _worktree_alert.
|
|
@@ -30456,16 +30653,22 @@ function buildRunRow(cmd, ctx, nowSec2) {
|
|
|
30456
30653
|
row2["duration_sec"] = dur;
|
|
30457
30654
|
}
|
|
30458
30655
|
}
|
|
30656
|
+
const routedModel = (ctx.model ?? "").trim() !== "" ? ctx.model : ctx.agent ?? "";
|
|
30657
|
+
if (routedModel !== "")
|
|
30658
|
+
row2["model"] = routedModel;
|
|
30459
30659
|
if (ctx.cost !== void 0) {
|
|
30460
30660
|
row2["cost_usd"] = ctx.cost.estimatedCost;
|
|
30461
30661
|
row2["cost_effective_usd"] = ctx.cost.effectiveCost;
|
|
30462
|
-
|
|
30662
|
+
if (ctx.cost.model !== "")
|
|
30663
|
+
row2["model"] = ctx.cost.model;
|
|
30463
30664
|
row2["tokens_in"] = ctx.cost.tokensIn;
|
|
30464
30665
|
row2["tokens_out"] = ctx.cost.tokensOut;
|
|
30465
30666
|
if (ctx.cost.cacheRead !== void 0)
|
|
30466
30667
|
row2["tokens_cache_read"] = ctx.cost.cacheRead;
|
|
30467
30668
|
if (ctx.cost.cacheWrite !== void 0)
|
|
30468
30669
|
row2["tokens_cache_write"] = ctx.cost.cacheWrite;
|
|
30670
|
+
} else {
|
|
30671
|
+
row2["usage_unknown"] = true;
|
|
30469
30672
|
}
|
|
30470
30673
|
return row2;
|
|
30471
30674
|
}
|
|
@@ -30505,10 +30708,13 @@ function buildTerminalRecord(cmd, ctx, worktreePath, nowSec2) {
|
|
|
30505
30708
|
} else {
|
|
30506
30709
|
usage2 = absent("no_parseable_usage");
|
|
30507
30710
|
}
|
|
30711
|
+
const routedModel = (ctx.model ?? "").trim() !== "" ? ctx.model : ctx.agent ?? "";
|
|
30712
|
+
const model = ctx.cost !== void 0 && ctx.cost.model !== "" ? ctx.cost.model : routedModel;
|
|
30508
30713
|
return buildTerminalEvent({
|
|
30509
30714
|
cycleId: cmd.cycleId,
|
|
30510
30715
|
storyId,
|
|
30511
30716
|
agent: ctx.agent ?? "",
|
|
30717
|
+
model,
|
|
30512
30718
|
startedAt: ctx.startSec ?? nowSec2,
|
|
30513
30719
|
endedAt: nowSec2,
|
|
30514
30720
|
outcome: OUTCOME[cmd.status] ?? "unknown",
|
package/package.json
CHANGED
|
@@ -27,20 +27,23 @@ Load when the user wants to discuss approaches, design a solution, model domains
|
|
|
27
27
|
|
|
28
28
|
1. Clarify only when intent or boundaries are unclear.
|
|
29
29
|
2. Model domain depth proportional to risk and novelty.
|
|
30
|
-
3.
|
|
31
|
-
4.
|
|
32
|
-
5.
|
|
30
|
+
3. Detailed design before decomposition — for any non-trivial work, produce a concrete, implementable design artifact and get owner sign-off: (a) data/contract schema, (b) AT LEAST ONE complete worked sample of the intended output/behavior, (c) key interface signatures, (d) mapping/normalization rules, (e) edge cases & failure modes. Depth scales with risk/novelty; trivial work may be light.
|
|
31
|
+
4. Split into INVEST stories — each a slice of the agreed detailed design.
|
|
32
|
+
5. Write specs through roll story new and update backlog.
|
|
33
|
+
6. Self-score the design quality.
|
|
33
34
|
|
|
34
35
|
## Hard Gates
|
|
35
36
|
|
|
36
37
|
- Do not start implementation from this skill.
|
|
37
38
|
- Backlog rows and spec files must stay consistent.
|
|
38
39
|
- Peer review gates apply only when explicitly available/requested.
|
|
40
|
+
- No story decomposition until a detailed design exists and the owner has signed off (proportional to risk). Decomposition slices an agreed design — it is NOT a substitute for designing. If you cannot show at least one complete worked sample of the intended output/behavior, the design is NOT done.
|
|
39
41
|
|
|
40
42
|
## Gotchas
|
|
41
43
|
|
|
42
44
|
- Design writes backlog/spec artifacts; it must not quietly start code implementation.
|
|
43
45
|
- Use roll story new for story directories; do not hand-create backlog rows without matching dossier structure.
|
|
46
|
+
- Jumping from idea straight to INVEST stories (skipping detailed design) produces shallow specs and improvised, inconsistent implementation. Decomposition must slice a concrete, owner-agreed detailed design — schema + at least one complete worked sample + interfaces + mapping rules + edge cases.
|
|
44
47
|
|
|
45
48
|
## Maintenance
|
|
46
49
|
|
|
@@ -200,12 +200,28 @@ it('should handle concurrent writes', async () => {
|
|
|
200
200
|
|
|
201
201
|
---
|
|
202
202
|
|
|
203
|
+
## 9. Detailed Design Completeness 📐
|
|
204
|
+
|
|
205
|
+
**Definition:** Before decomposing work into stories, the detailed design must be concrete and implementable — not a sketch.
|
|
206
|
+
|
|
207
|
+
**Must include (proportional to risk/novelty):**
|
|
208
|
+
- [ ] Data/contract **schema**
|
|
209
|
+
- [ ] **At least one complete worked sample** of the intended output/behavior
|
|
210
|
+
- [ ] Key **interface signatures**
|
|
211
|
+
- [ ] **Mapping/normalization rules**
|
|
212
|
+
- [ ] **Edge cases & failure modes**
|
|
213
|
+
|
|
214
|
+
**Rule:** If you cannot show at least one complete worked sample, the design is NOT done. Decomposition slices an agreed design — it is not a substitute for designing.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
203
218
|
## Mandatory Check Process
|
|
204
219
|
|
|
205
220
|
During the **Test Design Review** phase of each Story, the following must be answered:
|
|
206
221
|
|
|
207
222
|
```markdown
|
|
208
223
|
### Engineering Common Sense Checklist
|
|
224
|
+
- [ ] **Detailed design completeness**: Does the detailed design include at least one complete worked sample plus schema, interface signatures, mapping rules, and edge cases?
|
|
209
225
|
- [ ] **Idempotency**: Can it be run repeatedly? Are there tests?
|
|
210
226
|
- [ ] **Cross-module contract**: Are IDs/formats/algorithms consistent?
|
|
211
227
|
- [ ] **Data flow**: Is the producer → consumer pipeline complete?
|
|
@@ -176,7 +176,7 @@ Expected AC:
|
|
|
176
176
|
Input: IDEA-NNN identifier from .roll/backlog.md
|
|
177
177
|
|
|
178
178
|
Execution path:
|
|
179
|
-
[Read .roll/backlog.md IDEA-NNN row] → [Analyze] → [DDD Slice] → [Split Stories]
|
|
179
|
+
[Read .roll/backlog.md IDEA-NNN row] → [Analyze] → [DDD Slice] → [Solution Design] → [Split Stories]
|
|
180
180
|
→ [Write BACKLOG 📋 Todo] → [Annotate IDEA row: → US-XXX] → Done
|
|
181
181
|
|
|
182
182
|
IDEA annotation: append `→ US-XXX` to the IDEA row's Description column.
|
|
@@ -289,10 +289,22 @@ User Input
|
|
|
289
289
|
│
|
|
290
290
|
▼
|
|
291
291
|
┌─────────────────────────────────────────┐
|
|
292
|
-
│ 3. Solution Design
|
|
293
|
-
│
|
|
294
|
-
│ -
|
|
292
|
+
│ 3. Detailed (Solution) Design ★MANDATORY│
|
|
293
|
+
│ Before ANY decomposition, for │
|
|
294
|
+
│ non-trivial work produce a concrete, │
|
|
295
|
+
│ implementable artifact (depth scales │
|
|
296
|
+
│ with risk/novelty): │
|
|
297
|
+
│ - Architecture / module decomposition │
|
|
295
298
|
│ - Dependency analysis │
|
|
299
|
+
│ Required artifacts (all of): │
|
|
300
|
+
│ (a) data/contract schema │
|
|
301
|
+
│ (b) ≥1 COMPLETE worked sample of │
|
|
302
|
+
│ the intended output/behavior │
|
|
303
|
+
│ (c) key interface signatures │
|
|
304
|
+
│ (d) mapping/normalization rules │
|
|
305
|
+
│ (e) edge cases & failure modes │
|
|
306
|
+
│ Rule: if you cannot show a complete │
|
|
307
|
+
│ worked sample, the design is NOT done.│
|
|
296
308
|
│ [Greenfield] Tactical Model per Context:
|
|
297
309
|
│ - Aggregate Root + Entities + VOs │
|
|
298
310
|
│ - Invariants (业务不变式) │
|
|
@@ -312,6 +324,15 @@ User Input
|
|
|
312
324
|
│ AGREE / skipped
|
|
313
325
|
▼
|
|
314
326
|
┌─────────────────────────────────────────┐
|
|
327
|
+
│ [gate] Owner Sign-off on Detailed Design │ ← REQUIRED before decomposition
|
|
328
|
+
│ Decomposition slices an AGREED design;│
|
|
329
|
+
│ it does NOT replace designing. Get │
|
|
330
|
+
│ owner sign-off (proportional to risk) │
|
|
331
|
+
│ before splitting into stories. │
|
|
332
|
+
└──────────────────┬──────────────────────┘
|
|
333
|
+
│ Signed off
|
|
334
|
+
▼
|
|
335
|
+
┌─────────────────────────────────────────┐
|
|
315
336
|
│ 4. Split into Stories │
|
|
316
337
|
│ - INVEST principles │
|
|
317
338
|
│ - Bounded Context → US domain prefix │
|