@seanyao/roll 4.719.3 → 4.721.1
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 +12 -0
- package/dist/roll.mjs +199 -54
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v4.721.1 — 2026-07-21
|
|
6
|
+
|
|
7
|
+
### 稳定性
|
|
8
|
+
- loop 看门狗的"进展"判定从 stdout 改为 git 状态(新 commit / 工作区文件变动):输出缓冲型 agent 写文件时不再被 15 分钟无进展误杀;新增 `no-state-change` 杀因——默认 25 分钟无 commit 且无文件变动即终止(即使 stdout 仍在输出),原地打转的 cycle 不再烧满 45 分钟墙钟;阈值可用 `ROLL_CYCLE_NO_STATE_CHANGE_SEC` 覆盖(FIX-1477)[loop]
|
|
9
|
+
- attest 闸的"无 AC 块豁免"提前到渲染之前评估:无 AC 块的卡不再因渲染失败或空壳报告被硬拦,直接合法放行(FIX-1476)[loop]
|
|
10
|
+
|
|
11
|
+
## v4.720.1 — 2026-07-20
|
|
12
|
+
|
|
13
|
+
### 稳定性
|
|
14
|
+
- `roll worktree cleanup` 现在能认出压缩合并的交付,未交付分支仍会保留(FIX-1471)[loop]
|
|
15
|
+
- 暂停后可用 `roll loop go --cards` 只跑指定卡,其他启动方式继续保持暂停(FIX-1472)[loop]
|
|
16
|
+
|
|
5
17
|
## v4.719.3 — 2026-07-19
|
|
6
18
|
|
|
7
19
|
### 稳定性
|
package/dist/roll.mjs
CHANGED
|
@@ -14642,15 +14642,20 @@ function timeoutTeardownCommands(ctx) {
|
|
|
14642
14642
|
function cycleTimeoutVerdict(input) {
|
|
14643
14643
|
const wall = input.wallLimitSec ?? CYCLE_WALL_TIMEOUT_SEC;
|
|
14644
14644
|
const idle = input.noProgressLimitSec ?? CYCLE_NO_PROGRESS_SEC;
|
|
14645
|
+
const stateIdle = input.noStateChangeLimitSec ?? CYCLE_NO_STATE_CHANGE_SEC;
|
|
14645
14646
|
if (wall > 0 && input.elapsedSec >= wall) {
|
|
14646
14647
|
return { timedOut: true, reason: "wall", elapsedSec: input.elapsedSec, idleSec: input.idleSec };
|
|
14647
14648
|
}
|
|
14648
14649
|
if (idle > 0 && input.idleSec >= idle) {
|
|
14649
14650
|
return { timedOut: true, reason: "no-progress", elapsedSec: input.elapsedSec, idleSec: input.idleSec };
|
|
14650
14651
|
}
|
|
14652
|
+
if (input.stateIdleSec !== void 0 && stateIdle > 0 && input.stateIdleSec >= stateIdle) {
|
|
14653
|
+
return { timedOut: true, reason: "no-state-change", elapsedSec: input.elapsedSec, idleSec: input.idleSec };
|
|
14654
|
+
}
|
|
14651
14655
|
const wallRemain = wall > 0 ? wall - input.elapsedSec : Number.POSITIVE_INFINITY;
|
|
14652
14656
|
const idleRemain = idle > 0 ? idle - input.idleSec : Number.POSITIVE_INFINITY;
|
|
14653
|
-
|
|
14657
|
+
const stateRemain = input.stateIdleSec !== void 0 && stateIdle > 0 ? stateIdle - input.stateIdleSec : Number.POSITIVE_INFINITY;
|
|
14658
|
+
return { timedOut: false, remainingSec: Math.min(wallRemain, idleRemain, stateRemain) };
|
|
14654
14659
|
}
|
|
14655
14660
|
function stallVerdict(input) {
|
|
14656
14661
|
const threshold = input.stallThresholdSec ?? CYCLE_STALL_THRESHOLD_SEC;
|
|
@@ -15141,7 +15146,7 @@ function adversarialDegradedCmd(cycleId, storyId, cause, from = "adversarial") {
|
|
|
15141
15146
|
function initialCycleState(ctx) {
|
|
15142
15147
|
return { phase: "pick", ctx, attempt: 0, done: false };
|
|
15143
15148
|
}
|
|
15144
|
-
var CYCLE_TIMEOUT_SEC, WATCHDOG_KILL_GRACE_SEC, CYCLE_WALL_TIMEOUT_SEC, CYCLE_NO_PROGRESS_SEC, CYCLE_STALL_THRESHOLD_SEC, STALL_STARTUP_GRACE_SEC, MAX_AGENT_ATTEMPTS, RETRY_BASE_BACKOFF_SEC, EVENT_VALID_PHASES;
|
|
15149
|
+
var CYCLE_TIMEOUT_SEC, WATCHDOG_KILL_GRACE_SEC, CYCLE_WALL_TIMEOUT_SEC, CYCLE_NO_PROGRESS_SEC, CYCLE_NO_STATE_CHANGE_SEC, CYCLE_STALL_THRESHOLD_SEC, STALL_STARTUP_GRACE_SEC, MAX_AGENT_ATTEMPTS, RETRY_BASE_BACKOFF_SEC, EVENT_VALID_PHASES;
|
|
15145
15150
|
var init_orchestrator = __esm({
|
|
15146
15151
|
"packages/core/dist/loop/orchestrator.js"() {
|
|
15147
15152
|
"use strict";
|
|
@@ -15153,6 +15158,7 @@ var init_orchestrator = __esm({
|
|
|
15153
15158
|
WATCHDOG_KILL_GRACE_SEC = 5;
|
|
15154
15159
|
CYCLE_WALL_TIMEOUT_SEC = 2700;
|
|
15155
15160
|
CYCLE_NO_PROGRESS_SEC = 900;
|
|
15161
|
+
CYCLE_NO_STATE_CHANGE_SEC = 1500;
|
|
15156
15162
|
CYCLE_STALL_THRESHOLD_SEC = 600;
|
|
15157
15163
|
STALL_STARTUP_GRACE_SEC = 120;
|
|
15158
15164
|
MAX_AGENT_ATTEMPTS = 3;
|
|
@@ -247550,6 +247556,7 @@ Loop \u5DF2\u5904\u4E8E\u6682\u505C
|
|
|
247550
247556
|
Loop \u5DF2\u6682\u505C \u2014 \u540E\u7EED\u6392\u7A0B\u5468\u671F\u5C06\u8DF3\u8FC7
|
|
247551
247557
|
`);
|
|
247552
247558
|
process.stdout.write("mode: guided \u2014 scheduler will not start long-running Story execution until `roll loop resume`\n");
|
|
247559
|
+
process.stdout.write(" supervisor one-shot: `roll loop go --cards <ids> --max-cycles 1` runs exactly those cards while staying paused (no autonomous scheduling).\n");
|
|
247553
247560
|
return 0;
|
|
247554
247561
|
}
|
|
247555
247562
|
async function loopResumeCommand(_args, deps = realDeps()) {
|
|
@@ -251253,6 +251260,11 @@ function runAttestGate(worktreeCwd, storyId, cycleId, mode, sinceSec, sinks, sco
|
|
|
251253
251260
|
sinks.event({ cycleId, verdict: "skipped", reasons: reasons2 });
|
|
251254
251261
|
return { verdict: "skipped", mode, reasons: reasons2, blocked: blocked4 };
|
|
251255
251262
|
}
|
|
251263
|
+
if (storyHasAcBlock(specRepoCwd, storyId) === false) {
|
|
251264
|
+
const reasons2 = ["story has no AC block; acceptance report not required"];
|
|
251265
|
+
sinks.event({ cycleId, verdict: "produced", reasons: reasons2 });
|
|
251266
|
+
return { verdict: "produced", mode, reasons: reasons2, blocked: false };
|
|
251267
|
+
}
|
|
251256
251268
|
if (renderExitCode !== 0) {
|
|
251257
251269
|
const reasons2 = [`attest render failed for ${storyId} (exit ${renderExitCode})`];
|
|
251258
251270
|
const blocked4 = mode === "hard";
|
|
@@ -251260,11 +251272,6 @@ function runAttestGate(worktreeCwd, storyId, cycleId, mode, sinceSec, sinks, sco
|
|
|
251260
251272
|
sinks.event({ cycleId, verdict: "skipped", reasons: reasons2 });
|
|
251261
251273
|
return { verdict: "skipped", mode, reasons: reasons2, blocked: blocked4 };
|
|
251262
251274
|
}
|
|
251263
|
-
if (storyHasAcBlock(specRepoCwd, storyId) === false) {
|
|
251264
|
-
const reasons2 = ["story has no AC block; acceptance report not required"];
|
|
251265
|
-
sinks.event({ cycleId, verdict: "produced", reasons: reasons2 });
|
|
251266
|
-
return { verdict: "produced", mode, reasons: reasons2, blocked: false };
|
|
251267
|
-
}
|
|
251268
251275
|
const diagnostics = violatesMustDeclareSurface(specRepoCwd, storyId) ? [MUST_DECLARE_FAIL_REASON] : [];
|
|
251269
251276
|
const redAcs = redAcFailures(worktreeCwd, storyId, scoreRepoCwd);
|
|
251270
251277
|
if (redAcs.length > 0) {
|
|
@@ -264497,6 +264504,7 @@ import { createInterface as createInterface3 } from "node:readline";
|
|
|
264497
264504
|
// packages/cli/dist/lib/goal-progress.js
|
|
264498
264505
|
init_dist();
|
|
264499
264506
|
var GOAL_ALLOWED_CARDS_ENV = "ROLL_LOOP_GO_ALLOWED_CARDS";
|
|
264507
|
+
var GOAL_GUIDED_ENV = "ROLL_LOOP_GO_GUIDED";
|
|
264500
264508
|
var DELIVERY_STATUSES = /* @__PURE__ */ new Set(["built", "published", "done", "merged"]);
|
|
264501
264509
|
var DELIVERY_OUTCOMES = /* @__PURE__ */ new Set(["published_pending_merge", "delivered"]);
|
|
264502
264510
|
function parseAllowedCardsEnv(env = process.env) {
|
|
@@ -264505,6 +264513,11 @@ function parseAllowedCardsEnv(env = process.env) {
|
|
|
264505
264513
|
const raw = env[GOAL_ALLOWED_CARDS_ENV] ?? "";
|
|
264506
264514
|
return new Set(raw.split(",").map((s) => s.trim()).filter((s) => s !== ""));
|
|
264507
264515
|
}
|
|
264516
|
+
function isGuidedRunOnce(allowed2, env = process.env) {
|
|
264517
|
+
if ((env[GOAL_GUIDED_ENV] ?? "").trim() !== "1")
|
|
264518
|
+
return false;
|
|
264519
|
+
return allowed2 !== void 0 && allowed2.size > 0;
|
|
264520
|
+
}
|
|
264508
264521
|
var OUT_OF_SCOPE_STATUS = "\u{1F6AB} Hold (outside goal scope)";
|
|
264509
264522
|
function scopeBacklogForAllowedCards(items, allowed2) {
|
|
264510
264523
|
if (allowed2 === void 0)
|
|
@@ -265428,6 +265441,7 @@ function parseOptions(args) {
|
|
|
265428
265441
|
let noTmux = false;
|
|
265429
265442
|
let attach = false;
|
|
265430
265443
|
let scopeSpecified = false;
|
|
265444
|
+
let cardsFlagSpecified = false;
|
|
265431
265445
|
let reviewMode = "auto";
|
|
265432
265446
|
let reviewModeSpecified = false;
|
|
265433
265447
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -265497,11 +265511,13 @@ function parseOptions(args) {
|
|
|
265497
265511
|
}
|
|
265498
265512
|
if (arg === "--cards") {
|
|
265499
265513
|
cards.push(...parseCards(args[i + 1]));
|
|
265514
|
+
cardsFlagSpecified = true;
|
|
265500
265515
|
i += 1;
|
|
265501
265516
|
continue;
|
|
265502
265517
|
}
|
|
265503
265518
|
if (arg.startsWith("--cards=")) {
|
|
265504
265519
|
cards.push(...parseCards(arg.slice("--cards=".length)));
|
|
265520
|
+
cardsFlagSpecified = true;
|
|
265505
265521
|
continue;
|
|
265506
265522
|
}
|
|
265507
265523
|
if (!arg.startsWith("-"))
|
|
@@ -265517,6 +265533,7 @@ function parseOptions(args) {
|
|
|
265517
265533
|
attach,
|
|
265518
265534
|
scope,
|
|
265519
265535
|
scopeSpecified,
|
|
265536
|
+
cardsFlagSpecified,
|
|
265520
265537
|
reviewMode,
|
|
265521
265538
|
reviewModeSpecified,
|
|
265522
265539
|
...maxCycles !== void 0 ? { maxCycles } : {},
|
|
@@ -265526,6 +265543,9 @@ function parseOptions(args) {
|
|
|
265526
265543
|
function hasHelpArg2(args) {
|
|
265527
265544
|
return args.includes("--help") || args.includes("-h");
|
|
265528
265545
|
}
|
|
265546
|
+
function isGuidedRun(opts) {
|
|
265547
|
+
return opts.cardsFlagSpecified && opts.scopeSpecified && opts.scope.kind === "cards" && opts.scope.cards.length > 0;
|
|
265548
|
+
}
|
|
265529
265549
|
function loopGoHelp() {
|
|
265530
265550
|
return [
|
|
265531
265551
|
"Usage: roll loop go [--epic <name>|--cards <ids>|--all] [--for <duration>] [--max-cycles <n>] [--review <auto|hetero|self|off>] [--attach] [--no-tmux]",
|
|
@@ -265550,6 +265570,12 @@ function loopGoHelp() {
|
|
|
265550
265570
|
" The startup banner shows the EFFECTIVE scope; an inherited scope is flagged so a flagless go can never silently narrow. Pass --all to reset to the full backlog.",
|
|
265551
265571
|
" \u542F\u52A8\u6A2A\u5E45\u663E\u793A\u751F\u6548\u7684 scope\uFF1B\u6CBF\u7528\u7684 scope \u4F1A\u88AB\u6807\u6CE8\uFF0Cflagless go \u7EDD\u4E0D\u9759\u9ED8\u6536\u7A84\u3002\u7528 --all \u91CD\u7F6E\u56DE\u5168\u91CF\u3002",
|
|
265552
265572
|
"",
|
|
265573
|
+
"Guided one-shot while paused (FIX-1472):",
|
|
265574
|
+
" An explicit `roll loop go --cards X` runs the named card(s) even while `roll loop pause` is in effect \u2014 it is a supervisor action, not autonomous scheduling. It installs no scheduler and picks no other card; bound it with --max-cycles.",
|
|
265575
|
+
" \u663E\u5F0F\u7684 `roll loop go --cards X` \u5373\u4F7F\u5728 `roll loop pause` \u751F\u6548\u65F6\u4E5F\u4F1A\u6267\u884C\u6307\u5B9A\u5361\u2014\u2014\u8FD9\u662F Supervisor \u624B\u52A8\u52A8\u4F5C\uFF0C\u4E0D\u662F\u81EA\u4E3B\u6392\u7A0B\u3002\u5B83\u4E0D\u5B89\u88C5\u6392\u7A0B\u3001\u4E0D\u9009\u522B\u7684\u5361\uFF1B\u7528 --max-cycles \u9650\u5B9A\u6B21\u6570\u3002",
|
|
265576
|
+
" Pause still stops autonomous scheduling; only an explicitly-named --cards scope bypasses it (a flagless/inherited/--all/--epic go keeps honoring the pause).",
|
|
265577
|
+
" pause \u4ECD\u7136\u505C\u6B62\u81EA\u4E3B\u6392\u7A0B\uFF1B\u53EA\u6709\u672C\u6B21\u663E\u5F0F\u6307\u5B9A\u7684 --cards \u8303\u56F4\u624D\u4F1A\u7ED5\u8FC7\uFF08flagless/\u6CBF\u7528/--all/--epic \u7684 go \u4ECD\u9075\u5B88\u6682\u505C\uFF09\u3002",
|
|
265578
|
+
"",
|
|
265553
265579
|
"Progress guardrails (the loop stops on NO PROGRESS, not on cost):",
|
|
265554
265580
|
" productivity floor A cycle whose agent EXECUTED but produced 0 commits and no delivery is a `gave_up` terminal \u2014 alerted on the first occurrence (no streak).",
|
|
265555
265581
|
" dead-loop breaker A card is skipped after consecutive no-progress cycles; the whole goal STOPS after K consecutive no-progress cycles (a loud ALERT) \u2014 an unmergeable card can never spin forever.",
|
|
@@ -265673,6 +265699,20 @@ function followGoLiveFeed(projectPath3, rollBin4) {
|
|
|
265673
265699
|
watch.on("error", finish);
|
|
265674
265700
|
});
|
|
265675
265701
|
}
|
|
265702
|
+
function buildRunOnceChildEnv(input, base = process.env) {
|
|
265703
|
+
return {
|
|
265704
|
+
...base,
|
|
265705
|
+
ROLL_LOOP_GO_CHILD: "1",
|
|
265706
|
+
// FIX-1022: unattended go-driver child must never trigger the macOS
|
|
265707
|
+
// screencapture TCC prompt (isTTY is unreliable under PTY wrapping).
|
|
265708
|
+
ROLL_NO_SCREENCAP: base["ROLL_NO_SCREENCAP"] ?? "1",
|
|
265709
|
+
...input.allowedCards !== void 0 ? { [GOAL_ALLOWED_CARDS_ENV]: input.allowedCards.join(",") } : {},
|
|
265710
|
+
// FIX-1472: guided is set on EVERY call (never conditionally spread) so an
|
|
265711
|
+
// inherited ambient "1" cannot survive into an unguided child. Fail closed
|
|
265712
|
+
// to "0" — isGuidedRunOnce treats anything but "1" as not guided.
|
|
265713
|
+
[GOAL_GUIDED_ENV]: input.guided === true ? "1" : "0"
|
|
265714
|
+
};
|
|
265715
|
+
}
|
|
265676
265716
|
function realRunOnce(input) {
|
|
265677
265717
|
const bin = rollBin();
|
|
265678
265718
|
const cmd = bin.endsWith(".js") || bin.endsWith(".mjs") ? process.execPath : bin;
|
|
@@ -265681,14 +265721,7 @@ function realRunOnce(input) {
|
|
|
265681
265721
|
const child = spawn8(cmd, args, {
|
|
265682
265722
|
cwd: input.projectPath,
|
|
265683
265723
|
detached: true,
|
|
265684
|
-
env:
|
|
265685
|
-
...process.env,
|
|
265686
|
-
ROLL_LOOP_GO_CHILD: "1",
|
|
265687
|
-
// FIX-1022: unattended go-driver child must never trigger the macOS
|
|
265688
|
-
// screencapture TCC prompt (isTTY is unreliable under PTY wrapping).
|
|
265689
|
-
ROLL_NO_SCREENCAP: process.env["ROLL_NO_SCREENCAP"] ?? "1",
|
|
265690
|
-
...input.allowedCards !== void 0 ? { [GOAL_ALLOWED_CARDS_ENV]: input.allowedCards.join(",") } : {}
|
|
265691
|
-
},
|
|
265724
|
+
env: buildRunOnceChildEnv(input),
|
|
265692
265725
|
stdio: "inherit"
|
|
265693
265726
|
});
|
|
265694
265727
|
child.on("exit", (code) => resolve17(code ?? 1));
|
|
@@ -266587,11 +266620,12 @@ async function runGoWorker(id, opts, deps) {
|
|
|
266587
266620
|
progress = progressFromGoal(goal);
|
|
266588
266621
|
initialUsage = runSummaryFromGoal(goal);
|
|
266589
266622
|
const deadlineSec = timeboxDeadlineSec(goal, opts, startedSec);
|
|
266623
|
+
const guided = isGuidedRun(opts);
|
|
266590
266624
|
bus.appendEvent(evPath, { type: "goal:session_start", sessionId: sid, scope: goal.scope, ts: startedSec });
|
|
266591
266625
|
while (true) {
|
|
266592
266626
|
if (stopRequested)
|
|
266593
266627
|
break;
|
|
266594
|
-
if (existsSync64(pauseMarkerPath2(id.path, id.slug))) {
|
|
266628
|
+
if (!guided && existsSync64(pauseMarkerPath2(id.path, id.slug))) {
|
|
266595
266629
|
stopReason2 = "pause_marker";
|
|
266596
266630
|
break;
|
|
266597
266631
|
}
|
|
@@ -266647,10 +266681,10 @@ async function runGoWorker(id, opts, deps) {
|
|
|
266647
266681
|
}
|
|
266648
266682
|
if (stopRequested)
|
|
266649
266683
|
break;
|
|
266650
|
-
if (existsSync64(pauseMarkerPath2(id.path, id.slug)))
|
|
266684
|
+
if (!guided && existsSync64(pauseMarkerPath2(id.path, id.slug)))
|
|
266651
266685
|
continue;
|
|
266652
266686
|
const before = readRunSnapshot(runsPath2(id.path));
|
|
266653
|
-
await deps.runOnce({ projectPath: id.path, allowedCards });
|
|
266687
|
+
await deps.runOnce({ projectPath: id.path, allowedCards, ...guided ? { guided: true } : {} });
|
|
266654
266688
|
goal = updateUsage(id.path, goal, baseline, initialUsage, deps.nowIso());
|
|
266655
266689
|
writeGoal(gPath, goal);
|
|
266656
266690
|
const after = readRunSnapshot(runsPath2(id.path));
|
|
@@ -266707,7 +266741,7 @@ async function runGoWorker(id, opts, deps) {
|
|
|
266707
266741
|
stopReason2 = "safety_pause";
|
|
266708
266742
|
break;
|
|
266709
266743
|
}
|
|
266710
|
-
if (existsSync64(pauseMarkerPath2(id.path, id.slug))) {
|
|
266744
|
+
if (!guided && existsSync64(pauseMarkerPath2(id.path, id.slug))) {
|
|
266711
266745
|
stopReason2 = "pause_marker";
|
|
266712
266746
|
break;
|
|
266713
266747
|
}
|
|
@@ -284487,7 +284521,8 @@ function readCycleTimeoutThresholds(repoCwd) {
|
|
|
284487
284521
|
}
|
|
284488
284522
|
return {
|
|
284489
284523
|
wallSec: envNum("ROLL_CYCLE_WALL_TIMEOUT_SEC") ?? wallSec,
|
|
284490
|
-
noProgressSec: envNum("ROLL_CYCLE_NO_PROGRESS_SEC") ?? noProgressSec
|
|
284524
|
+
noProgressSec: envNum("ROLL_CYCLE_NO_PROGRESS_SEC") ?? noProgressSec,
|
|
284525
|
+
noStateChangeSec: envNum("ROLL_CYCLE_NO_STATE_CHANGE_SEC") ?? CYCLE_NO_STATE_CHANGE_SEC
|
|
284491
284526
|
};
|
|
284492
284527
|
}
|
|
284493
284528
|
function readStallThreshold(repoCwd) {
|
|
@@ -284503,15 +284538,18 @@ function readStallThreshold(repoCwd) {
|
|
|
284503
284538
|
}
|
|
284504
284539
|
function startSpawnTimeoutWatchdog(opts) {
|
|
284505
284540
|
const { cycleId, thresholds, clock: clock2, commitCount, appendEvent: appendEvent3 } = opts;
|
|
284541
|
+
const stateSignature = opts.stateSignature;
|
|
284506
284542
|
const kill = opts.kill ?? (() => killLiveAgents("SIGKILL"));
|
|
284507
284543
|
const pollMs = opts.pollMs ?? (Number((process.env["ROLL_TIMEOUT_POLL_MS"] ?? "").trim()) || TIMEOUT_POLL_MS);
|
|
284508
|
-
if (thresholds.wallSec <= 0 && thresholds.noProgressSec <= 0) {
|
|
284544
|
+
if (thresholds.wallSec <= 0 && thresholds.noProgressSec <= 0 && thresholds.noStateChangeSec <= 0) {
|
|
284509
284545
|
return { markProgress: () => {
|
|
284510
284546
|
}, stop: () => ({ firedReason: null }) };
|
|
284511
284547
|
}
|
|
284512
284548
|
const startSec = clock2();
|
|
284513
284549
|
let lastProgressSec = startSec;
|
|
284550
|
+
let lastStateSec = startSec;
|
|
284514
284551
|
let lastCommitCount = -1;
|
|
284552
|
+
let lastSignature;
|
|
284515
284553
|
let firedReason = null;
|
|
284516
284554
|
let running = false;
|
|
284517
284555
|
const markProgress = () => {
|
|
@@ -284526,16 +284564,32 @@ function startSpawnTimeoutWatchdog(opts) {
|
|
|
284526
284564
|
const n = await commitCount();
|
|
284527
284565
|
if (n > lastCommitCount) {
|
|
284528
284566
|
lastCommitCount = n;
|
|
284529
|
-
|
|
284567
|
+
const now3 = clock2();
|
|
284568
|
+
lastProgressSec = now3;
|
|
284569
|
+
lastStateSec = now3;
|
|
284530
284570
|
}
|
|
284531
284571
|
} catch {
|
|
284532
284572
|
}
|
|
284573
|
+
if (stateSignature !== void 0) {
|
|
284574
|
+
try {
|
|
284575
|
+
const sig = await stateSignature();
|
|
284576
|
+
if (lastSignature !== void 0 && sig !== lastSignature) {
|
|
284577
|
+
const now3 = clock2();
|
|
284578
|
+
lastProgressSec = now3;
|
|
284579
|
+
lastStateSec = now3;
|
|
284580
|
+
}
|
|
284581
|
+
lastSignature = sig;
|
|
284582
|
+
} catch {
|
|
284583
|
+
}
|
|
284584
|
+
}
|
|
284533
284585
|
const now2 = clock2();
|
|
284534
284586
|
const verdict = cycleTimeoutVerdict({
|
|
284535
284587
|
elapsedSec: now2 - startSec,
|
|
284536
284588
|
idleSec: now2 - lastProgressSec,
|
|
284589
|
+
stateIdleSec: now2 - lastStateSec,
|
|
284537
284590
|
wallLimitSec: thresholds.wallSec,
|
|
284538
|
-
noProgressLimitSec: thresholds.noProgressSec
|
|
284591
|
+
noProgressLimitSec: thresholds.noProgressSec,
|
|
284592
|
+
noStateChangeLimitSec: thresholds.noStateChangeSec
|
|
284539
284593
|
});
|
|
284540
284594
|
if (verdict.timedOut) {
|
|
284541
284595
|
firedReason = verdict.reason;
|
|
@@ -284565,6 +284619,12 @@ function startSpawnTimeoutWatchdog(opts) {
|
|
|
284565
284619
|
lastCommitCount = await commitCount();
|
|
284566
284620
|
} catch {
|
|
284567
284621
|
}
|
|
284622
|
+
if (stateSignature !== void 0) {
|
|
284623
|
+
try {
|
|
284624
|
+
lastSignature = await stateSignature();
|
|
284625
|
+
} catch {
|
|
284626
|
+
}
|
|
284627
|
+
}
|
|
284568
284628
|
})();
|
|
284569
284629
|
const timer = setInterval(() => void tick(), pollMs);
|
|
284570
284630
|
timer.unref?.();
|
|
@@ -284732,11 +284792,13 @@ async function executeSpawnAgentCommand(cmd, ports, ctx) {
|
|
|
284732
284792
|
appendEvent: (ev) => ports.events.appendEvent(ports.paths.eventsPath, ev),
|
|
284733
284793
|
thresholdSec: readStallThreshold(ports.repoCwd).thresholdSec
|
|
284734
284794
|
});
|
|
284795
|
+
const statusSignature = ports.git.worktreeStatusSignature;
|
|
284735
284796
|
const timeoutWatchdog = startSpawnTimeoutWatchdog({
|
|
284736
284797
|
cycleId: ctx.cycleId ?? "",
|
|
284737
284798
|
thresholds: readCycleTimeoutThresholds(ports.repoCwd),
|
|
284738
284799
|
clock: ports.clock,
|
|
284739
284800
|
commitCount: () => ports.git.commitsAhead(execCwd, observeBase),
|
|
284801
|
+
...statusSignature !== void 0 ? { stateSignature: () => statusSignature(execCwd) } : {},
|
|
284740
284802
|
appendEvent: (ev) => ports.events.appendEvent(ports.paths.eventsPath, ev)
|
|
284741
284803
|
});
|
|
284742
284804
|
const skillBodyForSpawn = maybeInjectProjectMap(
|
|
@@ -287724,6 +287786,13 @@ function nodePorts(opts) {
|
|
|
287724
287786
|
const n = Number((r.stdout ?? "0").trim());
|
|
287725
287787
|
return Number.isFinite(n) ? n : 0;
|
|
287726
287788
|
},
|
|
287789
|
+
async worktreeStatusSignature(worktreeCwd) {
|
|
287790
|
+
const r = await execFileAsync17("git", ["status", "--porcelain"], {
|
|
287791
|
+
cwd: worktreeCwd,
|
|
287792
|
+
encoding: "utf8"
|
|
287793
|
+
});
|
|
287794
|
+
return r.stdout ?? "";
|
|
287795
|
+
},
|
|
287727
287796
|
async mainAhead(repoCwd) {
|
|
287728
287797
|
const r = await execFileAsync17("git", ["rev-list", "--count", "origin/main..main"], {
|
|
287729
287798
|
cwd: repoCwd,
|
|
@@ -288829,7 +288898,7 @@ import { execFileSync as execFileSync32 } from "node:child_process";
|
|
|
288829
288898
|
import { homedir as homedir34 } from "node:os";
|
|
288830
288899
|
import { appendFileSync as appendFileSync20, existsSync as existsSync134, mkdirSync as mkdirSync73, rmSync as rmSync25 } from "node:fs";
|
|
288831
288900
|
import { dirname as dirname73, join as join157, relative as relative14, resolve as resolve14, sep as sep8 } from "node:path";
|
|
288832
|
-
var MERGED_KINDS = /* @__PURE__ */ new Set(["ancestor", "patch_equivalent"]);
|
|
288901
|
+
var MERGED_KINDS = /* @__PURE__ */ new Set(["ancestor", "patch_equivalent", "final_tree"]);
|
|
288833
288902
|
function isSafelyDisposable(rec) {
|
|
288834
288903
|
return rec.owner === "loop" && rec.active === false && rec.dirtyTracked === false && rec.disposition === "disposable_candidate" && MERGED_KINDS.has(rec.mergeEvidence.kind) && typeof rec.head === "string" && rec.head.length > 0;
|
|
288835
288904
|
}
|
|
@@ -288906,9 +288975,12 @@ function planWorktreeCleanup(audit, threshold, standaloneMergedBranches = []) {
|
|
|
288906
288975
|
preserved
|
|
288907
288976
|
};
|
|
288908
288977
|
}
|
|
288909
|
-
function defaultRemoveBranch(repositoryRoot, branch) {
|
|
288978
|
+
function defaultRemoveBranch(repositoryRoot, branch, expectedSha) {
|
|
288979
|
+
if (!isFullGitOid(expectedSha)) {
|
|
288980
|
+
return { ok: false, detail: `refused: expected sha ${expectedSha} is not a full git OID` };
|
|
288981
|
+
}
|
|
288910
288982
|
try {
|
|
288911
|
-
execFileSync32("git", ["-C", repositoryRoot, "
|
|
288983
|
+
execFileSync32("git", ["-C", repositoryRoot, "update-ref", "-d", `refs/heads/${branch}`, expectedSha], {
|
|
288912
288984
|
encoding: "utf8",
|
|
288913
288985
|
stdio: ["ignore", "pipe", "pipe"]
|
|
288914
288986
|
});
|
|
@@ -289111,7 +289183,7 @@ async function applyWorktreeCleanup(plan, options) {
|
|
|
289111
289183
|
branchesRemoved.push(removalB);
|
|
289112
289184
|
continue;
|
|
289113
289185
|
}
|
|
289114
|
-
const r = removeBranchFn(repositoryRoot, bc.branch);
|
|
289186
|
+
const r = removeBranchFn(repositoryRoot, bc.branch, bc.expectedSha);
|
|
289115
289187
|
if (!r.ok) {
|
|
289116
289188
|
refuseB(`delete-failed: ${r.detail}`);
|
|
289117
289189
|
continue;
|
|
@@ -289257,7 +289329,7 @@ function renderResultHuman(result2) {
|
|
|
289257
289329
|
}
|
|
289258
289330
|
return lines3.join("\n").trimEnd() + "\n";
|
|
289259
289331
|
}
|
|
289260
|
-
var CLEANUP_USAGE = "Usage: roll worktree cleanup [--dry-run | --apply] [--json] [--repo <path>]\n Safely recover from branch/worktree canary pressure using the worktree\n audit as the SOLE authority. Removes ONLY inactive, merged, clean\n `disposable_candidate` loop worktrees, plus (FIX-1454) standalone ephemeral\n branches that are verifiably
|
|
289332
|
+
var CLEANUP_USAGE = "Usage: roll worktree cleanup [--dry-run | --apply] [--json] [--repo <path>]\n Safely recover from branch/worktree canary pressure using the worktree\n audit as the SOLE authority. Removes ONLY inactive, merged, clean\n `disposable_candidate` loop worktrees, plus (FIX-1454) standalone ephemeral\n branches that are verifiably delivered and attached to no worktree. Delivery\n is PROVEN by one of: every commit is an ancestor of the integration branch;\n `git cherry` shows every commit already has an equivalent patch upstream; or\n (FIX-1471, squash merges) the branch tip tree is byte-identical to the merge\n commit of the branch's OWN merged GitHub PR (matched by exact head ref) AND\n that merge commit is an ancestor of the integration branch. A merged PR alone\n is NEVER sufficient, and no arbitrary same-tree commit on the integration\n branch is ever used. Never a path/ref that is merely old or counted, and never\n a preserved (unpublished / dirty / active / external / current / protected /\n unmerged) one.\n\n Always dry-run first. Default (no flag) is --dry-run.\n --dry-run print counted refs/dirs, audit dispositions, and the minimal\n candidate set to clear pressure. Never mutates git state.\n --apply re-run the audit before EVERY removal; remove only revalidated\n candidates via git, prune registration, emit events. A changed\n head / new dirt / missing path / concurrent activation fails\n closed (no substitution). Then resume explicitly: roll loop resume\n --json emit the schema-1 plan (dry-run) or result (apply) as JSON\n --repo override the project root (default: current directory)\n --reclaim-orphan <path> (FIX-1460) bounded-rm ONE named orphan loop dir\n (deregistered from git; delivery not auto-provable) after you\n review it. Fails closed unless it is an inactive loop orphan\n inside .roll/loop/worktrees. Auto-reclaim of provably-delivered\n orphans happens under --apply.\n\n \u5B89\u5168\u6E05\u7406:\u4EC5\u79FB\u9664\u5BA1\u8BA1\u5224\u5B9A\u4E3A\u5DF2\u5408\u5E76\u3001\u5E72\u51C0\u3001\u975E\u6D3B\u8DC3\u7684 disposable_candidate;\n \u5148\u8DD1 --dry-run,\u518D --apply,\u6700\u540E\u624B\u52A8 roll loop resume\u3002";
|
|
289261
289333
|
function resolveThreshold() {
|
|
289262
289334
|
const parsed = parseInt(process.env["ROLL_BRANCH_CANARY_MAX"] ?? "", 10);
|
|
289263
289335
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_BRANCH_CANARY_MAX;
|
|
@@ -289265,20 +289337,41 @@ function resolveThreshold() {
|
|
|
289265
289337
|
function gitCap(repoRoot3, args) {
|
|
289266
289338
|
return execFileSync32("git", ["-C", repoRoot3, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
289267
289339
|
}
|
|
289268
|
-
function classifyBranchMerge(branch, integrationBranch, git5) {
|
|
289340
|
+
function classifyBranchMerge(branch, integrationBranch, git5, prMergeCommit) {
|
|
289269
289341
|
if (git5(["merge-base", "--is-ancestor", branch, integrationBranch]).ok)
|
|
289270
289342
|
return "ancestor";
|
|
289271
289343
|
const cherry = git5(["cherry", integrationBranch, branch]);
|
|
289272
|
-
if (
|
|
289344
|
+
if (cherry.ok) {
|
|
289345
|
+
const lines3 = cherry.stdout.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
289346
|
+
if (lines3.length > 0 && lines3.every((l) => l.startsWith("-")))
|
|
289347
|
+
return "patch_equivalent";
|
|
289348
|
+
}
|
|
289349
|
+
return classifyFinalTreeDelivery(branch, integrationBranch, git5, prMergeCommit);
|
|
289350
|
+
}
|
|
289351
|
+
function classifyFinalTreeDelivery(branch, integrationBranch, git5, prMergeCommit) {
|
|
289352
|
+
const rawMerge = prMergeCommit(branch);
|
|
289353
|
+
if (rawMerge === null)
|
|
289273
289354
|
return null;
|
|
289274
|
-
const
|
|
289275
|
-
if (
|
|
289355
|
+
const merge = rawMerge.trim();
|
|
289356
|
+
if (!isFullGitOid(merge))
|
|
289276
289357
|
return null;
|
|
289277
|
-
if (
|
|
289358
|
+
if (!git5(["merge-base", "--is-ancestor", merge, integrationBranch]).ok)
|
|
289278
289359
|
return null;
|
|
289279
|
-
|
|
289280
|
-
|
|
289281
|
-
|
|
289360
|
+
const branchTreeProbe = git5(["rev-parse", `${branch}^{tree}`]);
|
|
289361
|
+
if (!branchTreeProbe.ok)
|
|
289362
|
+
return null;
|
|
289363
|
+
const branchTree = branchTreeProbe.stdout.trim();
|
|
289364
|
+
if (branchTree === "")
|
|
289365
|
+
return null;
|
|
289366
|
+
const mergeTreeProbe = git5(["rev-parse", `${merge}^{tree}`]);
|
|
289367
|
+
if (!mergeTreeProbe.ok)
|
|
289368
|
+
return null;
|
|
289369
|
+
const mergeTree = mergeTreeProbe.stdout.trim();
|
|
289370
|
+
if (mergeTree === "")
|
|
289371
|
+
return null;
|
|
289372
|
+
if (branchTree !== mergeTree)
|
|
289373
|
+
return null;
|
|
289374
|
+
return "final_tree";
|
|
289282
289375
|
}
|
|
289283
289376
|
function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
|
|
289284
289377
|
const attachedBranches = new Set(audit.records.filter((r) => r.owner === "loop" && typeof r.branch === "string" && r.branch !== "").map((r) => r.branch.replace(/^refs\/heads\//, "")));
|
|
@@ -289298,11 +289391,12 @@ function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
|
|
|
289298
289391
|
return null;
|
|
289299
289392
|
}
|
|
289300
289393
|
},
|
|
289301
|
-
// FIX-1458 (#1465): delivery is proven
|
|
289302
|
-
//
|
|
289303
|
-
//
|
|
289304
|
-
//
|
|
289305
|
-
//
|
|
289394
|
+
// FIX-1458 (#1465), FIX-1471: delivery is proven by fresh git patch evidence
|
|
289395
|
+
// (ancestor OR `git cherry` patch-equivalence) OR, for the squash-merge case,
|
|
289396
|
+
// by the branch tip tree matching the merge commit of the branch's OWN merged
|
|
289397
|
+
// GitHub PR (an ancestor of integration). A merged PR label alone never
|
|
289398
|
+
// authorizes deletion, and no arbitrary same-tree integration commit is ever
|
|
289399
|
+
// consulted — that would silently discard unique commits (US-ORG-003/007/004).
|
|
289306
289400
|
branchMerge: (branch) => classifyBranchMerge(branch, integrationBranch, (args) => {
|
|
289307
289401
|
try {
|
|
289308
289402
|
return { ok: true, stdout: gitCap(repoRoot3, args) };
|
|
@@ -289310,9 +289404,39 @@ function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
|
|
|
289310
289404
|
const stdout = typeof err16.stdout === "string" ? err16.stdout : "";
|
|
289311
289405
|
return { ok: false, stdout };
|
|
289312
289406
|
}
|
|
289313
|
-
})
|
|
289407
|
+
}, (b) => ghMergedPrMergeCommit(repoRoot3, b))
|
|
289314
289408
|
};
|
|
289315
289409
|
}
|
|
289410
|
+
function isFullGitOid(value) {
|
|
289411
|
+
return /^[0-9a-f]{40}$/.test(value) || /^[0-9a-f]{64}$/.test(value);
|
|
289412
|
+
}
|
|
289413
|
+
function parseMergedPrMergeCommit(raw, branch) {
|
|
289414
|
+
let j;
|
|
289415
|
+
try {
|
|
289416
|
+
j = JSON.parse(raw);
|
|
289417
|
+
} catch {
|
|
289418
|
+
return null;
|
|
289419
|
+
}
|
|
289420
|
+
if (j.state !== "MERGED")
|
|
289421
|
+
return null;
|
|
289422
|
+
if (typeof j.mergedAt !== "string" || j.mergedAt === "")
|
|
289423
|
+
return null;
|
|
289424
|
+
if (typeof j.headRefName !== "string" || j.headRefName !== branch)
|
|
289425
|
+
return null;
|
|
289426
|
+
const oid = j.mergeCommit?.oid;
|
|
289427
|
+
if (typeof oid !== "string" || !isFullGitOid(oid))
|
|
289428
|
+
return null;
|
|
289429
|
+
return oid;
|
|
289430
|
+
}
|
|
289431
|
+
function ghMergedPrMergeCommit(repoRoot3, branch) {
|
|
289432
|
+
let out3;
|
|
289433
|
+
try {
|
|
289434
|
+
out3 = execFileSync32("gh", ["pr", "view", branch, "--json", "state,mergedAt,mergeCommit,headRefName"], { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
289435
|
+
} catch {
|
|
289436
|
+
return null;
|
|
289437
|
+
}
|
|
289438
|
+
return parseMergedPrMergeCommit(out3, branch);
|
|
289439
|
+
}
|
|
289316
289440
|
function resolveIntegrationForCleanup(repoRoot3) {
|
|
289317
289441
|
try {
|
|
289318
289442
|
const head = gitCap(repoRoot3, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]).trim();
|
|
@@ -290111,7 +290235,7 @@ async function loopRunOnceCommand(args) {
|
|
|
290111
290235
|
removePendingPublish(rt, storyId);
|
|
290112
290236
|
}
|
|
290113
290237
|
}
|
|
290114
|
-
if (isLoopPaused(id.path, id.slug)) {
|
|
290238
|
+
if (isLoopPaused(id.path, id.slug) && !isGuidedRunOnce(allowedCards)) {
|
|
290115
290239
|
const lang10 = resolveLang({
|
|
290116
290240
|
rollLang: process.env["ROLL_LANG"],
|
|
290117
290241
|
lcAll: process.env["LC_ALL"],
|
|
@@ -292823,28 +292947,49 @@ function assertTestProofFresh(exec) {
|
|
|
292823
292947
|
throw new Error("code changed since last test run \u2014 run `roll test` on the current staged tree");
|
|
292824
292948
|
}
|
|
292825
292949
|
}
|
|
292950
|
+
function hasOnlyPreparedReleaseChanges(exec) {
|
|
292951
|
+
const status2 = exec("git", ["status", "--porcelain", "--untracked-files=all"]).trim();
|
|
292952
|
+
if (status2 === "")
|
|
292953
|
+
return false;
|
|
292954
|
+
const allowed2 = /* @__PURE__ */ new Set(["package.json", "CHANGELOG.md"]);
|
|
292955
|
+
const unexpected = status2.split("\n").map((line) => line.slice(2).trim()).filter((path) => !allowed2.has(path));
|
|
292956
|
+
if (unexpected.length > 0) {
|
|
292957
|
+
throw new Error(`unexpected worktree changes during release recovery: ${unexpected.join(", ")}`);
|
|
292958
|
+
}
|
|
292959
|
+
return true;
|
|
292960
|
+
}
|
|
292826
292961
|
function commitPushWithGate(opts) {
|
|
292827
292962
|
const { branch, message, rollManaged, exec } = opts;
|
|
292828
292963
|
const original = exec("git", ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
|
|
292964
|
+
const preparedChanges = hasOnlyPreparedReleaseChanges(exec);
|
|
292829
292965
|
let createdLocal = false;
|
|
292966
|
+
const checkoutExistingBranch = () => {
|
|
292967
|
+
if (preparedChanges) {
|
|
292968
|
+
exec("git", ["checkout", "--merge", branch]);
|
|
292969
|
+
return;
|
|
292970
|
+
}
|
|
292971
|
+
exec("git", ["checkout", branch]);
|
|
292972
|
+
};
|
|
292830
292973
|
const checkoutBranch = () => {
|
|
292974
|
+
let localSha = "";
|
|
292831
292975
|
try {
|
|
292832
|
-
|
|
292833
|
-
if (localSha !== "") {
|
|
292834
|
-
exec("git", ["checkout", branch]);
|
|
292835
|
-
return;
|
|
292836
|
-
}
|
|
292976
|
+
localSha = exec("git", ["rev-parse", "--verify", `refs/heads/${branch}`]).trim();
|
|
292837
292977
|
} catch {
|
|
292838
292978
|
}
|
|
292979
|
+
if (localSha !== "") {
|
|
292980
|
+
checkoutExistingBranch();
|
|
292981
|
+
return;
|
|
292982
|
+
}
|
|
292983
|
+
let remote = "";
|
|
292839
292984
|
try {
|
|
292840
|
-
|
|
292841
|
-
if (remote !== "") {
|
|
292842
|
-
exec("git", ["fetch", "origin", `${branch}:${branch}`]);
|
|
292843
|
-
exec("git", ["checkout", branch]);
|
|
292844
|
-
return;
|
|
292845
|
-
}
|
|
292985
|
+
remote = exec("git", ["ls-remote", "--heads", "origin", branch]).trim();
|
|
292846
292986
|
} catch {
|
|
292847
292987
|
}
|
|
292988
|
+
if (remote !== "") {
|
|
292989
|
+
exec("git", ["fetch", "origin", `${branch}:${branch}`]);
|
|
292990
|
+
checkoutExistingBranch();
|
|
292991
|
+
return;
|
|
292992
|
+
}
|
|
292848
292993
|
createdLocal = true;
|
|
292849
292994
|
exec("git", ["checkout", "-b", branch]);
|
|
292850
292995
|
};
|