@seanyao/roll 4.720.1 → 4.721.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/roll.mjs +199 -42
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v4.721.2 — 2026-07-21
6
+
7
+ ### 稳定性
8
+ - loop 的构建子进程失联或被系统杀掉后,现在会在限定时间内察觉、收尾并写入终态(标记为 aborted),不再让整个受监督周期永久挂住;新增子进程存活/超时检测(FIX-1474)[loop]
9
+
10
+ ## v4.721.1 — 2026-07-21
11
+
12
+ ### 稳定性
13
+ - loop 看门狗的"进展"判定从 stdout 改为 git 状态(新 commit / 工作区文件变动):输出缓冲型 agent 写文件时不再被 15 分钟无进展误杀;新增 `no-state-change` 杀因——默认 25 分钟无 commit 且无文件变动即终止(即使 stdout 仍在输出),原地打转的 cycle 不再烧满 45 分钟墙钟;阈值可用 `ROLL_CYCLE_NO_STATE_CHANGE_SEC` 覆盖(FIX-1477)[loop]
14
+ - attest 闸的"无 AC 块豁免"提前到渲染之前评估:无 AC 块的卡不再因渲染失败或空壳报告被硬拦,直接合法放行(FIX-1476)[loop]
15
+
5
16
  ## v4.720.1 — 2026-07-20
6
17
 
7
18
  ### 稳定性
package/dist/roll.mjs CHANGED
@@ -14639,18 +14639,36 @@ function timeoutTeardownCommands(ctx) {
14639
14639
  { kind: "release_lock" }
14640
14640
  ];
14641
14641
  }
14642
+ function abortedTeardownCommands(ctx) {
14643
+ return [
14644
+ { kind: "kill_agent", graceSec: WATCHDOG_KILL_GRACE_SEC },
14645
+ { kind: "measure_worktree" },
14646
+ { kind: "emit_event", event: cycleEndEvent(ctx, "aborted") },
14647
+ { kind: "append_run", status: "aborted", outcome: mapV2Status("aborted"), cycleId: ctx.cycleId },
14648
+ {
14649
+ kind: "append_alert",
14650
+ message: `cycle ${ctx.cycleId}: builder child process lost (killed out-of-band) \u2014 cycle aborted; worktree preserved`
14651
+ },
14652
+ { kind: "release_lock" }
14653
+ ];
14654
+ }
14642
14655
  function cycleTimeoutVerdict(input) {
14643
14656
  const wall = input.wallLimitSec ?? CYCLE_WALL_TIMEOUT_SEC;
14644
14657
  const idle = input.noProgressLimitSec ?? CYCLE_NO_PROGRESS_SEC;
14658
+ const stateIdle = input.noStateChangeLimitSec ?? CYCLE_NO_STATE_CHANGE_SEC;
14645
14659
  if (wall > 0 && input.elapsedSec >= wall) {
14646
14660
  return { timedOut: true, reason: "wall", elapsedSec: input.elapsedSec, idleSec: input.idleSec };
14647
14661
  }
14648
14662
  if (idle > 0 && input.idleSec >= idle) {
14649
14663
  return { timedOut: true, reason: "no-progress", elapsedSec: input.elapsedSec, idleSec: input.idleSec };
14650
14664
  }
14665
+ if (input.stateIdleSec !== void 0 && stateIdle > 0 && input.stateIdleSec >= stateIdle) {
14666
+ return { timedOut: true, reason: "no-state-change", elapsedSec: input.elapsedSec, idleSec: input.idleSec };
14667
+ }
14651
14668
  const wallRemain = wall > 0 ? wall - input.elapsedSec : Number.POSITIVE_INFINITY;
14652
14669
  const idleRemain = idle > 0 ? idle - input.idleSec : Number.POSITIVE_INFINITY;
14653
- return { timedOut: false, remainingSec: Math.min(wallRemain, idleRemain) };
14670
+ const stateRemain = input.stateIdleSec !== void 0 && stateIdle > 0 ? stateIdle - input.stateIdleSec : Number.POSITIVE_INFINITY;
14671
+ return { timedOut: false, remainingSec: Math.min(wallRemain, idleRemain, stateRemain) };
14654
14672
  }
14655
14673
  function stallVerdict(input) {
14656
14674
  const threshold = input.stallThresholdSec ?? CYCLE_STALL_THRESHOLD_SEC;
@@ -14828,6 +14846,12 @@ function cycleStep(state, event) {
14828
14846
  { kind: "cleanup_worktree", branch: state.ctx.branch }
14829
14847
  ]);
14830
14848
  case "agent_exited": {
14849
+ if (event.lost === true) {
14850
+ return {
14851
+ state: { ...state, phase: "execute", terminal: "aborted", done: true, ctx: { ...state.ctx, agentExitCode: event.exit, agentTimedOut: event.timedOut } },
14852
+ commands: abortedTeardownCommands(terminalCtx(state))
14853
+ };
14854
+ }
14831
14855
  const plan = retryPlan({ attempt: state.attempt, exit: event.exit, timedOut: event.timedOut });
14832
14856
  if (plan.action === "abort_timeout") {
14833
14857
  return {
@@ -15141,7 +15165,7 @@ function adversarialDegradedCmd(cycleId, storyId, cause, from = "adversarial") {
15141
15165
  function initialCycleState(ctx) {
15142
15166
  return { phase: "pick", ctx, attempt: 0, done: false };
15143
15167
  }
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;
15168
+ 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
15169
  var init_orchestrator = __esm({
15146
15170
  "packages/core/dist/loop/orchestrator.js"() {
15147
15171
  "use strict";
@@ -15153,6 +15177,7 @@ var init_orchestrator = __esm({
15153
15177
  WATCHDOG_KILL_GRACE_SEC = 5;
15154
15178
  CYCLE_WALL_TIMEOUT_SEC = 2700;
15155
15179
  CYCLE_NO_PROGRESS_SEC = 900;
15180
+ CYCLE_NO_STATE_CHANGE_SEC = 1500;
15156
15181
  CYCLE_STALL_THRESHOLD_SEC = 600;
15157
15182
  STALL_STARTUP_GRACE_SEC = 120;
15158
15183
  MAX_AGENT_ATTEMPTS = 3;
@@ -246181,6 +246206,10 @@ function spawnAndWait(bin, args, opts, pty = false, profile) {
246181
246206
  stderr += d.toString("utf8");
246182
246207
  });
246183
246208
  liveAgents.add(child);
246209
+ try {
246210
+ opts.onSpawn?.(child);
246211
+ } catch {
246212
+ }
246184
246213
  let settled = false;
246185
246214
  let exitDrainFallback;
246186
246215
  const settle = (result2) => {
@@ -251254,6 +251283,11 @@ function runAttestGate(worktreeCwd, storyId, cycleId, mode, sinceSec, sinks, sco
251254
251283
  sinks.event({ cycleId, verdict: "skipped", reasons: reasons2 });
251255
251284
  return { verdict: "skipped", mode, reasons: reasons2, blocked: blocked4 };
251256
251285
  }
251286
+ if (storyHasAcBlock(specRepoCwd, storyId) === false) {
251287
+ const reasons2 = ["story has no AC block; acceptance report not required"];
251288
+ sinks.event({ cycleId, verdict: "produced", reasons: reasons2 });
251289
+ return { verdict: "produced", mode, reasons: reasons2, blocked: false };
251290
+ }
251257
251291
  if (renderExitCode !== 0) {
251258
251292
  const reasons2 = [`attest render failed for ${storyId} (exit ${renderExitCode})`];
251259
251293
  const blocked4 = mode === "hard";
@@ -251261,11 +251295,6 @@ function runAttestGate(worktreeCwd, storyId, cycleId, mode, sinceSec, sinks, sco
251261
251295
  sinks.event({ cycleId, verdict: "skipped", reasons: reasons2 });
251262
251296
  return { verdict: "skipped", mode, reasons: reasons2, blocked: blocked4 };
251263
251297
  }
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
251298
  const diagnostics = violatesMustDeclareSurface(specRepoCwd, storyId) ? [MUST_DECLARE_FAIL_REASON] : [];
251270
251299
  const redAcs = redAcFailures(worktreeCwd, storyId, scoreRepoCwd);
251271
251300
  if (redAcs.length > 0) {
@@ -284515,7 +284544,8 @@ function readCycleTimeoutThresholds(repoCwd) {
284515
284544
  }
284516
284545
  return {
284517
284546
  wallSec: envNum("ROLL_CYCLE_WALL_TIMEOUT_SEC") ?? wallSec,
284518
- noProgressSec: envNum("ROLL_CYCLE_NO_PROGRESS_SEC") ?? noProgressSec
284547
+ noProgressSec: envNum("ROLL_CYCLE_NO_PROGRESS_SEC") ?? noProgressSec,
284548
+ noStateChangeSec: envNum("ROLL_CYCLE_NO_STATE_CHANGE_SEC") ?? CYCLE_NO_STATE_CHANGE_SEC
284519
284549
  };
284520
284550
  }
284521
284551
  function readStallThreshold(repoCwd) {
@@ -284531,15 +284561,18 @@ function readStallThreshold(repoCwd) {
284531
284561
  }
284532
284562
  function startSpawnTimeoutWatchdog(opts) {
284533
284563
  const { cycleId, thresholds, clock: clock2, commitCount, appendEvent: appendEvent3 } = opts;
284564
+ const stateSignature = opts.stateSignature;
284534
284565
  const kill = opts.kill ?? (() => killLiveAgents("SIGKILL"));
284535
284566
  const pollMs = opts.pollMs ?? (Number((process.env["ROLL_TIMEOUT_POLL_MS"] ?? "").trim()) || TIMEOUT_POLL_MS);
284536
- if (thresholds.wallSec <= 0 && thresholds.noProgressSec <= 0) {
284567
+ if (thresholds.wallSec <= 0 && thresholds.noProgressSec <= 0 && thresholds.noStateChangeSec <= 0) {
284537
284568
  return { markProgress: () => {
284538
284569
  }, stop: () => ({ firedReason: null }) };
284539
284570
  }
284540
284571
  const startSec = clock2();
284541
284572
  let lastProgressSec = startSec;
284573
+ let lastStateSec = startSec;
284542
284574
  let lastCommitCount = -1;
284575
+ let lastSignature;
284543
284576
  let firedReason = null;
284544
284577
  let running = false;
284545
284578
  const markProgress = () => {
@@ -284554,16 +284587,32 @@ function startSpawnTimeoutWatchdog(opts) {
284554
284587
  const n = await commitCount();
284555
284588
  if (n > lastCommitCount) {
284556
284589
  lastCommitCount = n;
284557
- lastProgressSec = clock2();
284590
+ const now3 = clock2();
284591
+ lastProgressSec = now3;
284592
+ lastStateSec = now3;
284558
284593
  }
284559
284594
  } catch {
284560
284595
  }
284596
+ if (stateSignature !== void 0) {
284597
+ try {
284598
+ const sig = await stateSignature();
284599
+ if (lastSignature !== void 0 && sig !== lastSignature) {
284600
+ const now3 = clock2();
284601
+ lastProgressSec = now3;
284602
+ lastStateSec = now3;
284603
+ }
284604
+ lastSignature = sig;
284605
+ } catch {
284606
+ }
284607
+ }
284561
284608
  const now2 = clock2();
284562
284609
  const verdict = cycleTimeoutVerdict({
284563
284610
  elapsedSec: now2 - startSec,
284564
284611
  idleSec: now2 - lastProgressSec,
284612
+ stateIdleSec: now2 - lastStateSec,
284565
284613
  wallLimitSec: thresholds.wallSec,
284566
- noProgressLimitSec: thresholds.noProgressSec
284614
+ noProgressLimitSec: thresholds.noProgressSec,
284615
+ noStateChangeLimitSec: thresholds.noStateChangeSec
284567
284616
  });
284568
284617
  if (verdict.timedOut) {
284569
284618
  firedReason = verdict.reason;
@@ -284593,6 +284642,12 @@ function startSpawnTimeoutWatchdog(opts) {
284593
284642
  lastCommitCount = await commitCount();
284594
284643
  } catch {
284595
284644
  }
284645
+ if (stateSignature !== void 0) {
284646
+ try {
284647
+ lastSignature = await stateSignature();
284648
+ } catch {
284649
+ }
284650
+ }
284596
284651
  })();
284597
284652
  const timer = setInterval(() => void tick(), pollMs);
284598
284653
  timer.unref?.();
@@ -284662,6 +284717,68 @@ function startStallDetector(opts) {
284662
284717
  }
284663
284718
  };
284664
284719
  }
284720
+ var LIVENESS_POLL_MS = 5e3;
284721
+ var LIVENESS_CONFIRM_TICKS = 2;
284722
+ function defaultIsAlive(pid) {
284723
+ try {
284724
+ process.kill(pid, 0);
284725
+ return true;
284726
+ } catch (e) {
284727
+ return e.code === "EPERM";
284728
+ }
284729
+ }
284730
+ function startBuilderLivenessProbe(opts) {
284731
+ const { cycleId, agent, appendEvent: appendEvent3 } = opts;
284732
+ const isAlive = opts.isAlive ?? defaultIsAlive;
284733
+ const kill = opts.kill ?? (() => killLiveAgents("SIGKILL"));
284734
+ const pollMs = opts.pollMs ?? (Number((process.env["ROLL_LIVENESS_POLL_MS"] ?? "").trim()) || LIVENESS_POLL_MS);
284735
+ const confirmTicks = opts.confirmTicks ?? LIVENESS_CONFIRM_TICKS;
284736
+ let lost = false;
284737
+ let deadStreak = 0;
284738
+ const tick = () => {
284739
+ if (lost || !opts.spawnPending())
284740
+ return;
284741
+ const pid = opts.pid();
284742
+ if (pid === void 0) {
284743
+ deadStreak = 0;
284744
+ return;
284745
+ }
284746
+ let alive = true;
284747
+ try {
284748
+ alive = isAlive(pid);
284749
+ } catch {
284750
+ }
284751
+ if (alive) {
284752
+ deadStreak = 0;
284753
+ return;
284754
+ }
284755
+ deadStreak += 1;
284756
+ if (deadStreak < confirmTicks)
284757
+ return;
284758
+ lost = true;
284759
+ clearInterval(timer);
284760
+ try {
284761
+ appendEvent3({ type: "cycle:agent_lost", cycleId, agent, pid, ts: Date.now() });
284762
+ } catch {
284763
+ }
284764
+ try {
284765
+ kill();
284766
+ } catch {
284767
+ }
284768
+ try {
284769
+ opts.onLost?.({ pid });
284770
+ } catch {
284771
+ }
284772
+ };
284773
+ const timer = setInterval(tick, pollMs);
284774
+ timer.unref?.();
284775
+ return {
284776
+ stop: () => {
284777
+ clearInterval(timer);
284778
+ return { lost };
284779
+ }
284780
+ };
284781
+ }
284665
284782
  function createCaptureMarkerSink(runDir, capture) {
284666
284783
  let buf = "";
284667
284784
  const pending = [];
@@ -284760,11 +284877,13 @@ async function executeSpawnAgentCommand(cmd, ports, ctx) {
284760
284877
  appendEvent: (ev) => ports.events.appendEvent(ports.paths.eventsPath, ev),
284761
284878
  thresholdSec: readStallThreshold(ports.repoCwd).thresholdSec
284762
284879
  });
284880
+ const statusSignature = ports.git.worktreeStatusSignature;
284763
284881
  const timeoutWatchdog = startSpawnTimeoutWatchdog({
284764
284882
  cycleId: ctx.cycleId ?? "",
284765
284883
  thresholds: readCycleTimeoutThresholds(ports.repoCwd),
284766
284884
  clock: ports.clock,
284767
284885
  commitCount: () => ports.git.commitsAhead(execCwd, observeBase),
284886
+ ...statusSignature !== void 0 ? { stateSignature: () => statusSignature(execCwd) } : {},
284768
284887
  appendEvent: (ev) => ports.events.appendEvent(ports.paths.eventsPath, ev)
284769
284888
  });
284770
284889
  const skillBodyForSpawn = maybeInjectProjectMap(
@@ -284783,6 +284902,24 @@ ${skillBodyForSpawn}` : skillBodyForSpawn;
284783
284902
  let timeoutFired = null;
284784
284903
  let activeMainLeak = { detected: false, files: [] };
284785
284904
  let mainLeakWatchdog;
284905
+ let spawnedPid;
284906
+ let spawnSettled = false;
284907
+ let agentLost = false;
284908
+ let resolveLostRace;
284909
+ const lostRace = new Promise((resolve17) => {
284910
+ resolveLostRace = () => resolve17({ stdout: "", stderr: "", exitCode: 137, timedOut: false });
284911
+ });
284912
+ const livenessProbe = startBuilderLivenessProbe({
284913
+ cycleId: ctx.cycleId ?? "",
284914
+ agent: cmd.agent,
284915
+ pid: () => spawnedPid,
284916
+ spawnPending: () => !spawnSettled,
284917
+ appendEvent: (ev) => ports.events.appendEvent(ports.paths.eventsPath, ev),
284918
+ onLost: () => {
284919
+ agentLost = true;
284920
+ resolveLostRace?.();
284921
+ }
284922
+ });
284786
284923
  try {
284787
284924
  appendWriteProtectionEvent(ports, applyMainCheckoutWriteProtection({
284788
284925
  repoCwd: ports.repoCwd,
@@ -284791,37 +284928,47 @@ ${skillBodyForSpawn}` : skillBodyForSpawn;
284791
284928
  nowMs: () => eventTs2(ports)
284792
284929
  }));
284793
284930
  mainLeakWatchdog = startMainCheckoutLeakWatchdog(ports, ctx);
284794
- res = await ports.agentSpawn(cmd.agent, {
284795
- purpose: "builder",
284796
- // E4: the builder runs in the submodule cycle worktree for a submodule
284797
- // story (execCwd); its git env + writable roots target the submodule's
284798
- // own repo/object store so its commits actually land there.
284799
- cwd: execCwd,
284800
- skillBody: finalSkillBody,
284801
- ...ctx.evidenceRunDir !== void 0 ? { runDir: ctx.evidenceRunDir } : {},
284802
- writableRoots: submoduleAgentWritableRoots(ports.repoCwd, execRepoCwd, ports.paths.alertsPath),
284803
- ...ctx.model !== void 0 && ctx.model !== "" ? { model: ctx.model } : {},
284804
- env: {
284805
- ...process.env,
284806
- ROLL_LOOP_ALERT: ports.paths.alertsPath,
284807
- ...worktreeGitEnv(execCwd, ports.repoCwd),
284808
- ...agentSpawnEnvironment(cmd.agent)
284809
- },
284810
- // FIX-204B: pin the executor-picked story into the agent prompt — the
284811
- // claim (pick_story 🔨) and the work must be the same story.
284812
- ...ctx.storyId !== void 0 && ctx.storyId !== "" ? { storyId: ctx.storyId } : {},
284813
- onChunk: (d) => {
284814
- timeoutWatchdog.markProgress();
284815
- stallDetector.markProgress();
284816
- captureSink?.onChunk(d);
284817
- try {
284818
- appendFileSync16(livePath, d);
284819
- } catch {
284931
+ res = await Promise.race([
284932
+ ports.agentSpawn(cmd.agent, {
284933
+ purpose: "builder",
284934
+ // E4: the builder runs in the submodule cycle worktree for a submodule
284935
+ // story (execCwd); its git env + writable roots target the submodule's
284936
+ // own repo/object store so its commits actually land there.
284937
+ cwd: execCwd,
284938
+ skillBody: finalSkillBody,
284939
+ ...ctx.evidenceRunDir !== void 0 ? { runDir: ctx.evidenceRunDir } : {},
284940
+ writableRoots: submoduleAgentWritableRoots(ports.repoCwd, execRepoCwd, ports.paths.alertsPath),
284941
+ ...ctx.model !== void 0 && ctx.model !== "" ? { model: ctx.model } : {},
284942
+ env: {
284943
+ ...process.env,
284944
+ ROLL_LOOP_ALERT: ports.paths.alertsPath,
284945
+ ...worktreeGitEnv(execCwd, ports.repoCwd),
284946
+ ...agentSpawnEnvironment(cmd.agent)
284947
+ },
284948
+ // FIX-204B: pin the executor-picked story into the agent prompt the
284949
+ // claim (pick_story 🔨) and the work must be the same story.
284950
+ ...ctx.storyId !== void 0 && ctx.storyId !== "" ? { storyId: ctx.storyId } : {},
284951
+ onChunk: (d) => {
284952
+ timeoutWatchdog.markProgress();
284953
+ stallDetector.markProgress();
284954
+ captureSink?.onChunk(d);
284955
+ try {
284956
+ appendFileSync16(livePath, d);
284957
+ } catch {
284958
+ }
284959
+ signalRecorder.accept(d);
284960
+ },
284961
+ // FIX-1474: hand the live child to the liveness probe so an
284962
+ // out-of-band death is detected within a bounded window.
284963
+ onSpawn: (child) => {
284964
+ spawnedPid = child.pid;
284820
284965
  }
284821
- signalRecorder.accept(d);
284822
- }
284823
- });
284966
+ }),
284967
+ lostRace
284968
+ ]);
284824
284969
  } finally {
284970
+ spawnSettled = true;
284971
+ livenessProbe.stop();
284825
284972
  if (mainLeakWatchdog !== void 0) {
284826
284973
  activeMainLeak = await mainLeakWatchdog.stop();
284827
284974
  }
@@ -284929,7 +285076,7 @@ ${res.stderr}`.split("\n").find((l) => l.trim() !== "") ?? "").slice(0, 200);
284929
285076
  // FIX-343 (step ①): the builder session id is part of the auditable
284930
285077
  // header — the attest gate's scorer≠builder-session invariant is then
284931
285078
  // traceable to a recorded build-session id, not asserted.
284932
- `# exit=${res.exitCode} timedOut=${res.timedOut} build-session=${builderSessionId}
285079
+ `# exit=${res.exitCode} timedOut=${res.timedOut} lost=${agentLost} build-session=${builderSessionId}
284933
285080
  --- stdout ---
284934
285081
  ${res.stdout}
284935
285082
  --- stderr ---
@@ -284977,7 +285124,10 @@ ${res.stderr}
284977
285124
  cycleStartSec: ctx.startSec
284978
285125
  }) ?? void 0 : void 0;
284979
285126
  return {
284980
- event: { type: "agent_exited", exit: res.exitCode, timedOut: res.timedOut },
285127
+ // FIX-1474: a lost child converges the cycle to the explicit `aborted`
285128
+ // terminal in the orchestrator (no retry, no blocked) — the death was
285129
+ // environmental, recorded fail-loud via cycle:agent_lost.
285130
+ event: { type: "agent_exited", exit: res.exitCode, timedOut: res.timedOut, ...agentLost ? { lost: true } : {} },
284981
285131
  // FIX-343 (step ①): persist the builder session id on the cycle context so
284982
285132
  // it survives to the attest gate (the scorer≠builder-session invariant is then
284983
285133
  // traceable to a recorded build-session id, not asserted).
@@ -287752,6 +287902,13 @@ function nodePorts(opts) {
287752
287902
  const n = Number((r.stdout ?? "0").trim());
287753
287903
  return Number.isFinite(n) ? n : 0;
287754
287904
  },
287905
+ async worktreeStatusSignature(worktreeCwd) {
287906
+ const r = await execFileAsync17("git", ["status", "--porcelain"], {
287907
+ cwd: worktreeCwd,
287908
+ encoding: "utf8"
287909
+ });
287910
+ return r.stdout ?? "";
287911
+ },
287755
287912
  async mainAhead(repoCwd) {
287756
287913
  const r = await execFileAsync17("git", ["rev-list", "--count", "origin/main..main"], {
287757
287914
  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.2",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "packageManager": "pnpm@11.1.3",
6
6
  "scripts": {