@seanyao/roll 4.702.2 → 4.702.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,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v4.702.3 — 2026-07-02
6
+
7
+ ### 稳定性
8
+ - `roll init` 现在会负责提交并推送自己生成的项目约定文件,`roll loop go` 不再一开工就撞 dirty(FIX-1072)`[loop-engine]`
9
+ - Builder 提交会被钉在 cycle worktree,不再把提交漏到主 checkout(FIX-1073)`[loop-engine]`
10
+ - 状态面板给出的 AI client 修复命令现在可以直接执行,不再多带无效参数(FIX-1070)`[cli]`
11
+
12
+ ### 可见性
13
+ - `roll loop watch` 不再把旧 cycle 的卡片和 agent 贴到新的空转或重试状态上(FIX-1074)`[loop-observability]`
14
+
5
15
  ## v4.702.2 — 2026-07-02
6
16
 
7
17
  ### Breaking changes
package/README.md CHANGED
@@ -53,6 +53,10 @@ waits for owner confirmation before writing. Automation must use
53
53
  After any init path, `roll next` is the continuation button: it reads the same
54
54
  brief, onboard plan, backlog, and Roll markers, then prints one best next
55
55
  command instead of a menu.
56
+ When `roll init` writes Roll-owned meta files inside a git worktree, it also
57
+ adds, commits, and pushes those files to `origin` when possible, then prints the
58
+ commit/push result. Product files you created yourself are not included in that
59
+ finalization commit.
56
60
  First time through? Start with [Getting started](guide/en/getting-started.md).
57
61
 
58
62
  ## Language Surfaces
@@ -205,7 +209,7 @@ with `roll loop resume` when ready.
205
209
  | `roll loop <on\|off\|go\|watch\|runs\|cycles\|cycle\|alert\|…>` | Run, observe, and maintain the autonomous executor |
206
210
  | `roll next` | Continue init/onboard with one best next command |
207
211
  | `roll release [--dry-run\|--showcase]` | Release planning/flow plus golden-path showcase support |
208
- | `roll setup [skills\|offboard\|-f]` | Install/sync conventions or remove Roll-owned project artifacts |
212
+ | `roll setup [-f\|--force] [--reselect]` / `roll setup skills\|offboard` | Install/sync conventions or remove Roll-owned project artifacts |
209
213
  | `roll status [ci\|pulse] [--json]` | Project health, CI state, and delivery pulse |
210
214
  | `roll test [--where] [--reset]` | Run tests through the isolation adapter |
211
215
  | `roll update` | Upgrade the global Roll install and re-sync conventions |
package/dist/roll.mjs CHANGED
@@ -12475,6 +12475,12 @@ function signalLabel(ev) {
12475
12475
  return `pair ${clean2(ev.peer)} ${ev.cause}`;
12476
12476
  case "pair:excluded":
12477
12477
  return `pair ${clean2(ev.agent)} excluded ${ev.cause} (${ev.failures})`;
12478
+ case "agent:retry":
12479
+ return `retry ${clean2(ev.fromAgent)} \u2192 ${clean2(ev.toAgent)} (${clean2(ev.reason)})`;
12480
+ case "goal:gate_tripped":
12481
+ return `paused ${clean2(ev.reason)} (${ev.gate})`;
12482
+ case "goal:state":
12483
+ return `goal ${clean2(ev.to)} ${clean2(ev.reason)}`;
12478
12484
  default:
12479
12485
  return void 0;
12480
12486
  }
@@ -12486,6 +12492,20 @@ function summarizeWatchEvents(lines3, durableLookup, nowMs) {
12486
12492
  const summary = { tcrCount: 0, hasEnd: false };
12487
12493
  const events = [];
12488
12494
  let seen = false;
12495
+ const resetCycleScope = (cycleId) => {
12496
+ if (summary.cycleId === cycleId)
12497
+ return;
12498
+ summary.cycleId = cycleId;
12499
+ summary.storyId = void 0;
12500
+ summary.agent = void 0;
12501
+ summary.phase = void 0;
12502
+ summary.tcrCount = 0;
12503
+ summary.outcome = void 0;
12504
+ summary.hasEnd = false;
12505
+ summary.acceptedScore = void 0;
12506
+ summary.attest = void 0;
12507
+ summary.lastPr = void 0;
12508
+ };
12489
12509
  for (const line of lines3) {
12490
12510
  const ev = parseEventLine(line);
12491
12511
  if (ev === null)
@@ -12493,26 +12513,26 @@ function summarizeWatchEvents(lines3, durableLookup, nowMs) {
12493
12513
  events.push(ev);
12494
12514
  seen = true;
12495
12515
  if (ev.type === "cycle:start") {
12496
- summary.cycleId = ev.cycleId;
12516
+ resetCycleScope(ev.cycleId);
12497
12517
  summary.storyId = ev.storyId;
12498
12518
  summary.agent = ev.agent;
12499
- summary.phase = void 0;
12500
- summary.tcrCount = 0;
12501
- summary.outcome = void 0;
12502
- summary.hasEnd = false;
12503
- summary.acceptedScore = void 0;
12504
- summary.attest = void 0;
12505
- summary.lastPr = void 0;
12506
12519
  } else if (ev.type === "cycle:phase") {
12507
- summary.cycleId = ev.cycleId;
12520
+ resetCycleScope(ev.cycleId);
12508
12521
  summary.phase = ev.phase;
12509
12522
  } else if (ev.type === "cycle:tcr") {
12510
- summary.cycleId = ev.cycleId;
12523
+ resetCycleScope(ev.cycleId);
12511
12524
  summary.tcrCount += 1;
12512
- } else if (ev.type === "cycle:end" || ev.type === "cycle:terminal") {
12513
- summary.cycleId = ev.cycleId;
12525
+ } else if (ev.type === "cycle:end") {
12526
+ resetCycleScope(ev.cycleId);
12527
+ summary.outcome = ev.outcome;
12528
+ summary.hasEnd = true;
12529
+ } else if (ev.type === "cycle:terminal") {
12530
+ resetCycleScope(ev.cycleId);
12531
+ summary.storyId = ev.storyId;
12532
+ summary.agent = ev.agent;
12514
12533
  summary.outcome = ev.outcome;
12515
12534
  summary.hasEnd = true;
12535
+ summary.tcrCount = ev.tcr.present ? ev.tcr.value : 0;
12516
12536
  }
12517
12537
  if (ev.type === "pair:score") {
12518
12538
  summary.acceptedScore = { peer: ev.peer, score: ev.score, verdict: ev.verdict };
@@ -12584,7 +12604,7 @@ function renderWatchStatusSummary(summary, nowMs) {
12584
12604
  const story = summary.storyId !== void 0 && summary.storyId !== "" ? summary.storyId : "story unknown";
12585
12605
  const agent = summary.agent !== void 0 && summary.agent !== "" ? summary.agent : "agent unknown";
12586
12606
  const phase = summary.phase !== void 0 ? `phase ${summary.phase}` : "phase unknown";
12587
- const classification = summary.classification !== void 0 ? summary.classification : "active";
12607
+ const classification = summary.classification !== void 0 ? summary.classification : summary.hasEnd ? "ended" : "active";
12588
12608
  const tcr = `${summary.tcrCount} TCR`;
12589
12609
  const parts = [`status ${phase}`, classification, quietText(summary, nowMs), story, agent, cycle];
12590
12610
  if (summary.microStep !== void 0) {
@@ -12656,11 +12676,18 @@ function previousTransitionLine(previous) {
12656
12676
  return ` previous: ${parts.join(" \xB7 ")}`;
12657
12677
  }
12658
12678
  function nextTransitionLine(current, ctx) {
12679
+ if (current.storyId === void 0 || current.storyId === "") {
12680
+ const outcome = current.hasEnd ? current.outcome ?? "unknown" : "no active story";
12681
+ return ` next: no runnable work \xB7 ${outcome}`;
12682
+ }
12659
12683
  const story = current.storyId !== void 0 && current.storyId !== "" ? current.storyId : "story unknown";
12660
12684
  const brief = current.storyId !== void 0 && current.storyId !== "" ? ctx?.storyBrief?.(current.storyId) ?? "brief unavailable" : "brief unavailable";
12661
12685
  return ` next: ${story} \xB7 ${brief}`;
12662
12686
  }
12663
12687
  function builderTransitionLine(current, ctx) {
12688
+ if (current.storyId === void 0 || current.storyId === "") {
12689
+ return " builder: none \xB7 no story/agent selected";
12690
+ }
12664
12691
  const route = current.storyId !== void 0 && current.storyId !== "" ? ctx?.routeReason?.(current.storyId) : void 0;
12665
12692
  if (route !== void 0) {
12666
12693
  return ` builder: ${route.agent} \xB7 selected by ${route.reason}`;
@@ -18793,16 +18820,16 @@ async function crontabRead() {
18793
18820
  }
18794
18821
  }
18795
18822
  async function crontabWrite(text2) {
18796
- return new Promise((resolve11, reject) => {
18823
+ return new Promise((resolve12, reject) => {
18797
18824
  const child = execFile4("crontab", ["-"], (err16) => {
18798
18825
  if (err16 === null) {
18799
- resolve11(0);
18826
+ resolve12(0);
18800
18827
  } else if (typeof err16.code === "number") {
18801
- resolve11(err16.code);
18828
+ resolve12(err16.code);
18802
18829
  } else if (err16.code === "ENOENT") {
18803
18830
  reject(err16);
18804
18831
  } else {
18805
- resolve11(1);
18832
+ resolve12(1);
18806
18833
  }
18807
18834
  });
18808
18835
  child.stdin?.end(text2);
@@ -20846,14 +20873,14 @@ var init_mcp = __esm({
20846
20873
  child.on("exit", () => this.rejectAll(new Error("MCP server exited")));
20847
20874
  }
20848
20875
  static async open(child) {
20849
- await new Promise((resolve11, reject) => {
20876
+ await new Promise((resolve12, reject) => {
20850
20877
  const onError = (cause) => {
20851
20878
  child.off("spawn", onSpawn);
20852
20879
  reject(cause);
20853
20880
  };
20854
20881
  const onSpawn = () => {
20855
20882
  child.off("error", onError);
20856
- resolve11();
20883
+ resolve12();
20857
20884
  };
20858
20885
  child.once("error", onError);
20859
20886
  child.once("spawn", onSpawn);
@@ -20882,8 +20909,8 @@ var init_mcp = __esm({
20882
20909
  const id = this.nextId;
20883
20910
  this.nextId += 1;
20884
20911
  const message = { jsonrpc: "2.0", id, method, params };
20885
- return new Promise((resolve11, reject) => {
20886
- this.pending.set(id, { resolve: resolve11, reject });
20912
+ return new Promise((resolve12, reject) => {
20913
+ this.pending.set(id, { resolve: resolve12, reject });
20887
20914
  this.write(message);
20888
20915
  });
20889
20916
  }
@@ -20978,7 +21005,7 @@ async function requestWithRedirects(url, input, deps, timeoutMs, redirects, star
20978
21005
  return response;
20979
21006
  }
20980
21007
  function requestOnce(url, input, deps, timeoutMs, startedAt) {
20981
- return new Promise((resolve11, reject) => {
21008
+ return new Promise((resolve12, reject) => {
20982
21009
  const proxy = proxyFor(url);
20983
21010
  const viaProxy = proxy !== void 0;
20984
21011
  const requestUrl = viaProxy ? proxy : url;
@@ -21000,7 +21027,7 @@ function requestOnce(url, input, deps, timeoutMs, startedAt) {
21000
21027
  });
21001
21028
  res.on("end", () => {
21002
21029
  clearTimeout(timer);
21003
- resolve11({
21030
+ resolve12({
21004
21031
  statusCode: res.statusCode ?? 0,
21005
21032
  headers: normalizeHeaders(res.headers),
21006
21033
  body: deps.redact(Buffer.concat(chunks).toString("utf8")),
@@ -21054,7 +21081,7 @@ function isTimeout(cause) {
21054
21081
  function delay(ms) {
21055
21082
  if (ms <= 0)
21056
21083
  return Promise.resolve();
21057
- return new Promise((resolve11) => setTimeout(resolve11, ms));
21084
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
21058
21085
  }
21059
21086
  function failure5(invocation, startedAt, endedAt, code, message, retryable, detail, attempt) {
21060
21087
  return {
@@ -145685,7 +145712,7 @@ ${lanes.join("\n")}
145685
145712
  }
145686
145713
  }
145687
145714
  function createImportCallExpressionAMD(arg, containsLexicalThis) {
145688
- const resolve11 = factory2.createUniqueName("resolve");
145715
+ const resolve12 = factory2.createUniqueName("resolve");
145689
145716
  const reject = factory2.createUniqueName("reject");
145690
145717
  const parameters = [
145691
145718
  factory2.createParameterDeclaration(
@@ -145694,7 +145721,7 @@ ${lanes.join("\n")}
145694
145721
  /*dotDotDotToken*/
145695
145722
  void 0,
145696
145723
  /*name*/
145697
- resolve11
145724
+ resolve12
145698
145725
  ),
145699
145726
  factory2.createParameterDeclaration(
145700
145727
  /*modifiers*/
@@ -145711,7 +145738,7 @@ ${lanes.join("\n")}
145711
145738
  factory2.createIdentifier("require"),
145712
145739
  /*typeArguments*/
145713
145740
  void 0,
145714
- [factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve11, reject]
145741
+ [factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve12, reject]
145715
145742
  )
145716
145743
  )
145717
145744
  ]);
@@ -232642,8 +232669,8 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
232642
232669
  installPackage(options) {
232643
232670
  this.packageInstallId++;
232644
232671
  const request = { kind: "installPackage", ...options, id: this.packageInstallId };
232645
- const promise = new Promise((resolve11, reject) => {
232646
- (this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve11, reject });
232672
+ const promise = new Promise((resolve12, reject) => {
232673
+ (this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve12, reject });
232647
232674
  });
232648
232675
  this.installer.send(request);
232649
232676
  return promise;
@@ -232936,10 +232963,10 @@ __export(structure_scan_exports, {
232936
232963
  });
232937
232964
  import { createHash as createHash5 } from "node:crypto";
232938
232965
  import { existsSync as existsSync63, readFileSync as readFileSync59 } from "node:fs";
232939
- import { basename as basename13, dirname as dirname32, extname, join as join65, relative as relative5, resolve as resolve6, sep as sep4 } from "node:path";
232966
+ import { basename as basename13, dirname as dirname32, extname, join as join65, relative as relative5, resolve as resolve7, sep as sep4 } from "node:path";
232940
232967
  function buildStaticProjectGraph(input) {
232941
- const root = resolve6(input.root);
232942
- const tsconfigPaths = input.tsconfigPath ? [resolve6(root, input.tsconfigPath)] : findTsconfigs(root);
232968
+ const root = resolve7(input.root);
232969
+ const tsconfigPaths = input.tsconfigPath ? [resolve7(root, input.tsconfigPath)] : findTsconfigs(root);
232943
232970
  if (tsconfigPaths.length === 0) {
232944
232971
  return emptyGraph(root, "No tsconfig.json found for Dream structure scan");
232945
232972
  }
@@ -233086,7 +233113,7 @@ function createLanguageServiceHost(fileNames, options) {
233086
233113
  };
233087
233114
  }
233088
233115
  function normalizePath(root, path) {
233089
- return relative5(root, resolve6(path)).split(sep4).join("/");
233116
+ return relative5(root, resolve7(path)).split(sep4).join("/");
233090
233117
  }
233091
233118
  function isExcluded(root, path) {
233092
233119
  const rel = normalizePath(root, path);
@@ -233200,7 +233227,7 @@ function importEdges(root, sourceFile) {
233200
233227
  function resolveModulePath(root, containingFile, specifier) {
233201
233228
  if (!specifier.startsWith("."))
233202
233229
  return void 0;
233203
- const base = resolve6(dirname32(containingFile), specifier);
233230
+ const base = resolve7(dirname32(containingFile), specifier);
233204
233231
  for (const suffix of [".ts", ".tsx", ".mts", ".cts", "/index.ts"]) {
233205
233232
  const candidate = `${base}${suffix}`;
233206
233233
  if (existsSync63(candidate))
@@ -234138,7 +234165,7 @@ function networkNeeds(command, args) {
234138
234165
  }
234139
234166
  }
234140
234167
  function tcpConnect(host, port, timeoutMs) {
234141
- return new Promise((resolve11, reject) => {
234168
+ return new Promise((resolve12, reject) => {
234142
234169
  const socket = createConnection({ host, port });
234143
234170
  let settled = false;
234144
234171
  const done = (err16) => {
@@ -234150,7 +234177,7 @@ function tcpConnect(host, port, timeoutMs) {
234150
234177
  if (err16)
234151
234178
  reject(err16);
234152
234179
  else
234153
- resolve11();
234180
+ resolve12();
234154
234181
  };
234155
234182
  socket.setTimeout(timeoutMs, () => done(new Error("tcp timeout")));
234156
234183
  socket.once("connect", () => done());
@@ -234162,11 +234189,11 @@ async function networkReachable(probes = {}) {
234162
234189
  host: PROBE_HOST,
234163
234190
  port: PROBE_PORT
234164
234191
  };
234165
- const resolve11 = probes.resolve ?? ((h) => lookup(h));
234192
+ const resolve12 = probes.resolve ?? ((h) => lookup(h));
234166
234193
  const tcpProbe2 = probes.tcpProbe ?? (() => tcpConnect(target.host, target.port, TCP_TIMEOUT_MS));
234167
234194
  try {
234168
234195
  await Promise.race([
234169
- resolve11(target.host),
234196
+ resolve12(target.host),
234170
234197
  new Promise((_, rej) => {
234171
234198
  const timer = setTimeout(() => rej(new Error("dns timeout")), DNS_TIMEOUT_MS);
234172
234199
  if (typeof timer === "object")
@@ -234473,7 +234500,7 @@ init_dist2();
234473
234500
  import { spawn as spawn2 } from "node:child_process";
234474
234501
  import { existsSync as existsSync15, readFileSync as readFileSync12, realpathSync as realpathSync2, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
234475
234502
  import { homedir as homedir4 } from "node:os";
234476
- import { join as join18 } from "node:path";
234503
+ import { join as join18, resolve as resolve5 } from "node:path";
234477
234504
  var liveAgents = /* @__PURE__ */ new Set();
234478
234505
  function killLiveAgents(signal = "SIGKILL") {
234479
234506
  let n = 0;
@@ -234799,12 +234826,28 @@ function evidenceFrameEnv(runDir) {
234799
234826
  };
234800
234827
  }
234801
234828
  var INHERITED_GIT_ENV_KEYS = ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"];
234829
+ function worktreeGitEnv(cwd) {
234830
+ const gitPath = join18(cwd, ".git");
234831
+ if (!existsSync15(gitPath))
234832
+ return {};
234833
+ try {
234834
+ const text2 = readFileSync12(gitPath, "utf8");
234835
+ const match = /^gitdir:\s*(.+)\s*$/m.exec(text2);
234836
+ if (match?.[1] !== void 0 && match[1].trim() !== "") {
234837
+ return { GIT_DIR: resolve5(cwd, match[1].trim()), GIT_WORK_TREE: cwd };
234838
+ }
234839
+ } catch {
234840
+ return { GIT_DIR: gitPath, GIT_WORK_TREE: cwd };
234841
+ }
234842
+ return {};
234843
+ }
234802
234844
  function childEnv(opts) {
234803
234845
  const env = { ...opts.env ?? process.env };
234804
234846
  for (const key of INHERITED_GIT_ENV_KEYS)
234805
234847
  delete env[key];
234806
234848
  env.PWD = opts.cwd;
234807
234849
  delete env.OLDPWD;
234850
+ Object.assign(env, worktreeGitEnv(opts.cwd));
234808
234851
  return opts.runDir !== void 0 && opts.runDir !== "" ? { ...env, ...evidenceFrameEnv(opts.runDir) } : env;
234809
234852
  }
234810
234853
  function withAgentProfileEnv(agent, opts) {
@@ -234820,7 +234863,7 @@ function withAgentProfileEnv(agent, opts) {
234820
234863
  function spawnAndWait(bin, args, opts, pty = false) {
234821
234864
  process.stderr.write(`[runner] spawn ${bin} argv=${JSON.stringify(args.map((a) => a.length > 80 ? `${a.slice(0, 77)}...` : a))}
234822
234865
  `);
234823
- return new Promise((resolve11) => {
234866
+ return new Promise((resolve12) => {
234824
234867
  const child = spawn2(bin, args, {
234825
234868
  cwd: opts.cwd,
234826
234869
  env: childEnv(opts),
@@ -234863,7 +234906,7 @@ function spawnAndWait(bin, args, opts, pty = false) {
234863
234906
  opts.cleanup?.();
234864
234907
  } catch {
234865
234908
  }
234866
- resolve11(result);
234909
+ resolve12(result);
234867
234910
  };
234868
234911
  child.on("error", (e) => {
234869
234912
  settle({ stdout, stderr: `${stderr}${String(e)}
@@ -234890,13 +234933,13 @@ function realDeps() {
234890
234933
  sharedRoot: () => process.env["ROLL_SHARED_ROOT"] || join19(homedir5(), ".shared", "roll"),
234891
234934
  launchdDir: () => join19(homedir5(), "Library", "LaunchAgents"),
234892
234935
  scheduler: createScheduler(process.platform, { uid: process.getuid?.() ?? 501 }),
234893
- execRunner: (runner) => new Promise((resolve11) => {
234936
+ execRunner: (runner) => new Promise((resolve12) => {
234894
234937
  const child = spawn3("bash", [runner], {
234895
234938
  stdio: "inherit",
234896
234939
  env: { ...process.env, ROLL_LOOP_FORCE: "1" }
234897
234940
  });
234898
- child.on("exit", (code) => resolve11(code ?? 1));
234899
- child.on("error", () => resolve11(1));
234941
+ child.on("exit", (code) => resolve12(code ?? 1));
234942
+ child.on("error", () => resolve12(1));
234900
234943
  }),
234901
234944
  hasTmux: () => {
234902
234945
  try {
@@ -234907,7 +234950,7 @@ function realDeps() {
234907
234950
  },
234908
234951
  // The `loop now` inline observation: tail live.log while the cycle holds
234909
234952
  // the inner lock; Ctrl-C stops the TAIL only (the cycle lives in tmux).
234910
- observe: (rt) => new Promise((resolve11) => {
234953
+ observe: (rt) => new Promise((resolve12) => {
234911
234954
  const lock = join19(rt, "inner.lock");
234912
234955
  const tail3 = spawn3("tail", ["-n", "+1", "-F", join19(rt, "live.log")], { stdio: "inherit" });
234913
234956
  let sawLock = false;
@@ -234918,7 +234961,7 @@ function realDeps() {
234918
234961
  } catch {
234919
234962
  }
234920
234963
  process.removeListener("SIGINT", finish);
234921
- resolve11();
234964
+ resolve12();
234922
234965
  };
234923
234966
  const timer = setInterval(() => {
234924
234967
  if (existsSync16(lock))
@@ -249779,13 +249822,13 @@ function followSupervisorCollabStream(projectPath3, noColor2) {
249779
249822
  const tickLimit = envOptionalPositiveInt("ROLL_SUPERVISOR_COLLAB_WATCH_TICKS");
249780
249823
  let renderedCycles = 0;
249781
249824
  let ticks = 0;
249782
- return new Promise((resolve11) => {
249825
+ return new Promise((resolve12) => {
249783
249826
  let timer;
249784
249827
  const stop = (code) => {
249785
249828
  if (timer !== void 0)
249786
249829
  clearInterval(timer);
249787
249830
  process.removeListener("SIGINT", onSigint);
249788
- resolve11(code);
249831
+ resolve12(code);
249789
249832
  };
249790
249833
  const onSigint = () => stop(130);
249791
249834
  const tick = () => {
@@ -249876,7 +249919,7 @@ function unknownSupervisorLiveFlag(args) {
249876
249919
  function followSupervisorLiveBoard(projectPath3, intervalMs) {
249877
249920
  const tickLimit = envOptionalPositiveInt("ROLL_SUPERVISOR_LIVE_WATCH_TICKS");
249878
249921
  let ticks = 0;
249879
- return new Promise((resolve11) => {
249922
+ return new Promise((resolve12) => {
249880
249923
  let timer;
249881
249924
  let stopped = false;
249882
249925
  const stop = (code) => {
@@ -249887,7 +249930,7 @@ function followSupervisorLiveBoard(projectPath3, intervalMs) {
249887
249930
  clearInterval(timer);
249888
249931
  process.removeListener("SIGINT", onSigint);
249889
249932
  process.stdout.write("\x1B[?25h");
249890
- resolve11(code);
249933
+ resolve12(code);
249891
249934
  };
249892
249935
  const onSigint = () => stop(130);
249893
249936
  const tick = () => {
@@ -251053,7 +251096,7 @@ function spawnPeerReviewAgent(input) {
251053
251096
  const cmd = textAgentArgv(input.agent, input.prompt);
251054
251097
  if (cmd === null)
251055
251098
  return Promise.resolve({ status: "error", reason: "unsupported_reviewer", stdout: "" });
251056
- return new Promise((resolve11) => {
251099
+ return new Promise((resolve12) => {
251057
251100
  let stdout = "";
251058
251101
  let stderr = "";
251059
251102
  let timedOut = false;
@@ -251071,7 +251114,7 @@ function spawnPeerReviewAgent(input) {
251071
251114
  settled = true;
251072
251115
  if (timer !== void 0)
251073
251116
  clearTimeout(timer);
251074
- resolve11(result);
251117
+ resolve12(result);
251075
251118
  };
251076
251119
  timer = setTimeout(() => {
251077
251120
  timedOut = true;
@@ -251681,7 +251724,7 @@ function realDeps5() {
251681
251724
  startTmux: startGoTmux,
251682
251725
  followFeed: followGoLiveFeed,
251683
251726
  runOnce: realRunOnce,
251684
- sleep: (ms) => new Promise((resolve11) => setTimeout(resolve11, ms)),
251727
+ sleep: (ms) => new Promise((resolve12) => setTimeout(resolve12, ms)),
251685
251728
  externalTools: guideExternalToolSetup,
251686
251729
  preinstallChromium: () => {
251687
251730
  silentPreinstallChromium();
@@ -251726,6 +251769,59 @@ function eventsPath(projectPath3) {
251726
251769
  function runsPath2(projectPath3) {
251727
251770
  return join55(runtimeDir5(projectPath3), "runs.jsonl");
251728
251771
  }
251772
+ function parsePorcelainPath(line) {
251773
+ const raw = line.length > 3 ? line.slice(3).trim() : line.trim();
251774
+ const target = raw.includes(" -> ") ? raw.split(" -> ").at(-1) ?? raw : raw;
251775
+ return target.replace(/^"|"$/g, "").replace(/\/$/, "");
251776
+ }
251777
+ function gitDirtyPaths(projectPath3) {
251778
+ const result = spawnSync8("git", ["status", "--porcelain", "--untracked-files=all"], {
251779
+ cwd: projectPath3,
251780
+ encoding: "utf8",
251781
+ stdio: ["ignore", "pipe", "ignore"]
251782
+ });
251783
+ if (result.status !== 0)
251784
+ return [];
251785
+ return String(result.stdout ?? "").split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "").map(parsePorcelainPath).filter((path) => path !== ".roll/loop" && !path.startsWith(".roll/loop/")).slice(0, 50);
251786
+ }
251787
+ function isBootstrapArtifactPath(path) {
251788
+ if (path === "AGENTS.md" || path === "CLAUDE.md" || path === ".cursor-rules" || path === "project_rules.md")
251789
+ return true;
251790
+ if (path === ".roll" || path.startsWith(".roll/"))
251791
+ return true;
251792
+ if (path === ".claude" || path.startsWith(".claude/"))
251793
+ return true;
251794
+ return false;
251795
+ }
251796
+ function classifyBootstrapArtifacts(paths) {
251797
+ const files = paths.filter((path) => path.trim() !== "");
251798
+ if (files.length === 0)
251799
+ return { kind: "none", files: [] };
251800
+ return files.every(isBootstrapArtifactPath) ? { kind: "bootstrap_only", files } : { kind: "mixed", files };
251801
+ }
251802
+ function bootstrapArtifactsMessage(files) {
251803
+ const shown = files.slice(0, 12).join(", ");
251804
+ const more = files.length > 12 ? `, ... +${files.length - 12} more` : "";
251805
+ return [
251806
+ "roll loop go: bootstrap_artifacts_unconfirmed",
251807
+ ` files: ${shown}${more}`,
251808
+ " These files define project conventions/backlog metadata. Confirm their ownership before running builders:",
251809
+ " - commit them to the product repo if they are product truth",
251810
+ " - commit private Roll metadata inside .roll/roll-meta when applicable",
251811
+ " - ignore/externalize them by project policy",
251812
+ " - or clean them up and re-run init/design",
251813
+ " Then rerun: roll loop go",
251814
+ "",
251815
+ "roll loop go: bootstrap_artifacts_unconfirmed",
251816
+ " \u8FD9\u4E9B\u6587\u4EF6\u5B9A\u4E49\u9879\u76EE\u7EA6\u5B9A/backlog \u5143\u6570\u636E\u3002\u5148\u786E\u8BA4\u5F52\u5C5E\uFF0C\u518D\u542F\u52A8 builder\uFF1A",
251817
+ " - \u5C5E\u4E8E\u4EA7\u54C1\u4E8B\u5B9E\u5C31\u63D0\u4EA4\u5230\u4EA7\u54C1\u4ED3",
251818
+ " - \u5C5E\u4E8E\u79C1\u6709 Roll \u5143\u6570\u636E\u5C31\u63D0\u4EA4\u5230 .roll/roll-meta",
251819
+ " - \u6309\u9879\u76EE\u7EA6\u5B9A ignore/\u5916\u7F6E",
251820
+ " - \u6216\u6E05\u7406\u540E\u91CD\u8DD1 init/design",
251821
+ " \u7136\u540E\u518D\u8FD0\u884C\uFF1Aroll loop go",
251822
+ ""
251823
+ ].join("\n");
251824
+ }
251729
251825
  function parseOptions(args) {
251730
251826
  let scope = { kind: "all" };
251731
251827
  let maxCycles;
@@ -251958,7 +252054,7 @@ function startGoTmux(input) {
251958
252054
  }
251959
252055
  }
251960
252056
  function followGoLiveFeed(projectPath3, rollBin4) {
251961
- return new Promise((resolve11) => {
252057
+ return new Promise((resolve12) => {
251962
252058
  const cmd = rollBin4.endsWith(".js") || rollBin4.endsWith(".mjs") ? process.execPath : rollBin4;
251963
252059
  const args = rollBin4.endsWith(".js") || rollBin4.endsWith(".mjs") ? [rollBin4, "loop", "watch", "--since", "all"] : ["loop", "watch", "--since", "all"];
251964
252060
  const watch = spawn7(cmd, args, { cwd: projectPath3, stdio: ["ignore", "inherit", "inherit"] });
@@ -251968,7 +252064,7 @@ function followGoLiveFeed(projectPath3, rollBin4) {
251968
252064
  } catch {
251969
252065
  }
251970
252066
  process.removeListener("SIGINT", finish);
251971
- resolve11();
252067
+ resolve12();
251972
252068
  };
251973
252069
  process.on("SIGINT", finish);
251974
252070
  watch.on("exit", finish);
@@ -251979,7 +252075,7 @@ function realRunOnce(input) {
251979
252075
  const bin = rollBin();
251980
252076
  const cmd = bin.endsWith(".js") || bin.endsWith(".mjs") ? process.execPath : bin;
251981
252077
  const args = cmd === process.execPath ? [bin, "loop", "run-once"] : ["loop", "run-once"];
251982
- return new Promise((resolve11) => {
252078
+ return new Promise((resolve12) => {
251983
252079
  const child = spawn7(cmd, args, {
251984
252080
  cwd: input.projectPath,
251985
252081
  detached: true,
@@ -251993,8 +252089,8 @@ function realRunOnce(input) {
251993
252089
  },
251994
252090
  stdio: "inherit"
251995
252091
  });
251996
- child.on("exit", (code) => resolve11(code ?? 1));
251997
- child.on("error", () => resolve11(1));
252092
+ child.on("exit", (code) => resolve12(code ?? 1));
252093
+ child.on("error", () => resolve12(1));
251998
252094
  });
251999
252095
  }
252000
252096
  function writeGoal(path, goal) {
@@ -252822,6 +252918,14 @@ async function runGoWorker(id, opts, deps) {
252822
252918
  stopReason2 = preProgressGate.reason;
252823
252919
  break;
252824
252920
  }
252921
+ const bootstrap = classifyBootstrapArtifacts(gitDirtyPaths(id.path));
252922
+ if (bootstrap.kind === "bootstrap_only") {
252923
+ stopReason2 = "bootstrap_artifacts_unconfirmed";
252924
+ appendGoalGate(bus, evPath, sid, "progress", "paused", stopReason2, { files: bootstrap.files.join(", "), count: bootstrap.files.length }, deps.nowSec());
252925
+ process.stdout.write(bootstrapArtifactsMessage(bootstrap.files));
252926
+ goal = pauseGoal(id.path, bus, stopReason2, deps.nowIso(), deps.nowSec()) ?? goal;
252927
+ break;
252928
+ }
252825
252929
  const allowedCards = allowedCardsForScope(id.path, goal, progress);
252826
252930
  if (allScopeCardsSkipped(id.path, goal, progress)) {
252827
252931
  stopReason2 = "no_progress_on_all_cards";
@@ -253975,7 +254079,7 @@ function renderDesignNudge(lang10) {
253975
254079
  init_dist2();
253976
254080
  init_dist();
253977
254081
  import { existsSync as existsSync59, readdirSync as readdirSync24, readFileSync as readFileSync56, statSync as statSync22 } from "node:fs";
253978
- import { relative as relative4, resolve as resolve5 } from "node:path";
254082
+ import { relative as relative4, resolve as resolve6 } from "node:path";
253979
254083
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
253980
254084
  ".git",
253981
254085
  "node_modules",
@@ -253997,23 +254101,23 @@ function walkMarkdownFiles(dir, out3, includeGenerated) {
253997
254101
  continue;
253998
254102
  if (!includeGenerated && (entry.name === ".roll" || entry.name === "archive"))
253999
254103
  continue;
254000
- walkMarkdownFiles(resolve5(dir, entry.name), out3, includeGenerated);
254104
+ walkMarkdownFiles(resolve6(dir, entry.name), out3, includeGenerated);
254001
254105
  } else if (entry.isFile() && isMarkdownFile(entry.name)) {
254002
- out3.push(resolve5(dir, entry.name));
254106
+ out3.push(resolve6(dir, entry.name));
254003
254107
  }
254004
254108
  }
254005
254109
  }
254006
254110
  function defaultScanRoots(root, includeGenerated) {
254007
254111
  const roots = [];
254008
254112
  for (const rel of ["AGENTS.md", "roll.md", "conventions", "skills", "guide"]) {
254009
- const abs = resolve5(root, rel);
254113
+ const abs = resolve6(root, rel);
254010
254114
  if (!existsSync59(abs))
254011
254115
  continue;
254012
254116
  roots.push(abs);
254013
254117
  }
254014
254118
  if (includeGenerated) {
254015
254119
  for (const rel of [".roll", "archive"]) {
254016
- const abs = resolve5(root, rel);
254120
+ const abs = resolve6(root, rel);
254017
254121
  if (existsSync59(abs))
254018
254122
  roots.push(abs);
254019
254123
  }
@@ -256854,7 +256958,7 @@ function renderAiClients(out3, clients) {
256854
256958
  }
256855
256959
  out3.push(" " + pad(nameCol, 14) + pad(c("dim", cl.cfg_file), 14) + pad(syncCol, 14) + c("dim", String(cl.skills)));
256856
256960
  if (cl.sync === "out-of-sync" || cl.sync === "missing") {
256857
- out3.push(" " + c("dim", "fix: ") + c("blue", `roll setup -f ${cl.name}`));
256961
+ out3.push(" " + c("dim", "fix: ") + c("blue", "roll setup -f"));
256858
256962
  }
256859
256963
  }
256860
256964
  out3.push("", hr(), "");
@@ -257180,7 +257284,7 @@ function collectAgentPanel(projectPath3, deps = defaultAgentPanelDeps()) {
257180
257284
  costUsd72h: Number(s.cost.toFixed(4)),
257181
257285
  files,
257182
257286
  syncStale: isInstalled && stale,
257183
- ...isInstalled && stale ? { setupCmd: `roll setup -f ${name}` } : {}
257287
+ ...isInstalled && stale ? { setupCmd: "roll setup -f" } : {}
257184
257288
  });
257185
257289
  }
257186
257290
  rows.sort((a, b) => Number(b.installed) - Number(a.installed) || b.cycles72h - a.cycles72h || a.name.localeCompare(b.name));
@@ -258049,7 +258153,7 @@ function collectLoopHeartbeat(deps) {
258049
258153
  // packages/cli/dist/lib/git-hooks.js
258050
258154
  import { execFileSync as execFileSync20 } from "node:child_process";
258051
258155
  import { readdirSync as readdirSync29 } from "node:fs";
258052
- import { isAbsolute, join as join77, resolve as resolve7 } from "node:path";
258156
+ import { isAbsolute, join as join77, resolve as resolve8 } from "node:path";
258053
258157
  var HOOK_DESCRIPTIONS = {
258054
258158
  "pre-commit": {
258055
258159
  en: "TCR proof gate before commit",
@@ -258111,11 +258215,11 @@ function resolveHooksPath(projectPath3) {
258111
258215
  if (configured !== "" && configured !== ".git/hooks") {
258112
258216
  return {
258113
258217
  displayPath: configured,
258114
- fsPath: isAbsolute(configured) ? configured : resolve7(projectPath3, configured)
258218
+ fsPath: isAbsolute(configured) ? configured : resolve8(projectPath3, configured)
258115
258219
  };
258116
258220
  }
258117
258221
  const gitPath = gitOutput3(projectPath3, ["rev-parse", "--git-path", "hooks"]);
258118
- const fsPath = gitPath === "" ? join77(projectPath3, ".git", "hooks") : isAbsolute(gitPath) ? gitPath : resolve7(projectPath3, gitPath);
258222
+ const fsPath = gitPath === "" ? join77(projectPath3, ".git", "hooks") : isAbsolute(gitPath) ? gitPath : resolve8(projectPath3, gitPath);
258119
258223
  return {
258120
258224
  displayPath: configured === "" ? ".git/hooks" : configured,
258121
258225
  fsPath
@@ -260053,6 +260157,91 @@ function registerProject(projectDir) {
260053
260157
  } catch {
260054
260158
  }
260055
260159
  }
260160
+ var ROLL_OWNED_GIT_PATHS = [
260161
+ "AGENTS.md",
260162
+ ".claude/CLAUDE.md",
260163
+ ".roll/.version",
260164
+ ".roll/agents.yaml",
260165
+ ".roll/backlog.md",
260166
+ ".roll/brief.md",
260167
+ ".roll/briefs",
260168
+ ".roll/domain",
260169
+ ".roll/domain/context-map.md",
260170
+ ".roll/features",
260171
+ ".roll/features.md",
260172
+ ".roll/init-diagnosis.yaml",
260173
+ ".roll/onboard-changeset.yaml",
260174
+ ".roll/onboard-plan.yaml",
260175
+ ".roll/tech-analysis.md",
260176
+ ".roll/test-assessment.md",
260177
+ ".gitignore"
260178
+ ];
260179
+ function runGit(projectDir, args) {
260180
+ const result = spawnSync11("git", args, {
260181
+ cwd: projectDir,
260182
+ encoding: "utf8",
260183
+ stdio: ["ignore", "pipe", "pipe"]
260184
+ });
260185
+ return {
260186
+ ok: result.status === 0,
260187
+ stdout: String(result.stdout ?? ""),
260188
+ stderr: String(result.stderr ?? "")
260189
+ };
260190
+ }
260191
+ function existingRollOwnedPaths(projectDir) {
260192
+ return ROLL_OWNED_GIT_PATHS.filter((rel) => existsSync83(join86(projectDir, rel.replace(/\/$/, ""))));
260193
+ }
260194
+ function printGitFinalizeManual(reason, command) {
260195
+ warn4(`Roll meta git finalize needs manual follow-up: ${reason}`);
260196
+ process.stderr.write(` ${command}
260197
+ `);
260198
+ }
260199
+ function finalizeRollOwnedGit(projectDir) {
260200
+ if (!runGit(projectDir, ["rev-parse", "--is-inside-work-tree"]).ok)
260201
+ return;
260202
+ const topLevel = runGit(projectDir, ["rev-parse", "--show-toplevel"]);
260203
+ if (!topLevel.ok)
260204
+ return;
260205
+ if (realpathSync11(projectDir) !== realpathSync11(topLevel.stdout.trim()))
260206
+ return;
260207
+ const paths = existingRollOwnedPaths(projectDir);
260208
+ if (paths.length === 0)
260209
+ return;
260210
+ if (!runGit(projectDir, ["diff", "--cached", "--quiet"]).ok) {
260211
+ printGitFinalizeManual("existing staged changes; auto commit skipped", `git add -A -f -- ${paths.join(" ")} && git commit -m 'roll init: commit Roll-owned meta files'`);
260212
+ return;
260213
+ }
260214
+ const add = runGit(projectDir, ["add", "-A", "-f", "--", ...paths]);
260215
+ if (!add.ok) {
260216
+ printGitFinalizeManual("git add failed", `git add -A -f -- ${paths.join(" ")}`);
260217
+ return;
260218
+ }
260219
+ if (runGit(projectDir, ["diff", "--cached", "--quiet", "--", ...paths]).ok)
260220
+ return;
260221
+ const commit2 = runGit(projectDir, ["commit", "-m", "roll init: commit Roll-owned meta files"]);
260222
+ if (!commit2.ok) {
260223
+ printGitFinalizeManual("git commit failed", "git commit -m 'roll init: commit Roll-owned meta files' -- AGENTS.md .roll .claude .gitignore");
260224
+ return;
260225
+ }
260226
+ const sha = runGit(projectDir, ["rev-parse", "--short", "HEAD"]).stdout.trim();
260227
+ ok10(`Roll meta committed${sha !== "" ? `: ${sha}` : ""}`);
260228
+ const branch = runGit(projectDir, ["rev-parse", "--abbrev-ref", "HEAD"]).stdout.trim();
260229
+ if (branch === "" || branch === "HEAD") {
260230
+ printGitFinalizeManual("detached HEAD; push skipped", "git push origin HEAD");
260231
+ return;
260232
+ }
260233
+ const remotes = runGit(projectDir, ["remote"]).stdout.split("\n").filter((line) => line.trim() !== "");
260234
+ if (!remotes.includes("origin")) {
260235
+ printGitFinalizeManual("origin remote is not configured; push skipped", `git push -u origin ${branch}`);
260236
+ return;
260237
+ }
260238
+ const push2 = runGit(projectDir, ["push", "-u", "origin", branch]);
260239
+ if (!push2.ok) {
260240
+ printGitFinalizeManual("git push failed", `git push -u origin ${branch}`);
260241
+ return;
260242
+ }
260243
+ ok10(`Roll meta pushed: origin/${branch}`);
260244
+ }
260056
260245
  function err9(line) {
260057
260246
  const { RED, NC } = pal4();
260058
260247
  process.stderr.write(`${RED}[roll]${NC} ${line}
@@ -260914,6 +261103,7 @@ function initRepair(projectDir, facts, diagnosis2, opts = {}) {
260914
261103
  recordCreatedFileIfNeeded(projectDir, changeset, ".roll/.version", stampExisted);
260915
261104
  printMergeSummary(summary);
260916
261105
  ok10("Repair complete.");
261106
+ finalizeRollOwnedGit(projectDir);
260917
261107
  return 0;
260918
261108
  }
260919
261109
  function initApply(projectDir, opts = {}) {
@@ -261029,6 +261219,7 @@ function initApply(projectDir, opts = {}) {
261029
261219
  registerProject(projectDir);
261030
261220
  process.stdout.write("\n");
261031
261221
  ok10(m3("init.onboard_apply_complete_onboard"));
261222
+ finalizeRollOwnedGit(projectDir);
261032
261223
  return 0;
261033
261224
  } catch (error) {
261034
261225
  err9(m32("init.onboard_apply_failed"));
@@ -261859,6 +262050,7 @@ function initCommand(args, deps = {}) {
261859
262050
  registerProject(projectDir);
261860
262051
  const shouldNudge = detectDesignHandoff(projectDir).shouldNudge;
261861
262052
  emitInitUi(projectDir, hasAgents, syncStatus, summary, shouldNudge, repairMode ? "repair" : void 0, conciergeNextItems(brief, initDiagnosis));
262053
+ finalizeRollOwnedGit(projectDir);
261862
262054
  void err9;
261863
262055
  return 0;
261864
262056
  }
@@ -262057,7 +262249,7 @@ init_dist();
262057
262249
  init_dist2();
262058
262250
  import { spawnSync as spawnSync12 } from "node:child_process";
262059
262251
  import { existsSync as existsSync85, mkdirSync as mkdirSync37, readFileSync as readFileSync78, renameSync as renameSync16, statSync as statSync31, writeFileSync as writeFileSync39 } from "node:fs";
262060
- import { dirname as dirname39, join as join88, relative as relative6, resolve as resolve8 } from "node:path";
262252
+ import { dirname as dirname39, join as join88, relative as relative6, resolve as resolve9 } from "node:path";
262061
262253
 
262062
262254
  // packages/cli/dist/lib/review-page.js
262063
262255
  init_dist2();
@@ -262466,7 +262658,7 @@ function renderDesignReviewPageForTarget(ctx, cardsCreated) {
262466
262658
  return;
262467
262659
  const base = join88(".roll", "features", epic, ctx.target);
262468
262660
  const md = join88(base, "spec.md");
262469
- const absMd = resolve8(ctx.cwd, md);
262661
+ const absMd = resolve9(ctx.cwd, md);
262470
262662
  if (!existsSync85(absMd) || !hasDetailedDesign(absMd))
262471
262663
  return;
262472
262664
  const markdown = readFileSync78(absMd, "utf8");
@@ -262481,7 +262673,7 @@ function renderDesignReviewPageForTarget(ctx, cardsCreated) {
262481
262673
  markdown,
262482
262674
  lang: ctx.lang
262483
262675
  });
262484
- const out3 = resolve8(ctx.cwd, base, "design-review.html");
262676
+ const out3 = resolve9(ctx.cwd, base, "design-review.html");
262485
262677
  mkdirSync37(dirname39(out3), { recursive: true });
262486
262678
  const tmp = `${out3}.tmp`;
262487
262679
  writeFileSync39(tmp, html, "utf8");
@@ -262594,12 +262786,12 @@ function printHandoff(ctx, statusCode, rawTranscript) {
262594
262786
  const md = join88(base, "spec.md");
262595
262787
  const reviewHtml = join88(base, "design-review.html");
262596
262788
  const html = join88(base, "spec.html");
262597
- if (existsSync85(resolve8(ctx.cwd, md))) {
262598
- designPath = hasDetailedDesign(resolve8(ctx.cwd, md)) ? `${md}#detailed-design` : md;
262789
+ if (existsSync85(resolve9(ctx.cwd, md))) {
262790
+ designPath = hasDetailedDesign(resolve9(ctx.cwd, md)) ? `${md}#detailed-design` : md;
262599
262791
  }
262600
- if (existsSync85(resolve8(ctx.cwd, reviewHtml)))
262792
+ if (existsSync85(resolve9(ctx.cwd, reviewHtml)))
262601
262793
  reviewPagePath = reviewHtml;
262602
- else if (existsSync85(resolve8(ctx.cwd, html)))
262794
+ else if (existsSync85(resolve9(ctx.cwd, html)))
262603
262795
  reviewPagePath = html;
262604
262796
  }
262605
262797
  let status2;
@@ -262664,7 +262856,7 @@ function designCommand(args, deps = {}) {
262664
262856
  emit4(t(v3Catalog, l, "design.not_roll_project"));
262665
262857
  return 1;
262666
262858
  }
262667
- if (fromFile !== void 0 && !isRegularFile(resolve8(d.cwd, fromFile))) {
262859
+ if (fromFile !== void 0 && !isRegularFile(resolve9(d.cwd, fromFile))) {
262668
262860
  emit4(t(v3Catalog, l, "design.from_file_not_found", fromFile));
262669
262861
  return 1;
262670
262862
  }
@@ -263734,8 +263926,8 @@ async function probeAgentReachable(agent, spawn10, opts = {}) {
263734
263926
  try {
263735
263927
  res = await Promise.race([
263736
263928
  spawn10(agent, { cwd, skillBody: `Reply with exactly: ${token}`, timeoutMs, bare: true }),
263737
- new Promise((resolve11) => {
263738
- setTimeout(() => resolve11(null), timeoutMs).unref();
263929
+ new Promise((resolve12) => {
263930
+ setTimeout(() => resolve12(null), timeoutMs).unref();
263739
263931
  })
263740
263932
  ]);
263741
263933
  } catch (e) {
@@ -264621,7 +264813,7 @@ function reviewTimeoutMs(diffChars) {
264621
264813
  async function firstValid(promises) {
264622
264814
  if (promises.length === 0)
264623
264815
  return null;
264624
- return new Promise((resolve11, reject) => {
264816
+ return new Promise((resolve12, reject) => {
264625
264817
  let pending = promises.length;
264626
264818
  let settled = false;
264627
264819
  let lastError;
@@ -264633,7 +264825,7 @@ async function firstValid(promises) {
264633
264825
  if (threw)
264634
264826
  reject(lastError);
264635
264827
  else
264636
- resolve11(null);
264828
+ resolve12(null);
264637
264829
  }
264638
264830
  };
264639
264831
  for (const p of promises) {
@@ -264642,7 +264834,7 @@ async function firstValid(promises) {
264642
264834
  return;
264643
264835
  if (value !== null) {
264644
264836
  settled = true;
264645
- resolve11(value);
264837
+ resolve12(value);
264646
264838
  return;
264647
264839
  }
264648
264840
  onNonResult();
@@ -265025,7 +265217,7 @@ DIFF:
265025
265217
 
265026
265218
  // packages/cli/dist/runner/executor.js
265027
265219
  var execFileAsync10 = promisify10(execFile10);
265028
- function parsePorcelainPath(line) {
265220
+ function parsePorcelainPath2(line) {
265029
265221
  const raw = line.length > 3 ? line.slice(3).trim() : line.trim();
265030
265222
  const target = raw.includes(" -> ") ? raw.split(" -> ").at(-1) ?? raw : raw;
265031
265223
  return target.replace(/^"|"$/g, "");
@@ -265036,7 +265228,7 @@ async function checkMainDirty(repoCwd) {
265036
265228
  cwd: repoCwd,
265037
265229
  encoding: "utf8"
265038
265230
  });
265039
- return stdout.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "").map(parsePorcelainPath).filter((path) => path !== ".roll" && !path.startsWith(".roll/")).slice(0, 50);
265231
+ return stdout.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "").map(parsePorcelainPath2).filter((path) => path !== ".roll" && !path.startsWith(".roll/")).slice(0, 50);
265040
265232
  } catch {
265041
265233
  return [];
265042
265234
  }
@@ -265995,7 +266187,7 @@ ${stderr}
265995
266187
  // FIX-319: review-only framing, no worker autorun directive
265996
266188
  ...ctx.evidenceRunDir !== void 0 ? { runDir: ctx.evidenceRunDir } : {}
265997
266189
  }),
265998
- new Promise((resolve11) => setTimeout(() => resolve11(null), timeoutMs).unref())
266190
+ new Promise((resolve12) => setTimeout(() => resolve12(null), timeoutMs).unref())
265999
266191
  ]);
266000
266192
  } catch (e) {
266001
266193
  const detail = e instanceof Error ? e.message : String(e);
@@ -266135,7 +266327,7 @@ ${res.stderr}`;
266135
266327
  timeoutMs,
266136
266328
  ...ctx.evidenceRunDir !== void 0 ? { runDir: ctx.evidenceRunDir } : {}
266137
266329
  }),
266138
- new Promise((resolve11) => setTimeout(() => resolve11(null), timeoutMs).unref())
266330
+ new Promise((resolve12) => setTimeout(() => resolve12(null), timeoutMs).unref())
266139
266331
  ]);
266140
266332
  } catch (e) {
266141
266333
  const detail = e instanceof Error ? e.message : String(e);
@@ -269132,10 +269324,10 @@ function autoGc(projectPath3) {
269132
269324
  }
269133
269325
  }
269134
269326
  }
269135
- async function isOffline(resolve11 = (h) => lookup2(h)) {
269327
+ async function isOffline(resolve12 = (h) => lookup2(h)) {
269136
269328
  try {
269137
269329
  await Promise.race([
269138
- resolve11("github.com"),
269330
+ resolve12("github.com"),
269139
269331
  new Promise((_, rej) => {
269140
269332
  const t2 = setTimeout(() => rej(new Error("dns timeout")), 1500);
269141
269333
  if (typeof t2 === "object")
@@ -269204,7 +269396,7 @@ init_dist();
269204
269396
  import { spawnSync as spawnSync15 } from "node:child_process";
269205
269397
  import { existsSync as existsSync100, readFileSync as readFileSync94, realpathSync as realpathSync14, renameSync as renameSync18, rmSync as rmSync17, writeFileSync as writeFileSync50 } from "node:fs";
269206
269398
  import { homedir as homedir28 } from "node:os";
269207
- import { join as join104, resolve as resolve9 } from "node:path";
269399
+ import { join as join104, resolve as resolve10 } from "node:path";
269208
269400
  function pal5() {
269209
269401
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
269210
269402
  return noColor2 ? { RED: "", GREEN: "", YELLOW: "", CYAN: "", BOLD: "", NC: "" } : {
@@ -269308,7 +269500,7 @@ function writeFileAtomic2(path, text2) {
269308
269500
  }
269309
269501
  }
269310
269502
  function resolveChangesetItem(projectDir, item) {
269311
- return item.startsWith("/") ? resolve9(item) : resolve9(projectDir, item);
269503
+ return item.startsWith("/") ? resolve10(item) : resolve10(projectDir, item);
269312
269504
  }
269313
269505
  var HELP3 = `Usage: roll offboard [--confirm]
269314
269506
  Preview (default) or apply (--confirm) the removal of every
@@ -269329,7 +269521,7 @@ function offboardCommand(args) {
269329
269521
  }
269330
269522
  let projectDir;
269331
269523
  try {
269332
- projectDir = realpathSync14(resolve9(process.cwd()));
269524
+ projectDir = realpathSync14(resolve10(process.cwd()));
269333
269525
  } catch {
269334
269526
  projectDir = process.cwd();
269335
269527
  }
@@ -272118,7 +272310,7 @@ init_showcase2();
272118
272310
  // packages/cli/dist/commands/test.js
272119
272311
  import { spawnSync as spawnSync18 } from "node:child_process";
272120
272312
  import { appendFileSync as appendFileSync19, existsSync as existsSync108, mkdirSync as mkdirSync53, readFileSync as readFileSync102, rmSync as rmSync19, writeFileSync as writeFileSync56 } from "node:fs";
272121
- import { dirname as dirname50, join as join112, resolve as resolve10 } from "node:path";
272313
+ import { dirname as dirname50, join as join112, resolve as resolve11 } from "node:path";
272122
272314
  function pal7() {
272123
272315
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
272124
272316
  return noColor2 ? { CYAN: "", GREEN: "", YELLOW: "", RED: "", NC: "" } : { CYAN: "\x1B[0;36m", GREEN: "\x1B[0;32m", YELLOW: "\x1B[0;33m", RED: "\x1B[0;31m", NC: "\x1B[0m" };
@@ -272138,7 +272330,7 @@ function currentEvidenceFrame() {
272138
272330
  const raw = (process.env["ROLL_RUN_DIR"] ?? "").trim();
272139
272331
  if (raw === "")
272140
272332
  return null;
272141
- const runDir = resolve10(raw);
272333
+ const runDir = resolve11(raw);
272142
272334
  if (!existsSync108(runDir))
272143
272335
  return null;
272144
272336
  const frame = {
@@ -273114,7 +273306,7 @@ function registerAll() {
273114
273306
  if (args[0] === "offboard")
273115
273307
  return offboardCommand(args.slice(1));
273116
273308
  return setupCommand(args);
273117
- }, { help: "Usage: roll setup [skills|offboard]\n Install roll conventions/templates for this machine, or remove recorded Roll artifacts.\n\u672C\u673A\u5B89\u88C5\u6A21\u677F\uFF0C\u6216\u79FB\u9664\u8BB0\u5F55\u8FC7\u7684 Roll \u4EA7\u7269\u3002" });
273309
+ }, { help: "Usage: roll setup [-f|--force] [--reselect]\n roll setup skills [args...]\n roll setup offboard [args...]\n Install or re-sync Roll conventions/templates for this machine; use -f to force refresh.\n\u672C\u673A\u5B89\u88C5\u6216\u91CD\u65B0\u540C\u6B65 Roll \u6A21\u677F\u4E0E\u7EA6\u5B9A\uFF1B-f \u5F3A\u5236\u5237\u65B0\u3002" });
273118
273310
  registerPorted("ci", (args) => {
273119
273311
  if (args.includes("--wait"))
273120
273312
  return ciWaitCommand(args);
package/guide/en/loop.md CHANGED
@@ -213,6 +213,14 @@ bricked by a limit you set days ago. Scope (`--epic`/`--cards`) and `--review`
213
213
  still persist when unspecified, because they are the goal's identity, not a
214
214
  per-run safety knob.
215
215
 
216
+ Before the first builder cycle, `roll loop go` also has a fallback check for
217
+ leftover uncommitted Roll bootstrap artifacts such as `AGENTS.md`, `.claude/`,
218
+ or `.roll/` metadata. Current `roll init` should already commit and push those
219
+ Roll-owned files; if a historical or manual path still leaves only those files
220
+ dirty, the goal pauses with `bootstrap_artifacts_unconfirmed` and prints the
221
+ files to confirm. Commit or clean them, then run `roll loop go` again. This
222
+ preflight does not start a cycle and does not count as no-progress.
223
+
216
224
  `roll loop go` enforces safety only at cycle boundaries. `--budget <usd>` uses
217
225
  the effective run cost ledger and moves the goal to `budget_limited` when the
218
226
  budget is reached. An idle or aborted cycle that ran no agent counts as a known
@@ -45,6 +45,13 @@ and apply an onboard plan, repair partial markers, run the old-layout migration,
45
45
  start the loop on the next Todo card, or inspect status when nothing is
46
46
  actionable.
47
47
 
48
+ When a path writes Roll-owned meta files inside a git worktree, `roll init`
49
+ finishes by adding, committing, and pushing those files to `origin` when
50
+ possible. The finalize step is scoped to Roll-owned paths such as `AGENTS.md`,
51
+ `.claude/CLAUDE.md`, `.roll/**`, and Roll-managed `.gitignore` entries; product
52
+ source, PRDs, and other user files are left alone. If commit or push cannot be
53
+ completed, init prints the manual command to run.
54
+
48
55
  Upgrading from a pre-2.0 layout (`BACKLOG.md` at root or `docs/features/`)?
49
56
  Run `npx @seanyao/roll@2 migrate` first — see
50
57
  [migration-2.0.md](migration-2.0.md). `roll init` will refuse to scaffold on top
package/guide/zh/loop.md CHANGED
@@ -198,6 +198,12 @@ unknown,这类行仍按保守侧停止,不当作 0。用量闸检查 5 小
198
198
  不会让会话无限停摆;超时后 Roll 记录 `usage_wait_timeout` 审计事件并让 goal 保持暂停。
199
199
  `--for <duration>` 是墙钟时间盒:当前 cycle 收尾后,goal 以 `timebox` 原因暂停。
200
200
 
201
+ 启动第一个 builder cycle 前,`roll loop go` 还会兜底检查遗留未提交的 Roll bootstrap 产物,
202
+ 例如 `AGENTS.md`、`.claude/` 或 `.roll/` 元数据。新版 `roll init` 应该已经把这些
203
+ Roll-owned 文件提交并推送;如果历史版本或手工路径仍然只留下这些脏文件,goal 会以
204
+ `bootstrap_artifacts_unconfirmed` 暂停,并打印待确认文件。提交或清理后,再重新运行
205
+ `roll loop go`。这个 preflight 不会启动 cycle,也不会累加 no-progress。
206
+
201
207
  每次安全闸触发都会记录 `goal:gate_tripped`,`roll loop goal` 会显示最近一次安全闸读数。
202
208
 
203
209
  ### `roll loop goal` 字段含义
@@ -38,6 +38,12 @@ roll init
38
38
  从 brief 进入设计、审阅并 apply onboard plan、修复 partial 标记、执行旧布局迁移、
39
39
  对下一张 Todo 开 loop,或在没有可执行项时查看 status。
40
40
 
41
+ 当某条路径在 git worktree 中写入 Roll-owned meta 文件时,`roll init` 会在收尾阶段尽力
42
+ 把这些文件 add、commit 并 push 到 `origin`。这个 finalize 只覆盖 Roll 管理的路径,例如
43
+ `AGENTS.md`、`.claude/CLAUDE.md`、`.roll/**` 和 Roll 自己追加的 `.gitignore` 条目;
44
+ 产品源码、PRD 和其他用户文件不会被顺手提交。若 commit 或 push 无法完成,init 会打印需要
45
+ 手动执行的命令。
46
+
41
47
  正在从 2.0 之前的布局升级(`BACKLOG.md` 在根目录或 `docs/features/`)?
42
48
  先跑 `npx @seanyao/roll@2 migrate` —— 见
43
49
  [migration-2.0.md](migration-2.0.md)。`roll init` 会拒绝在迁移到一半的
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "4.702.2",
3
+ "version": "4.702.3",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "packageManager": "pnpm@11.1.3",
6
6
  "scripts": {