@seanyao/roll 4.720.1 → 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 CHANGED
@@ -2,6 +2,12 @@
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
+
5
11
  ## v4.720.1 — 2026-07-20
6
12
 
7
13
  ### 稳定性
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
- return { timedOut: false, remainingSec: Math.min(wallRemain, idleRemain) };
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;
@@ -251254,6 +251260,11 @@ function runAttestGate(worktreeCwd, storyId, cycleId, mode, sinceSec, sinks, sco
251254
251260
  sinks.event({ cycleId, verdict: "skipped", reasons: reasons2 });
251255
251261
  return { verdict: "skipped", mode, reasons: reasons2, blocked: blocked4 };
251256
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
+ }
251257
251268
  if (renderExitCode !== 0) {
251258
251269
  const reasons2 = [`attest render failed for ${storyId} (exit ${renderExitCode})`];
251259
251270
  const blocked4 = mode === "hard";
@@ -251261,11 +251272,6 @@ function runAttestGate(worktreeCwd, storyId, cycleId, mode, sinceSec, sinks, sco
251261
251272
  sinks.event({ cycleId, verdict: "skipped", reasons: reasons2 });
251262
251273
  return { verdict: "skipped", mode, reasons: reasons2, blocked: blocked4 };
251263
251274
  }
251264
- if (storyHasAcBlock(specRepoCwd, storyId) === false) {
251265
- const reasons2 = ["story has no AC block; acceptance report not required"];
251266
- sinks.event({ cycleId, verdict: "produced", reasons: reasons2 });
251267
- return { verdict: "produced", mode, reasons: reasons2, blocked: false };
251268
- }
251269
251275
  const diagnostics = violatesMustDeclareSurface(specRepoCwd, storyId) ? [MUST_DECLARE_FAIL_REASON] : [];
251270
251276
  const redAcs = redAcFailures(worktreeCwd, storyId, scoreRepoCwd);
251271
251277
  if (redAcs.length > 0) {
@@ -284515,7 +284521,8 @@ function readCycleTimeoutThresholds(repoCwd) {
284515
284521
  }
284516
284522
  return {
284517
284523
  wallSec: envNum("ROLL_CYCLE_WALL_TIMEOUT_SEC") ?? wallSec,
284518
- 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
284519
284526
  };
284520
284527
  }
284521
284528
  function readStallThreshold(repoCwd) {
@@ -284531,15 +284538,18 @@ function readStallThreshold(repoCwd) {
284531
284538
  }
284532
284539
  function startSpawnTimeoutWatchdog(opts) {
284533
284540
  const { cycleId, thresholds, clock: clock2, commitCount, appendEvent: appendEvent3 } = opts;
284541
+ const stateSignature = opts.stateSignature;
284534
284542
  const kill = opts.kill ?? (() => killLiveAgents("SIGKILL"));
284535
284543
  const pollMs = opts.pollMs ?? (Number((process.env["ROLL_TIMEOUT_POLL_MS"] ?? "").trim()) || TIMEOUT_POLL_MS);
284536
- if (thresholds.wallSec <= 0 && thresholds.noProgressSec <= 0) {
284544
+ if (thresholds.wallSec <= 0 && thresholds.noProgressSec <= 0 && thresholds.noStateChangeSec <= 0) {
284537
284545
  return { markProgress: () => {
284538
284546
  }, stop: () => ({ firedReason: null }) };
284539
284547
  }
284540
284548
  const startSec = clock2();
284541
284549
  let lastProgressSec = startSec;
284550
+ let lastStateSec = startSec;
284542
284551
  let lastCommitCount = -1;
284552
+ let lastSignature;
284543
284553
  let firedReason = null;
284544
284554
  let running = false;
284545
284555
  const markProgress = () => {
@@ -284554,16 +284564,32 @@ function startSpawnTimeoutWatchdog(opts) {
284554
284564
  const n = await commitCount();
284555
284565
  if (n > lastCommitCount) {
284556
284566
  lastCommitCount = n;
284557
- lastProgressSec = clock2();
284567
+ const now3 = clock2();
284568
+ lastProgressSec = now3;
284569
+ lastStateSec = now3;
284558
284570
  }
284559
284571
  } catch {
284560
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
+ }
284561
284585
  const now2 = clock2();
284562
284586
  const verdict = cycleTimeoutVerdict({
284563
284587
  elapsedSec: now2 - startSec,
284564
284588
  idleSec: now2 - lastProgressSec,
284589
+ stateIdleSec: now2 - lastStateSec,
284565
284590
  wallLimitSec: thresholds.wallSec,
284566
- noProgressLimitSec: thresholds.noProgressSec
284591
+ noProgressLimitSec: thresholds.noProgressSec,
284592
+ noStateChangeLimitSec: thresholds.noStateChangeSec
284567
284593
  });
284568
284594
  if (verdict.timedOut) {
284569
284595
  firedReason = verdict.reason;
@@ -284593,6 +284619,12 @@ function startSpawnTimeoutWatchdog(opts) {
284593
284619
  lastCommitCount = await commitCount();
284594
284620
  } catch {
284595
284621
  }
284622
+ if (stateSignature !== void 0) {
284623
+ try {
284624
+ lastSignature = await stateSignature();
284625
+ } catch {
284626
+ }
284627
+ }
284596
284628
  })();
284597
284629
  const timer = setInterval(() => void tick(), pollMs);
284598
284630
  timer.unref?.();
@@ -284760,11 +284792,13 @@ async function executeSpawnAgentCommand(cmd, ports, ctx) {
284760
284792
  appendEvent: (ev) => ports.events.appendEvent(ports.paths.eventsPath, ev),
284761
284793
  thresholdSec: readStallThreshold(ports.repoCwd).thresholdSec
284762
284794
  });
284795
+ const statusSignature = ports.git.worktreeStatusSignature;
284763
284796
  const timeoutWatchdog = startSpawnTimeoutWatchdog({
284764
284797
  cycleId: ctx.cycleId ?? "",
284765
284798
  thresholds: readCycleTimeoutThresholds(ports.repoCwd),
284766
284799
  clock: ports.clock,
284767
284800
  commitCount: () => ports.git.commitsAhead(execCwd, observeBase),
284801
+ ...statusSignature !== void 0 ? { stateSignature: () => statusSignature(execCwd) } : {},
284768
284802
  appendEvent: (ev) => ports.events.appendEvent(ports.paths.eventsPath, ev)
284769
284803
  });
284770
284804
  const skillBodyForSpawn = maybeInjectProjectMap(
@@ -287752,6 +287786,13 @@ function nodePorts(opts) {
287752
287786
  const n = Number((r.stdout ?? "0").trim());
287753
287787
  return Number.isFinite(n) ? n : 0;
287754
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
+ },
287755
287796
  async mainAhead(repoCwd) {
287756
287797
  const r = await execFileAsync17("git", ["rev-list", "--count", "origin/main..main"], {
287757
287798
  cwd: repoCwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "4.720.1",
3
+ "version": "4.721.1",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "packageManager": "pnpm@11.1.3",
6
6
  "scripts": {