@seanyao/roll 3.606.3 → 3.607.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.
package/dist/roll.mjs CHANGED
@@ -3028,6 +3028,21 @@ function agentBinNames(agent) {
3028
3028
  return null;
3029
3029
  }
3030
3030
  }
3031
+ function realAgentEnv() {
3032
+ return {
3033
+ home: homedir(),
3034
+ commandOnPath,
3035
+ dirExists: (p) => existsSync2(p),
3036
+ fileExecutable: (p) => {
3037
+ try {
3038
+ accessSync(p, constants.X_OK);
3039
+ return statSync(p).isFile();
3040
+ } catch {
3041
+ return false;
3042
+ }
3043
+ }
3044
+ };
3045
+ }
3031
3046
  function commandOnPath(bin) {
3032
3047
  for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
3033
3048
  if (dir === "")
@@ -3130,6 +3145,115 @@ var nodeFileStore = {
3130
3145
  }
3131
3146
  };
3132
3147
 
3148
+ // packages/core/dist/agent/registry.js
3149
+ function agentBinNames2(agent) {
3150
+ switch (agent) {
3151
+ case "claude":
3152
+ return ["claude"];
3153
+ case "codex":
3154
+ case "openai":
3155
+ return ["codex"];
3156
+ case "agy":
3157
+ case "gemini":
3158
+ return ["agy", "gemini"];
3159
+ case "kimi":
3160
+ return ["kimi-code", "kimi-cli", "kimi"];
3161
+ case "deepseek":
3162
+ return ["deepseek"];
3163
+ case "qwen":
3164
+ return ["qwen"];
3165
+ case "pi":
3166
+ return ["pi"];
3167
+ default:
3168
+ return null;
3169
+ }
3170
+ }
3171
+ function joinPath(...parts) {
3172
+ return parts.join("/");
3173
+ }
3174
+ function agentInstalledByName2(env, agent, dir) {
3175
+ const home = env.home;
3176
+ switch (agent) {
3177
+ case "trae":
3178
+ return env.dirExists(joinPath(home, "Library", "Application Support", "Trae")) || env.dirExists(joinPath(home, ".config", "Trae"));
3179
+ case "opencode":
3180
+ return env.fileExecutable(joinPath(home, ".opencode", "bin", "opencode"));
3181
+ case "cursor":
3182
+ return env.commandOnPath("cursor") || env.dirExists(joinPath(home, ".cursor"));
3183
+ case "openclaw":
3184
+ return env.dirExists(joinPath(home, ".openclaw", "workspace"));
3185
+ }
3186
+ const bins = agentBinNames2(agent);
3187
+ if (bins !== null)
3188
+ return bins.some((b) => env.commandOnPath(b));
3189
+ return dir !== void 0 && dir !== "" && env.dirExists(dir);
3190
+ }
3191
+ var FIRST_INSTALLED_ORDER = [
3192
+ "claude",
3193
+ "codex",
3194
+ "kimi",
3195
+ "deepseek",
3196
+ "qwen",
3197
+ "agy",
3198
+ "pi",
3199
+ "cursor",
3200
+ "opencode",
3201
+ "trae",
3202
+ "openclaw"
3203
+ ];
3204
+ function firstInstalledAgent(env) {
3205
+ return FIRST_INSTALLED_ORDER.find((a) => agentInstalledByName2(env, a));
3206
+ }
3207
+ function lineAgentValue(line) {
3208
+ const s = line.replace(/\t/g, " ");
3209
+ if (s.startsWith("agent:"))
3210
+ return s.slice("agent:".length);
3211
+ const m7 = /[ ,{]agent:/.exec(s);
3212
+ if (m7 === null)
3213
+ return void 0;
3214
+ return s.slice(m7.index + m7[0].length);
3215
+ }
3216
+ function cleanAgentValue(raw) {
3217
+ let v = raw;
3218
+ const hash = v.indexOf("#");
3219
+ if (hash >= 0)
3220
+ v = v.slice(0, hash);
3221
+ v = v.replace(/[{}",']/g, "").replace(/,/g, "");
3222
+ return v.trim();
3223
+ }
3224
+ function readSlotFromText(text, slot) {
3225
+ let inBlock = false;
3226
+ let agent = "";
3227
+ let found = false;
3228
+ const slotHeader = `${slot}:`;
3229
+ for (const raw of text.split("\n")) {
3230
+ const line = raw.replace(/\r$/, "");
3231
+ if (line === slotHeader || line.startsWith(`${slot}: `) || line.startsWith(`${slot}:{`)) {
3232
+ inBlock = true;
3233
+ const v = lineAgentValue(line);
3234
+ if (v !== void 0) {
3235
+ agent = v;
3236
+ found = true;
3237
+ }
3238
+ } else if (line.length > 0 && line[0] !== " ") {
3239
+ if (inBlock)
3240
+ break;
3241
+ } else if (inBlock && !found) {
3242
+ const v = lineAgentValue(line);
3243
+ if (v !== void 0) {
3244
+ agent = v;
3245
+ found = true;
3246
+ }
3247
+ }
3248
+ if (found)
3249
+ break;
3250
+ }
3251
+ if (!inBlock)
3252
+ return void 0;
3253
+ const cleaned = cleanAgentValue(agent);
3254
+ return cleaned === "" ? void 0 : cleaned;
3255
+ }
3256
+
3133
3257
  // packages/core/dist/agent/router.js
3134
3258
  var EASY_MAX_MIN = 8;
3135
3259
  var HARD_MIN_MIN = 20;
@@ -5439,6 +5563,91 @@ var ANSI_CSS = `
5439
5563
  }
5440
5564
  `.trim();
5441
5565
 
5566
+ // packages/core/dist/html/chrome.js
5567
+ function bi(en, zh) {
5568
+ return `<span class="lang-en">${en}</span><span class="lang-zh">${zh}</span>`;
5569
+ }
5570
+ var DARK_VARS = `--bg:#161410; --bg-raise:#1d1a14; --fg:#e9e2d0; --muted:#99907a; --line:#38332c; --accent:#e05b3e; --grain:rgba(255,255,255,.018); --pass:#57ab5a; --info:#539bf5; --warn:#c69026; --claim:#e0823d; --fail:#e5534b; --block:#986ee2;`;
5571
+ var CHROME_CSS = `
5572
+ :root { color-scheme: light dark;
5573
+ --bg:#f6f2e9; --bg-raise:#fdfbf5; --fg:#211d14; --muted:#79705d; --line:#d9d1bd; --accent:#a83825;
5574
+ --grain:rgba(60,40,10,.03);
5575
+ --pass:#2f7d3b; --info:#2c6cb0; --warn:#9a7b00; --claim:#c4602c; --fail:#c03328; --block:#6e40c9;
5576
+ --serif:"Iowan Old Style","Palatino","Book Antiqua",Georgia,"Songti SC","Noto Serif CJK SC",serif;
5577
+ --sans:-apple-system,"PingFang SC","Segoe UI","Microsoft YaHei",sans-serif;
5578
+ --mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace; }
5579
+ :root[data-theme="dark"] { ${DARK_VARS} }
5580
+ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { ${DARK_VARS} } }
5581
+ html { background:var(--bg); }
5582
+ body { margin:0 auto; max-width:880px; padding:30px 20px 80px; background:var(--bg); color:var(--fg);
5583
+ font:15px/1.7 var(--sans); }
5584
+ body::after { content:""; position:fixed; inset:0; pointer-events:none; z-index:1;
5585
+ background-image:radial-gradient(var(--grain) 1px, transparent 1px); background-size:3px 3px; }
5586
+ .kicker { font:11.5px/1 var(--serif); letter-spacing:.24em; text-transform:uppercase; color:var(--accent);
5587
+ border-top:3px double var(--line); padding-top:10px; margin:0 0 10px; }
5588
+ h1 { font:600 26px/1.3 var(--serif); margin:0 0 4px; letter-spacing:.01em; }
5589
+ h2 { font:600 17px/1.4 var(--serif); border-bottom:1px solid var(--line); padding-bottom:6px; letter-spacing:.02em; }
5590
+ code { font-family:var(--mono); background:rgba(127,110,70,.10); padding:1px 6px; border-radius:4px; font-size:.9em; }
5591
+ pre { background:rgba(127,110,70,.07); padding:12px; border-radius:6px; overflow-x:auto; }
5592
+ a { color:var(--accent); text-decoration-color:color-mix(in srgb, var(--accent) 40%, transparent); }
5593
+ section { border:1px solid var(--line); border-radius:8px; padding:14px 18px; margin:14px 0;
5594
+ background:var(--bg-raise); position:relative; z-index:2; }
5595
+ .empty { color:var(--muted); font-style:italic; }
5596
+ .meta, .muted { color:var(--muted); font-size:13px; }
5597
+ footer { color:var(--muted); font-size:12.5px; font-family:var(--serif); letter-spacing:.04em;
5598
+ margin-top:40px; border-top:3px double var(--line); padding-top:12px; }
5599
+ /* language switching \u2014 both sides render until the chrome script picks one */
5600
+ [data-lang="en"] .lang-zh { display:none; } [data-lang="zh"] .lang-en { display:none; }
5601
+ /* chrome bar */
5602
+ .chrome { position:fixed; top:14px; right:14px; z-index:10; display:flex; gap:8px; font-family:var(--sans); }
5603
+ .chrome .seg { display:flex; border:1px solid var(--line); border-radius:999px; overflow:hidden;
5604
+ background:color-mix(in srgb, var(--bg-raise) 82%, transparent); backdrop-filter:blur(6px); }
5605
+ .chrome button { all:unset; cursor:pointer; font-size:12px; line-height:1; padding:7px 11px; color:var(--muted); }
5606
+ .chrome button.on { background:var(--accent); color:#fdfbf5; }
5607
+ .chrome button:not(.on):hover { color:var(--fg); }
5608
+ @media print { body { max-width:none; padding:0; } body::after, .chrome { display:none; }
5609
+ section { break-inside:avoid; background:none; } }
5610
+ `;
5611
+ var CHROME_CONTROLS = `<nav class="chrome" aria-label="page controls">
5612
+ <div class="seg" role="group" aria-label="language"><button type="button" data-set-lang="en">EN</button><button type="button" data-set-lang="zh">\u4E2D</button></div>
5613
+ <div class="seg" role="group" aria-label="theme"><button type="button" data-set-theme="light" aria-label="light">\u2600</button><button type="button" data-set-theme="dark" aria-label="dark">\u263E</button></div>
5614
+ </nav>`;
5615
+ var CHROME_SCRIPT = `<script>
5616
+ (function () {
5617
+ var d = document.documentElement;
5618
+ function get(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
5619
+ function set(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
5620
+ var lang = get("roll-lang") || ((navigator.language || "").toLowerCase().indexOf("zh") === 0 ? "zh" : "en");
5621
+ var theme = get("roll-theme") || (window.matchMedia && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
5622
+ function apply() {
5623
+ d.setAttribute("data-lang", lang);
5624
+ d.setAttribute("lang", lang === "zh" ? "zh-CN" : "en");
5625
+ d.setAttribute("data-theme", theme);
5626
+ var bs = document.querySelectorAll("[data-set-lang],[data-set-theme]");
5627
+ for (var i = 0; i < bs.length; i++) {
5628
+ var b = bs[i];
5629
+ var on = b.getAttribute("data-set-lang") === lang || b.getAttribute("data-set-theme") === theme;
5630
+ b.classList.toggle("on", on);
5631
+ b.setAttribute("aria-pressed", String(on));
5632
+ }
5633
+ }
5634
+ apply();
5635
+ document.addEventListener("DOMContentLoaded", function () {
5636
+ var bs = document.querySelectorAll("[data-set-lang],[data-set-theme]");
5637
+ for (var i = 0; i < bs.length; i++) {
5638
+ bs[i].addEventListener("click", function () {
5639
+ var l = this.getAttribute("data-set-lang");
5640
+ var t = this.getAttribute("data-set-theme");
5641
+ if (l) { lang = l; set("roll-lang", l); }
5642
+ if (t) { theme = t; set("roll-theme", t); }
5643
+ apply();
5644
+ });
5645
+ }
5646
+ apply();
5647
+ });
5648
+ })();
5649
+ </script>`;
5650
+
5442
5651
  // packages/core/dist/attest/report.js
5443
5652
  var BADGE = {
5444
5653
  pass: { icon: "\u2705", en: "Pass", zh: "\u901A\u8FC7", cls: "s-pass" },
@@ -5476,9 +5685,9 @@ function deliveryBlock(d) {
5476
5685
  if (d.cycleId !== void 0 && d.cycleId !== "")
5477
5686
  parts.push(`<dt>Cycle</dt><dd><code>${esc(d.cycleId)}</code></dd>`);
5478
5687
  if (d.timeline !== void 0 && d.timeline !== "")
5479
- parts.push(`<dt>\u65F6\u95F4\u7EBF \xB7 Timeline</dt><dd>${esc(d.timeline)}</dd>`);
5688
+ parts.push(`<dt>${bi("Timeline", "\u65F6\u95F4\u7EBF")}</dt><dd>${esc(d.timeline)}</dd>`);
5480
5689
  if (d.cost !== void 0 && d.cost !== "")
5481
- parts.push(`<dt>\u6210\u672C \xB7 Cost</dt><dd>${esc(d.cost)}</dd>`);
5690
+ parts.push(`<dt>${bi("Cost", "\u6210\u672C")}</dt><dd>${esc(d.cost)}</dd>`);
5482
5691
  return parts.length > 0 ? `<dl class="delivery">${parts.join("")}</dl>` : "";
5483
5692
  }
5484
5693
  function cardContextBlock(ctx) {
@@ -5501,7 +5710,7 @@ function cardContextBlock(ctx) {
5501
5710
  rows.push(delivery);
5502
5711
  if (rows.length === 0)
5503
5712
  return "";
5504
- return `<section class="card-context"><h2>\u5361\u4E0A\u4E0B\u6587 \xB7 Context</h2>
5713
+ return `<section class="card-context"><h2>${bi("Context", "\u5361\u4E0A\u4E0B\u6587")}</h2>
5505
5714
  ${rows.join("\n")}
5506
5715
  </section>`;
5507
5716
  }
@@ -5511,11 +5720,11 @@ function acSection(item) {
5511
5720
  const business = item.evidence.filter((e) => e.kind !== "text");
5512
5721
  const technical = item.evidence.filter((e) => e.kind === "text");
5513
5722
  const bizHtml = business.map(evidenceCard).join("\n");
5514
- const techHtml = technical.length > 0 ? `<details class="tech"><summary>\u6280\u672F\u7EC6\u8282 \xB7 Technical detail\uFF08${technical.length}\uFF09</summary>
5723
+ const techHtml = technical.length > 0 ? `<details class="tech"><summary>${bi(`Technical detail (${technical.length})`, `\u6280\u672F\u7EC6\u8282\uFF08${technical.length}\uFF09`)}</summary>
5515
5724
  ${technical.map(evidenceCard).join("\n")}
5516
5725
  </details>` : "";
5517
5726
  return `<section class="ac ${b.cls}" id="${esc(item.id)}">
5518
- <h3><span class="badge">${b.icon} ${b.en} \xB7 ${b.zh}</span> <code>${esc(item.id)}</code></h3>
5727
+ <h3><span class="badge">${b.icon} ${bi(b.en, b.zh)}</span> <code>${esc(item.id)}</code></h3>
5519
5728
  <p class="ac-text">${esc(item.text)}</p>
5520
5729
  ${note}
5521
5730
  ${bizHtml}
@@ -5527,15 +5736,15 @@ function beforeAfterBlock(pairs) {
5527
5736
  return "";
5528
5737
  const fig = (ref, side, cls) => ref.href !== void 0 ? `<figure class="shot ${cls}"><img src="${esc(ref.href)}" alt="${esc(ref.label)}"><figcaption>${side}\uFF1A${esc(ref.label)}</figcaption></figure>` : "";
5529
5738
  const groups = pairs.map((p) => {
5530
- const before = fig(p.before, "Before \xB7 \u6539\u524D", "ba-before");
5531
- const after = fig(p.after, "After \xB7 \u6539\u540E", "ba-after");
5739
+ const before = fig(p.before, bi("Before", "\u6539\u524D"), "ba-before");
5740
+ const after = fig(p.after, bi("After", "\u6539\u540E"), "ba-after");
5532
5741
  if (before === "" && after === "")
5533
5742
  return "";
5534
5743
  return `<div class="before-after"><h3>${esc(p.label)}</h3><div class="ba-pair">${before}${after}</div></div>`;
5535
5744
  }).filter((s) => s !== "");
5536
5745
  if (groups.length === 0)
5537
5746
  return "";
5538
- return `<section class="before-after-section"><h2>\u5BF9\u7167\u5B9E\u62CD \xB7 Before / After</h2>
5747
+ return `<section class="before-after-section"><h2>${bi("Before / After", "\u5BF9\u7167\u5B9E\u62CD")}</h2>
5539
5748
  ${groups.join("\n")}
5540
5749
  </section>`;
5541
5750
  }
@@ -5543,7 +5752,7 @@ function selfCaptureBlock(refs) {
5543
5752
  if (refs === void 0 || refs.length === 0)
5544
5753
  return "";
5545
5754
  const figs = refs.map(evidenceCard).join("\n");
5546
- return `<section class="self-capture"><h2>Gate self-capture \xB7 \u81EA\u4EA7\u5B9E\u62CD</h2>
5755
+ return `<section class="self-capture"><h2>${bi("Gate self-capture", "Gate \u81EA\u4EA7\u5B9E\u62CD")}</h2>
5547
5756
  ${figs}
5548
5757
  </section>`;
5549
5758
  }
@@ -5567,7 +5776,7 @@ function evidenceIndexBlock(items, beforeAfter, selfCaptures) {
5567
5776
  row2(r);
5568
5777
  if (rows.length === 0)
5569
5778
  return "";
5570
- return `<section class="evidence-index"><h2>\u8BC1\u636E\u7D22\u5F15 \xB7 Evidence index</h2>
5779
+ return `<section class="evidence-index"><h2>${bi("Evidence index", "\u8BC1\u636E\u7D22\u5F15")}</h2>
5571
5780
  <table class="ev-index"><thead><tr><th>Kind</th><th>Label</th><th>Locator</th></tr></thead>
5572
5781
  <tbody>${rows.join("\n")}</tbody></table></section>`;
5573
5782
  }
@@ -5581,7 +5790,7 @@ function processTraceBlock(p) {
5581
5790
  if (p === void 0)
5582
5791
  return "";
5583
5792
  const rows = [];
5584
- const mode = p.delivery === "manual" ? `<p class="delivery-mode">\u{1F9D1}\u200D\u{1F527} conductor \u624B\u5DE5\u4EA4\u4ED8 \xB7 delivered by hand\uFF08\u65E0\u81EA\u52A8\u5468\u671F \xB7 no loop cycle\uFF09</p>` : `<p class="delivery-mode">\u{1F501} loop cycle${p.cycleId !== void 0 && p.cycleId !== "" ? ` <code>${esc(p.cycleId)}</code>` : ""}${p.agent !== void 0 && p.agent !== "" ? ` \xB7 agent <code>${esc(p.agent)}</code>` : ""}</p>`;
5793
+ const mode = p.delivery === "manual" ? `<p class="delivery-mode">\u{1F9D1}\u200D\u{1F527} ${bi("delivered by hand (no loop cycle)", "conductor \u624B\u5DE5\u4EA4\u4ED8\uFF08\u65E0\u81EA\u52A8\u5468\u671F\uFF09")}</p>` : `<p class="delivery-mode">\u{1F501} loop cycle${p.cycleId !== void 0 && p.cycleId !== "" ? ` <code>${esc(p.cycleId)}</code>` : ""}${p.agent !== void 0 && p.agent !== "" ? ` \xB7 agent <code>${esc(p.agent)}</code>` : ""}</p>`;
5585
5794
  rows.push(mode);
5586
5795
  if (p.timeline !== void 0 && p.timeline.length > 0) {
5587
5796
  const li = p.timeline.map((t2) => `<li class="tl-${t2.layer === "signal" ? "signal" : "outline"}"><span class="tl-offset">${esc(fmtOffset(t2.offsetSec))}</span> <span class="tl-label">${esc(t2.label)}</span></li>`).join("\n");
@@ -5590,18 +5799,18 @@ ${li}
5590
5799
  </ol>`);
5591
5800
  }
5592
5801
  if (p.missing !== void 0 && p.missing.length > 0) {
5593
- rows.push(`<p class="trace-missing">\u8FC7\u7A0B\u6570\u636E\u7F3A\u5931 \xB7 missing process data\uFF1A${p.missing.map(esc).join(" \xB7 ")}</p>`);
5802
+ rows.push(`<p class="trace-missing">${bi("missing process data", "\u8FC7\u7A0B\u6570\u636E\u7F3A\u5931")}\uFF1A${p.missing.map(esc).join(" \xB7 ")}</p>`);
5594
5803
  }
5595
5804
  if (p.transcript !== void 0) {
5596
5805
  const t2 = p.transcript;
5597
- const note = t2.truncated ? `\u5DF2\u622A\u65AD \xB7 truncated\uFF08\u5C55\u793A ${t2.shownLen} / ${t2.totalLen} \u5B57\u7B26 \xB7 chars shown\uFF09` : `\u5B8C\u6574\u5185\u8054 \xB7 full inline\uFF08${t2.totalLen} \u5B57\u7B26 \xB7 chars\uFF09`;
5598
- const idx = t2.originalPath !== void 0 && t2.originalPath !== "" ? `<p class="orig-path">\u673A\u5668\u539F\u4EF6 \xB7 machine original\uFF1A<code>${esc(t2.originalPath)}</code></p>` : "";
5599
- rows.push(`<details class="transcript"><summary>\u5B8C\u6574\u8F6C\u5F55 \xB7 Full transcript\uFF08${esc(note)}\uFF09</summary>
5806
+ const note = t2.truncated ? bi(`truncated \u2014 ${t2.shownLen} / ${t2.totalLen} chars shown`, `\u5DF2\u622A\u65AD\uFF08\u5C55\u793A ${t2.shownLen} / ${t2.totalLen} \u5B57\u7B26\uFF09`) : bi(`full inline \u2014 ${t2.totalLen} chars`, `\u5B8C\u6574\u5185\u8054\uFF08${t2.totalLen} \u5B57\u7B26\uFF09`);
5807
+ const idx = t2.originalPath !== void 0 && t2.originalPath !== "" ? `<p class="orig-path">${bi("machine original", "\u673A\u5668\u539F\u4EF6")}\uFF1A<code>${esc(t2.originalPath)}</code></p>` : "";
5808
+ rows.push(`<details class="transcript"><summary>${bi("Full transcript", "\u5B8C\u6574\u8F6C\u5F55")}\uFF08${note}\uFF09</summary>
5600
5809
  ${idx}
5601
5810
  ${t2.inlineHtml}
5602
5811
  </details>`);
5603
5812
  }
5604
- return `<section class="process-trace"><h2>\u8FC7\u7A0B\u6863\u6848 \xB7 Process trace</h2>
5813
+ return `<section class="process-trace"><h2>${bi("Process trace", "\u8FC7\u7A0B\u6863\u6848")}</h2>
5605
5814
  ${rows.join("\n")}
5606
5815
  </section>`;
5607
5816
  }
@@ -5609,7 +5818,7 @@ function selfScoreBlock(entries) {
5609
5818
  if (entries === void 0 || entries.length === 0)
5610
5819
  return "";
5611
5820
  const li = entries.map((e) => `<li><b>${esc(String(e.score))}</b>/10 \xB7 ${esc(e.verdict)} \xB7 <code>${esc(e.skill)}</code> \xB7 <span class="meta">${esc(e.ts)}</span>${e.note !== "" ? `<br><span class="note">${esc(e.note)}</span>` : ""}</li>`).join("\n");
5612
- return `<details class="selfscore"><summary>Self-Score \xB7 \u81EA\u8BC4\uFF08${entries.length}\uFF09</summary>
5821
+ return `<details class="selfscore"><summary>${bi("Self-Score", "\u81EA\u8BC4")}\uFF08${entries.length}\uFF09</summary>
5613
5822
  <ul>
5614
5823
  ${li}
5615
5824
  </ul>
@@ -5622,13 +5831,13 @@ function renderReport(input) {
5622
5831
  const counts = /* @__PURE__ */ new Map();
5623
5832
  for (const it of items)
5624
5833
  counts.set(it.status, (counts.get(it.status) ?? 0) + 1);
5625
- const summary = Object.keys(BADGE).filter((s) => (counts.get(s) ?? 0) > 0).map((s) => `<span class="badge ${BADGE[s].cls}">${BADGE[s].icon} ${BADGE[s].en} ${BADGE[s].zh} \xD7 ${counts.get(s)}</span>`).join(" ");
5834
+ const summary = Object.keys(BADGE).filter((s) => (counts.get(s) ?? 0) > 0).map((s) => `<span class="badge ${BADGE[s].cls}">${BADGE[s].icon} ${bi(BADGE[s].en, BADGE[s].zh)} \xD7 ${counts.get(s)}</span>`).join(" ");
5626
5835
  const facts = input.facts !== void 0 ? `<p class="facts">TCR commits: <b>${input.facts.tcrCount}</b> \xB7 CI: <b>${esc(input.facts.ciConclusion || "\u2014")}</b> \xB7 test-pass: <b>${esc(input.facts.testPassAge)}</b></p>` : "";
5627
- const disc = discrepancies.length > 0 ? `<section class="discrepancies"><h2>Discrepancies \xB7 \u8BC1\u636E\u7F3A\u53E3</h2>
5628
- <p>\u4E0B\u5217 AC \u56E0<strong>\u6CA1\u6709\u4EFB\u4F55\u8BC1\u636E\u6761\u76EE</strong>\u88AB\u5F3A\u5236\u964D\u7EA7\u4E3A \u{1F7E7} Claimed\uFF08\u7EA2\u7EBF\uFF0C\u6E32\u67D3\u5C42\u5F3A\u5236\uFF09\uFF1A</p>
5836
+ const disc = discrepancies.length > 0 ? `<section class="discrepancies"><h2>${bi("Discrepancies", "\u8BC1\u636E\u7F3A\u53E3")}</h2>
5837
+ <p>${bi("The ACs below carried <strong>zero evidence entries</strong> and were force-downgraded to \u{1F7E7} Claimed (red line, enforced by the renderer):", "\u4E0B\u5217 AC \u56E0<strong>\u6CA1\u6709\u4EFB\u4F55\u8BC1\u636E\u6761\u76EE</strong>\u88AB\u5F3A\u5236\u964D\u7EA7\u4E3A \u{1F7E7} Claimed\uFF08\u7EA2\u7EBF\uFF0C\u6E32\u67D3\u5C42\u5F3A\u5236\uFF09\uFF1A")}</p>
5629
5838
  <ul>${discrepancies.map((d) => `<li><a href="#${esc(d.id)}"><code>${esc(d.id)}</code></a> ${esc(d.text)}</li>`).join("\n")}</ul>
5630
5839
  </section>` : "";
5631
- const gate = facts !== "" ? `<h2>\u8D28\u91CF\u95E8\u7981 \xB7 Quality gate</h2>
5840
+ const gate = facts !== "" ? `<h2>${bi("Quality gate", "\u8D28\u91CF\u95E8\u7981")}</h2>
5632
5841
  ${facts}` : "";
5633
5842
  const evIndex = evidenceIndexBlock(items, input.beforeAfter, input.selfCaptures);
5634
5843
  const selfScore = selfScoreBlock(input.selfScores);
@@ -5643,31 +5852,31 @@ ${closingInner}
5643
5852
  <meta name="viewport" content="width=device-width, initial-scale=1">
5644
5853
  <title>${esc(input.storyId)} \u2014 Acceptance Evidence \xB7 \u9A8C\u6536\u8BC1\u636E</title>
5645
5854
  <style>
5646
- :root { color-scheme: light dark; --fg:#1f2328; --bg:#ffffff; --muted:#57606a; --line:#d0d7de; }
5647
- @media (prefers-color-scheme: dark) { :root { --fg:#e6edf3; --bg:#0d1117; --muted:#8b949e; --line:#30363d; } }
5648
- body { margin:0 auto; max-width:880px; padding:32px 20px 80px; background:var(--bg); color:var(--fg);
5649
- font:15px/1.65 -apple-system, "PingFang SC", "Segoe UI", sans-serif; }
5650
- h1 { font-size:22px; } h2 { font-size:18px; border-bottom:1px solid var(--line); padding-bottom:6px; }
5651
- code { background:rgba(127,127,127,.12); padding:1px 6px; border-radius:6px; font-size:.92em; }
5855
+ ${CHROME_CSS}
5652
5856
  .badge { display:inline-block; padding:2px 10px; border-radius:999px; font-size:12.5px; border:1px solid var(--line); }
5653
- .meta, .facts, .note { color:var(--muted); font-size:13px; }
5654
- section.ac { border:1px solid var(--line); border-radius:10px; padding:14px 16px; margin:14px 0; }
5857
+ .facts, .note { color:var(--muted); font-size:13px; }
5858
+ header.doc { position:relative; padding-right:96px; }
5859
+ .seal { position:absolute; right:0; top:2px; width:74px; height:74px; border:2px solid var(--accent);
5860
+ border-radius:50%; transform:rotate(-12deg); color:var(--accent); font-family:var(--serif);
5861
+ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1px;
5862
+ font-size:17px; letter-spacing:.12em; opacity:.85; box-shadow:inset 0 0 0 2px transparent, inset 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent); }
5863
+ .seal span { font-size:9.5px; letter-spacing:.34em; text-indent:.34em; }
5655
5864
  section.ac h3 { margin:0 0 6px; font-size:14.5px; }
5656
- .s-pass { border-left:4px solid #2da44e; } .s-readonly { border-left:4px solid #218bff; }
5657
- .s-partial { border-left:4px solid #d4a72c; } .s-claimed { border-left:4px solid #e8793a; }
5658
- .s-fail { border-left:4px solid #cf222e; } .s-blocked { border-left:4px solid #8250df; }
5659
- .s-missing { border-left:4px solid #cf222e; }
5660
- figure.shot { margin:10px 0; } figure.shot img { max-width:100%; border:1px solid var(--line); border-radius:8px; }
5865
+ .s-pass { border-left:4px solid var(--pass); } .s-readonly { border-left:4px solid var(--info); }
5866
+ .s-partial { border-left:4px solid var(--warn); } .s-claimed { border-left:4px solid var(--claim); }
5867
+ .s-fail { border-left:4px solid var(--fail); } .s-blocked { border-left:4px solid var(--block); }
5868
+ .s-missing { border-left:4px solid var(--fail); }
5869
+ figure.shot { margin:10px 0; } figure.shot img { max-width:100%; border:1px solid var(--line); border-radius:6px; }
5661
5870
  figure.shot figcaption { color:var(--muted); font-size:12.5px; }
5662
5871
  .ev { margin:6px 0; font-size:13.5px; } .ev-label { color:var(--muted); font-size:12.5px; margin-bottom:4px; }
5663
- .discrepancies { border:1px dashed #e8793a; border-radius:10px; padding:8px 16px; margin-top:28px; }
5664
- details.selfscore { margin-top:28px; border:1px solid var(--line); border-radius:10px; padding:8px 16px; }
5872
+ .discrepancies { border:1px dashed var(--claim); border-radius:8px; padding:8px 16px; margin-top:28px; }
5873
+ details.selfscore { margin-top:28px; border:1px solid var(--line); border-radius:8px; padding:8px 16px; background:var(--bg-raise); }
5665
5874
  details.selfscore summary { cursor:pointer; font-weight:600; }
5666
5875
  details.selfscore ul { margin:8px 0 4px; padding-left:18px; }
5667
- details.tech { margin:8px 0 2px; border:1px solid var(--line); border-radius:8px; padding:6px 12px; background:rgba(127,127,127,.04); }
5876
+ details.tech { margin:8px 0 2px; border:1px solid var(--line); border-radius:6px; padding:6px 12px; background:rgba(127,110,70,.04); }
5668
5877
  details.tech summary { cursor:pointer; color:var(--muted); font-size:12.5px; font-weight:600; }
5669
5878
  details.tech[open] summary { margin-bottom:6px; }
5670
- section.card-context { border:1px solid var(--line); border-radius:10px; padding:6px 16px 12px; margin:14px 0; }
5879
+ section.card-context { padding:6px 18px 12px; }
5671
5880
  section.card-context .one-liner { font-size:15.5px; font-weight:600; }
5672
5881
  section.card-context .ctx-meta { color:var(--muted); font-size:13px; }
5673
5882
  dl.delivery { display:grid; grid-template-columns:auto 1fr; gap:2px 12px; margin:8px 0 0; font-size:13.5px; }
@@ -5676,27 +5885,32 @@ dl.delivery dt { color:var(--muted); } dl.delivery dd { margin:0; }
5676
5885
  .ba-pair { display:flex; flex-wrap:wrap; gap:12px; } .ba-pair figure.shot { flex:1 1 280px; margin:0; }
5677
5886
  table.ev-index { width:100%; border-collapse:collapse; font-size:13px; margin-top:8px; }
5678
5887
  table.ev-index th, table.ev-index td { border:1px solid var(--line); padding:4px 8px; text-align:left; vertical-align:top; }
5679
- table.ev-index th { color:var(--muted); font-weight:600; }
5888
+ table.ev-index th { color:var(--muted); font-weight:600; font-family:var(--serif); letter-spacing:.04em; }
5680
5889
  table.ev-index td a { word-break:break-all; }
5681
- section.process-trace { margin-top:28px; border:1px solid var(--line); border-radius:10px; padding:6px 16px 12px; }
5890
+ section.process-trace { margin-top:28px; padding:6px 18px 12px; }
5682
5891
  .delivery-mode { font-size:13.5px; color:var(--muted); }
5683
5892
  ol.timeline { list-style:none; margin:8px 0; padding:0; font-size:13.5px; }
5684
5893
  ol.timeline li { padding:3px 0 3px 10px; border-left:2px solid var(--line); }
5685
- ol.timeline li.tl-signal { border-left:3px solid #218bff; font-weight:600; }
5894
+ ol.timeline li.tl-signal { border-left:3px solid var(--info); font-weight:600; }
5686
5895
  ol.timeline li.tl-outline { color:var(--muted); }
5687
- ol.timeline .tl-offset { display:inline-block; min-width:58px; color:var(--muted); font-variant-numeric:tabular-nums; font-size:12.5px; }
5688
- .trace-missing { color:#bf6a02; font-size:13px; }
5689
- details.transcript { margin-top:8px; border:1px solid var(--line); border-radius:8px; padding:6px 12px; background:rgba(127,127,127,.04); }
5896
+ ol.timeline .tl-offset { display:inline-block; min-width:58px; color:var(--muted); font-variant-numeric:tabular-nums; font-size:12.5px; font-family:var(--mono); }
5897
+ .trace-missing { color:var(--warn); font-size:13px; }
5898
+ details.transcript { margin-top:8px; border:1px solid var(--line); border-radius:6px; padding:6px 12px; background:rgba(127,110,70,.04); }
5690
5899
  details.transcript summary { cursor:pointer; color:var(--muted); font-size:12.5px; font-weight:600; }
5691
5900
  details.transcript .orig-path { font-size:12.5px; color:var(--muted); margin:6px 0; }
5692
- section.closing { margin-top:32px; border-top:2px solid var(--line); padding-top:8px; }
5693
- @media print { body { max-width:none; padding:0; } section.ac { break-inside:avoid; } }
5901
+ section.closing { margin-top:32px; border:none; background:none; border-radius:0; padding:8px 0 0; border-top:3px double var(--line); }
5694
5902
  ${ANSI_CSS}
5695
5903
  </style>
5904
+ ${CHROME_SCRIPT}
5696
5905
  </head>
5697
5906
  <body>
5907
+ ${CHROME_CONTROLS}
5908
+ <header class="doc">
5909
+ <p class="kicker">Roll \xB7 ${bi("Delivery Dossier", "\u4EA4\u4ED8\u6863\u6848")}</p>
5698
5910
  <h1>${esc(input.title)}</h1>
5699
- <p class="meta"><code>${esc(input.storyId)}</code> \xB7 generated ${esc(input.generatedAt)} \xB7 Gate: PASS</p>
5911
+ <p class="meta"><code>${esc(input.storyId)}</code> \xB7 ${bi("generated", "\u751F\u6210\u4E8E")} ${esc(input.generatedAt)} \xB7 Gate: PASS</p>
5912
+ <div class="seal" aria-hidden="true"><span>ROLL</span>\u9A8C\u8BAB</div>
5913
+ </header>
5700
5914
  ${cardContextBlock(input.context)}
5701
5915
  <p>${summary}</p>
5702
5916
  ${items.map(acSection).join("\n")}
@@ -5704,6 +5918,7 @@ ${beforeAfterBlock(input.beforeAfter)}
5704
5918
  ${selfCaptureBlock(input.selfCaptures)}
5705
5919
  ${processTraceBlock(input.process)}
5706
5920
  ${closing}
5921
+ <footer>Roll \xB7 ${bi("Acceptance Evidence", "\u9A8C\u6536\u8BC1\u636E")} \xB7 <code>${esc(input.storyId)}</code></footer>
5707
5922
  </body>
5708
5923
  </html>
5709
5924
  `;
@@ -5773,6 +5988,9 @@ function loopAlertPath() {
5773
5988
  const rt = process.env["ROLL_PROJECT_RUNTIME_DIR"];
5774
5989
  if (rt !== void 0 && rt !== "")
5775
5990
  return join4(rt, name);
5991
+ const local = join4(".roll", "loop", name);
5992
+ if (existsSync5(local))
5993
+ return local;
5776
5994
  const shared = process.env["_SHARED_ROOT"] ?? join4(homedir2(), ".shared", "roll");
5777
5995
  return join4(shared, "loop", name);
5778
5996
  }
@@ -5934,14 +6152,22 @@ function findFeatureFile(projectPath, storyId) {
5934
6152
  function reportFileName(storyId) {
5935
6153
  return `${storyId}-report.html`;
5936
6154
  }
6155
+ function isEpicName(name) {
6156
+ return name !== "" && name !== "." && name !== "features";
6157
+ }
6158
+ function epicFromFeaturePath(featureFile) {
6159
+ const storyDir = dirname4(featureFile);
6160
+ const epic = basename3(dirname4(storyDir));
6161
+ if (isEpicName(epic))
6162
+ return epic;
6163
+ const legacy = basename3(storyDir);
6164
+ return isEpicName(legacy) ? legacy : null;
6165
+ }
5937
6166
  function liveEpicOf(projectPath, storyId) {
5938
6167
  const file = findFeatureFile(projectPath, storyId);
5939
6168
  if (file === null)
5940
6169
  return null;
5941
- const epic = basename3(dirname4(file));
5942
- if (epic === "" || epic === "." || epic === "features")
5943
- return null;
5944
- return epic;
6170
+ return epicFromFeaturePath(file);
5945
6171
  }
5946
6172
  function readIndex(projectPath) {
5947
6173
  const p = join5(projectPath, ".roll", "index.json");
@@ -7149,7 +7375,70 @@ function screenshotEvidenceRef(result, href) {
7149
7375
 
7150
7376
  // packages/cli/dist/commands/attest.js
7151
7377
  import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync6, rmSync as rmSync5, symlinkSync as symlinkSync2, writeFileSync as writeFileSync7 } from "node:fs";
7152
- import { basename as basename4, dirname as dirname9, join as join10, relative } from "node:path";
7378
+ import { basename as basename4, join as join10, relative } from "node:path";
7379
+
7380
+ // packages/cli/dist/lib/story-page.js
7381
+ var STORY_ID_RE = /^(FIX|US|IDEA|REFACTOR)-/;
7382
+ function storyFamilyOf(storyId) {
7383
+ const m7 = STORY_ID_RE.exec(storyId);
7384
+ return m7 === null ? null : m7[1];
7385
+ }
7386
+ var STORY_PHASES = [
7387
+ { key: "design", en: "Design", zh: "\u8BBE\u8BA1", emptyEn: "Not yet started", emptyZh: "\u5C1A\u672A\u5F00\u59CB" },
7388
+ { key: "execution", en: "Execution", zh: "\u6267\u884C", emptyEn: "No cycles yet", emptyZh: "\u6682\u65E0\u5468\u671F" },
7389
+ { key: "delivery", en: "Delivery", zh: "\u4EA4\u4ED8", emptyEn: "Not yet delivered", emptyZh: "\u5C1A\u672A\u4EA4\u4ED8" },
7390
+ { key: "retrospective", en: "Retrospective", zh: "\u590D\u76D8", emptyEn: "Not yet written", emptyZh: "\u5C1A\u672A\u64B0\u5199" }
7391
+ ];
7392
+ function renderSpecMd(meta) {
7393
+ const type = meta.type ?? storyFamilyOf(meta.id)?.toLowerCase() ?? "unknown";
7394
+ return `---
7395
+ id: ${meta.id}
7396
+ ` + (meta.title !== void 0 ? `title: ${meta.title}
7397
+ ` : "") + `type: ${type}
7398
+ ` + (meta.epic !== void 0 ? `epic: ${meta.epic}
7399
+ ` : "") + `created: ${meta.created}
7400
+ ---
7401
+
7402
+ # ${meta.id}${meta.title !== void 0 ? ` \u2014 ${meta.title}` : ""}
7403
+ ` + (meta.note !== void 0 ? `
7404
+ > ${meta.note}
7405
+ ` : "");
7406
+ }
7407
+ function renderStoryPage(meta) {
7408
+ const q = (s) => s.replace(/"/g, "&quot;");
7409
+ const badge = storyFamilyOf(meta.id) ?? meta.id.split("-")[0];
7410
+ const sections = STORY_PHASES.map((p) => `<section class="phase phase-pending" data-phase="${p.key}"><h2>${bi(p.en, p.zh)}</h2><p class="empty">${bi(p.emptyEn, p.emptyZh)}</p></section>`).join("\n");
7411
+ return `<!DOCTYPE html>
7412
+ <html lang="zh-CN">
7413
+ <head>
7414
+ <meta charset="UTF-8">
7415
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7416
+ <title>${meta.title !== void 0 ? `${meta.id} \u2014 ${q(meta.title)}` : meta.id}</title>
7417
+ <style>
7418
+ ${CHROME_CSS}.phase-done { border-left:4px solid var(--pass); } .phase-pending { border-left:4px solid var(--line); }
7419
+ </style>
7420
+ ${CHROME_SCRIPT}
7421
+ </head>
7422
+ <body>
7423
+ ${CHROME_CONTROLS}
7424
+ <p class="kicker">Roll \xB7 ${bi("Story Dossier", "\u6545\u4E8B\u6863\u6848")}</p>
7425
+ <h1>${meta.id}</h1>
7426
+ <p class="meta"><code>${badge}</code> \xB7 ${bi("Created", "\u521B\u5EFA\u4E8E")} ${meta.created}</p>
7427
+ ` + (meta.title !== void 0 ? `<p>${q(meta.title)}</p>
7428
+ ` : "") + `${sections}
7429
+ <footer>Roll \xB7 <a href="spec.md">spec.md</a></footer>
7430
+ </body>
7431
+ </html>
7432
+ `;
7433
+ }
7434
+ function markPhaseDone(html, phaseKey, innerHtml) {
7435
+ const p = STORY_PHASES.find((x) => x.key === phaseKey);
7436
+ if (p === void 0)
7437
+ return html;
7438
+ return html.replace(new RegExp(`<section class="phase phase-pending" data-phase="${p.key}">[\\s\\S]*?</section>`), `<section class="phase phase-done" data-phase="${p.key}"><h2>${bi(p.en, p.zh)}</h2>${innerHtml}</section>`);
7439
+ }
7440
+
7441
+ // packages/cli/dist/commands/attest.js
7153
7442
  function defaultProcessReaders(projectPath, env) {
7154
7443
  const rt = (env.ROLL_PROJECT_RUNTIME_DIR ?? "").trim() || join10(projectPath, ".roll", "loop");
7155
7444
  const bus = new EventBus();
@@ -7380,11 +7669,11 @@ function buildCardContext(projectPath, featureFile, storyId, env) {
7380
7669
  summary = extractSummary(readFileSync9(featureFile, "utf8"));
7381
7670
  } catch {
7382
7671
  }
7383
- const epic = basename4(dirname9(featureFile));
7672
+ const epic = epicFromFeaturePath(featureFile);
7384
7673
  const cycleId = env.LOOP_CYCLE_ID;
7385
7674
  const ctx = {
7386
7675
  ...oneLiner !== void 0 && oneLiner !== "" ? { oneLiner } : {},
7387
- ...epic !== "" && epic !== "." && epic !== "features" ? { epic } : {},
7676
+ ...epic !== null ? { epic } : {},
7388
7677
  ...summary !== void 0 ? { summary } : {},
7389
7678
  ...row2.status !== void 0 ? { backlogStatus: row2.status } : {},
7390
7679
  ...cycleId !== void 0 && cycleId !== "" ? { delivery: { cycleId } } : {}
@@ -7535,6 +7824,18 @@ async function attestCommand(args, deps = {}) {
7535
7824
  } catch {
7536
7825
  warn2("latest symlink update failed (report still written)");
7537
7826
  }
7827
+ const indexPath = join10(storyDir, "index.html");
7828
+ if (existsSync14(indexPath)) {
7829
+ try {
7830
+ const reportRel = join10(runId, reportFileName(storyId));
7831
+ const deliveryHtml = `<p><a href="${reportRel}">${bi("Attestation report", "\u9A8C\u6536\u62A5\u544A")}</a></p>
7832
+ <p class="muted">${bi("Delivered", "\u4EA4\u4ED8\u4E8E")} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}</p>
7833
+ `;
7834
+ const idx = markPhaseDone(readFileSync9(indexPath, "utf8"), "delivery", deliveryHtml);
7835
+ writeFileSync7(indexPath, idx, "utf8");
7836
+ } catch {
7837
+ }
7838
+ }
7538
7839
  const smoke = smokeCheckReport(html, (rel) => existsSync14(join10(runDir, rel)));
7539
7840
  process.stdout.write(`Acceptance report written
7540
7841
  \u9A8C\u6536\u62A5\u544A\u5DF2\u751F\u6210
@@ -8274,7 +8575,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
8274
8575
  // packages/cli/dist/commands/setup-shared.js
8275
8576
  import { copyFileSync, existsSync as existsSync18, lstatSync, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync7, readlinkSync as readlinkSync2, realpathSync as realpathSync2, rmSync as rmSync6, statSync as statSync8, symlinkSync as symlinkSync3, writeFileSync as writeFileSync9 } from "node:fs";
8276
8577
  import { homedir as homedir5 } from "node:os";
8277
- import { basename as basename5, dirname as dirname10, join as join12 } from "node:path";
8578
+ import { basename as basename5, dirname as dirname9, join as join12 } from "node:path";
8278
8579
  function rollHome() {
8279
8580
  return process.env["ROLL_HOME"] ?? join12(homedir5(), ".roll");
8280
8581
  }
@@ -8322,7 +8623,7 @@ function canonicalDir(path) {
8322
8623
  return null;
8323
8624
  }
8324
8625
  }
8325
- function agentBinNames2(agent) {
8626
+ function agentBinNames3(agent) {
8326
8627
  switch (agent) {
8327
8628
  case "claude":
8328
8629
  return ["claude"];
@@ -8359,7 +8660,7 @@ function onPath(bin) {
8359
8660
  }
8360
8661
  return false;
8361
8662
  }
8362
- function agentInstalledByName2(agent, dir = "") {
8663
+ function agentInstalledByName3(agent, dir = "") {
8363
8664
  const home = homedir5();
8364
8665
  switch (agent) {
8365
8666
  case "trae":
@@ -8379,7 +8680,7 @@ function agentInstalledByName2(agent, dir = "") {
8379
8680
  default:
8380
8681
  break;
8381
8682
  }
8382
- const bins = agentBinNames2(agent);
8683
+ const bins = agentBinNames3(agent);
8383
8684
  if (bins !== null) {
8384
8685
  return bins.some((b) => onPath(b));
8385
8686
  }
@@ -8388,13 +8689,13 @@ function agentInstalledByName2(agent, dir = "") {
8388
8689
  function isAiInstalled(aiDir) {
8389
8690
  let bn = basename5(aiDir).replace(/^\./, "");
8390
8691
  if (bn === "agent" || bn === "workspace") {
8391
- bn = basename5(dirname10(aiDir)).replace(/^\./, "");
8692
+ bn = basename5(dirname9(aiDir)).replace(/^\./, "");
8392
8693
  }
8393
8694
  if (bn === "gemini")
8394
8695
  bn = "agy";
8395
8696
  if (bn === "kimi-code")
8396
8697
  bn = "kimi";
8397
- return agentInstalledByName2(bn, aiDir);
8698
+ return agentInstalledByName3(bn, aiDir);
8398
8699
  }
8399
8700
  function listDirEntries(dir) {
8400
8701
  try {
@@ -8425,7 +8726,7 @@ function pruneDir(installedDir, sourceDir) {
8425
8726
  function safeCopy(src, dst, force) {
8426
8727
  if (!existsSync18(src))
8427
8728
  return;
8428
- mkdirSync10(dirname10(dst), { recursive: true });
8729
+ mkdirSync10(dirname9(dst), { recursive: true });
8429
8730
  if (existsSync18(dst) && !force) {
8430
8731
  if (sameFile(src, dst))
8431
8732
  return;
@@ -8602,7 +8903,7 @@ loop_dream_hour: 3
8602
8903
  # loop_dream_minute: 10 # omit to auto-derive
8603
8904
  primary_agent: claude
8604
8905
  `;
8605
- function firstInstalledAgent() {
8906
+ function firstInstalledAgent2() {
8606
8907
  for (const agent of [
8607
8908
  "claude",
8608
8909
  "codex",
@@ -8616,7 +8917,7 @@ function firstInstalledAgent() {
8616
8917
  "trae",
8617
8918
  "openclaw"
8618
8919
  ]) {
8619
- if (agentInstalledByName2(agent))
8920
+ if (agentInstalledByName3(agent))
8620
8921
  return agent;
8621
8922
  }
8622
8923
  return null;
@@ -8641,7 +8942,7 @@ function installLocal(force) {
8641
8942
  }
8642
8943
  if (!existsSync18(cfg)) {
8643
8944
  writeFileSync9(cfg, DEFAULT_CONFIG);
8644
- const detected = firstInstalledAgent();
8945
+ const detected = firstInstalledAgent2();
8645
8946
  if (detected !== null && detected !== "claude")
8646
8947
  replacePrimaryAgent(detected);
8647
8948
  }
@@ -8651,7 +8952,7 @@ function installLocal(force) {
8651
8952
  function syncConventionForTool(src, mainDst, force) {
8652
8953
  if (!existsSync18(src))
8653
8954
  return;
8654
- const dstDir = dirname10(mainDst);
8955
+ const dstDir = dirname9(mainDst);
8655
8956
  if (dstDir !== join12(homedir5(), ".claude") && !isAiInstalled(dstDir) && !existsSync18(dstDir)) {
8656
8957
  return;
8657
8958
  }
@@ -10044,11 +10345,16 @@ function detectLiveCycle(rtDir, cycles, now, pidAlive = systemPidAlive) {
10044
10345
  const hb = livenessVerdict(join15(rtDir, "heartbeat"), { now: () => nowSec2 });
10045
10346
  if (!hb.alive)
10046
10347
  return dead;
10047
- const open = cycles.find((cy) => cy.start !== null && cy.end === null);
10048
- if (open === void 0 || open.start === null)
10049
- return dead;
10050
- const elapsedSec = Math.max(0, Math.trunc((now.getTime() - open.start.getTime()) / 1e3));
10051
- return { running: true, story: open.story, elapsedSec };
10348
+ const ZOMBIE_THRESHOLD_SEC = 2 * 3600;
10349
+ for (const cy of cycles) {
10350
+ if (cy.start === null || cy.end !== null)
10351
+ continue;
10352
+ const elapsedSec = Math.max(0, Math.trunc((now.getTime() - cy.start.getTime()) / 1e3));
10353
+ if (elapsedSec > ZOMBIE_THRESHOLD_SEC)
10354
+ continue;
10355
+ return { running: true, story: cy.story, elapsedSec };
10356
+ }
10357
+ return dead;
10052
10358
  }
10053
10359
  function loadRuns(slug) {
10054
10360
  const rtDir = loopRuntimeDir(slug);
@@ -10785,8 +11091,17 @@ function nextCronHint(zh) {
10785
11091
  const nxtSh = toShanghai(new Date(nxtEpoch));
10786
11092
  return `${pad22(nxtSh.getUTCHours())}:${pad22(nxtSh.getUTCMinutes())} \xB7 in ${mins}m ${pad22(secs)}s`;
10787
11093
  }
11094
+ function renderNow() {
11095
+ const v = process.env["ROLL_RENDER_NOW"] ?? "";
11096
+ if (v !== "") {
11097
+ const ms = /^\d+$/.test(v) ? Number(v) : Date.parse(v);
11098
+ if (!Number.isNaN(ms))
11099
+ return new Date(ms);
11100
+ }
11101
+ return /* @__PURE__ */ new Date();
11102
+ }
10788
11103
  function fixtureData() {
10789
- const now = /* @__PURE__ */ new Date();
11104
+ const now = renderNow();
10790
11105
  const events = [];
10791
11106
  const cron = [];
10792
11107
  let cycleId = 0;
@@ -10989,7 +11304,7 @@ function render(out2, events, cron, state, backlog, args) {
10989
11304
  out2.push(" " + c("dim", agentLine));
10990
11305
  let skillLine = "";
10991
11306
  try {
10992
- skillLine = selfScoreSummaryLine();
11307
+ skillLine = selfScoreSummaryLine(process.env["ROLL_NOTES_DIR"]);
10993
11308
  } catch {
10994
11309
  skillLine = "";
10995
11310
  }
@@ -11380,7 +11695,7 @@ roll-loop-status.py: error: unrecognized arguments: ${args.unknown.join(" ")}
11380
11695
  }
11381
11696
  renderState.useColor = !args.noColor && (process.env["NO_COLOR"] ?? "") === "" && (process.stdout.isTTY === true || (process.env["FORCE_COLOR"] ?? "") !== "");
11382
11697
  const lang4 = args.en ? "en" : args.zh ? "zh" : "both";
11383
- const now = /* @__PURE__ */ new Date();
11698
+ const now = renderNow();
11384
11699
  let events;
11385
11700
  let cron;
11386
11701
  let state;
@@ -11727,7 +12042,7 @@ function loopRunsCommand(argv) {
11727
12042
 
11728
12043
  // packages/cli/dist/commands/loop-signals.js
11729
12044
  import { appendFileSync as appendFileSync3, existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "node:fs";
11730
- import { dirname as dirname11, join as join17 } from "node:path";
12045
+ import { dirname as dirname10, join as join17 } from "node:path";
11731
12046
  function lang2() {
11732
12047
  return resolveLang({ rollLang: process.env["ROLL_LANG"], lcAll: process.env["LC_ALL"], lang: process.env["LANG"] });
11733
12048
  }
@@ -11782,7 +12097,7 @@ function loopSignalsCommand(argv) {
11782
12097
  const seenFile = join17(rtDir, `signals-seen-${slug}`);
11783
12098
  const candFile = join17(projectPath, ".roll", "signals", "candidates.md");
11784
12099
  mkdirSync11(rtDir, { recursive: true });
11785
- mkdirSync11(dirname11(candFile), { recursive: true });
12100
+ mkdirSync11(dirname10(candFile), { recursive: true });
11786
12101
  if (!existsSync22(seenFile))
11787
12102
  writeFileSync10(seenFile, "");
11788
12103
  let lastId = 0;
@@ -12040,7 +12355,7 @@ var out = { lines: [] };
12040
12355
  function emit(line) {
12041
12356
  out.lines.push(line);
12042
12357
  }
12043
- function agentBinNames3(agent) {
12358
+ function agentBinNames4(agent) {
12044
12359
  switch (agent) {
12045
12360
  case "claude":
12046
12361
  return ["claude"];
@@ -12077,7 +12392,7 @@ function commandOnPath2(bin) {
12077
12392
  }
12078
12393
  return false;
12079
12394
  }
12080
- function agentInstalledByName3(agent, dir) {
12395
+ function agentInstalledByName4(agent, dir) {
12081
12396
  const home = homedir9();
12082
12397
  switch (agent) {
12083
12398
  case "trae":
@@ -12096,7 +12411,7 @@ function agentInstalledByName3(agent, dir) {
12096
12411
  case "openclaw":
12097
12412
  return existsSync24(join19(home, ".openclaw", "workspace"));
12098
12413
  default: {
12099
- const bins = agentBinNames3(agent);
12414
+ const bins = agentBinNames4(agent);
12100
12415
  if (bins !== null)
12101
12416
  return bins.some(commandOnPath2);
12102
12417
  return dir !== "" && existsSync24(dir) && safeIsDir(dir);
@@ -12145,7 +12460,7 @@ function agentSection(p) {
12145
12460
  dir = dir.replace(/^ /, "");
12146
12461
  if (dir.startsWith("~"))
12147
12462
  dir = home + dir.slice(1);
12148
- const installed = agentInstalledByName3(name, dir) ? t(v2Catalog, msgLang6(), "doctor.agent_installed") : t(v2Catalog, msgLang6(), "doctor.agent_missing");
12463
+ const installed = agentInstalledByName4(name, dir) ? t(v2Catalog, msgLang6(), "doctor.agent_installed") : t(v2Catalog, msgLang6(), "doctor.agent_missing");
12149
12464
  const dirExists = safeIsDir(dir) ? t(v2Catalog, msgLang6(), "doctor.agent_dir_exists") : t(v2Catalog, msgLang6(), "doctor.agent_dir_missing");
12150
12465
  const tag = name === primary ? ` (${t(v2Catalog, msgLang6(), "doctor.agent_primary_label")})` : "";
12151
12466
  emit(` ${padEnd(name, 10)} ${padEnd(installed, 14)} ${dirExists}${tag}`);
@@ -12351,16 +12666,8 @@ function killLiveAgents(signal2 = "SIGKILL") {
12351
12666
  }
12352
12667
  var AUTORUN_DIRECTIVE = "[roll \u81EA\u4E3B\u6A21\u5F0F] \u4F60\u6B63\u5728\u65E0\u4EBA\u503C\u5B88\u7684\u81EA\u52A8\u5316\u5FAA\u73AF\u4E2D\u8FD0\u884C,\u8FD9\u4E0D\u662F\u5BF9\u8BDD\u3002\u8BF7\u7ACB\u5373\u3001\u5B8C\u6574\u5730\u6267\u884C\u4E0B\u9762\u8FD9\u4EFD\u6280\u80FD\u6587\u6863\u63CF\u8FF0\u7684\u5DE5\u4F5C\u6D41,\u76F4\u5230\u5B8C\u6210\u4EA4\u4ED8\u6216\u5199\u51FA ALERT \u4E3A\u6B62;\u4E25\u7981\u53CD\u95EE\u3001\u4E25\u7981\u7B49\u5F85\u786E\u8BA4\u3001\u4E25\u7981\u53EA\u590D\u8FF0\u6216\u603B\u7ED3\u800C\u4E0D\u52A8\u624B\u3002\u6280\u80FD\u6587\u6863\u5982\u4E0B: ";
12353
12668
  var AGENT_ARGV_TODO = {
12354
- kimi: "kimi-code|kimi-cli|kimi -p <prompt> (FIX-126/133; no stream-json splice)",
12355
- pi: "pi -p <prompt> (text mode; no usage on stdout \u2014 cost via session file)",
12356
- deepseek: "deepseek <prompt> (positional)",
12357
- codex: "codex exec <prompt>",
12358
12669
  openai: "codex exec <prompt>",
12359
- opencode: "opencode run <prompt>",
12360
- gemini: "agy -p --dangerously-skip-permissions <prompt> (FIX-153)",
12361
- agy: "agy -p --dangerously-skip-permissions <prompt> (FIX-153)",
12362
- antigravity: "agy -p --dangerously-skip-permissions <prompt> (FIX-153)",
12363
- qwen: "qwen <prompt> (positional)"
12670
+ opencode: "opencode run <prompt>"
12364
12671
  };
12365
12672
  function storyPinDirective(storyId) {
12366
12673
  return `[\u672C\u5468\u671F\u6307\u5B9A\u6545\u4E8B] \u8C03\u5EA6\u5668\u5DF2\u9501\u5B9A ${storyId} \u5E76\u5728 backlog \u6807\u8BB0 \u{1F528} In Progress\u2014\u2014\u53EA\u6267\u884C\u8FD9\u4E00\u4E2A\u6545\u4E8B,\u4E25\u7981\u91CD\u65B0\u6311\u9009\u6216\u987A\u624B\u505A\u522B\u7684;\u82E5\u5B83\u786E\u5B9E\u4E0D\u53EF\u6267\u884C,\u5199 ALERT \u8BF4\u660E\u539F\u56E0\u540E\u5E72\u51C0\u9000\u51FA\u3002
@@ -12371,7 +12678,7 @@ function buildClaudeArgv(input) {
12371
12678
  const bin = input.bin ?? "claude";
12372
12679
  const pin = input.storyId !== void 0 && input.storyId !== "" ? storyPinDirective(input.storyId) : "";
12373
12680
  const prompt = `${AUTORUN_DIRECTIVE}${pin}${input.skillBody}`;
12374
- const args = [
12681
+ const args = input.interactive ? ["-p", prompt, "--dangerously-skip-permissions", "--add-dir", input.worktree] : [
12375
12682
  "-p",
12376
12683
  prompt,
12377
12684
  "--verbose",
@@ -12383,20 +12690,42 @@ function buildClaudeArgv(input) {
12383
12690
  ];
12384
12691
  return { bin, args };
12385
12692
  }
12386
- var realAgentSpawn = (agent, opts) => {
12387
- if (agent !== "claude") {
12388
- const hint = AGENT_ARGV_TODO[agent] ?? "unknown agent";
12389
- throw new Error(`runner: agent '${agent}' argv not yet ported (only 'claude' is). v2 shape: ${hint}`);
12693
+ function buildSpawnCommand(agent, opts) {
12694
+ const prompt = `${AUTORUN_DIRECTIVE}${opts.storyId !== void 0 && opts.storyId !== "" ? storyPinDirective(opts.storyId) : ""}${opts.skillBody}`;
12695
+ if (agent === "claude") {
12696
+ return buildClaudeArgv({
12697
+ worktree: opts.cwd,
12698
+ skillBody: opts.skillBody,
12699
+ ...opts.storyId !== void 0 ? { storyId: opts.storyId } : {},
12700
+ bin: opts.bin,
12701
+ interactive: opts.interactive
12702
+ });
12390
12703
  }
12391
- const { bin, args } = buildClaudeArgv({
12392
- worktree: opts.cwd,
12393
- skillBody: opts.skillBody,
12394
- ...opts.storyId !== void 0 ? { storyId: opts.storyId } : {},
12395
- bin: opts.bin
12396
- });
12704
+ if (agent === "pi") {
12705
+ return { bin: opts.bin ?? "pi", args: ["-p", prompt] };
12706
+ }
12707
+ if (agent === "kimi") {
12708
+ return { bin: opts.bin ?? "kimi", args: ["-p", prompt] };
12709
+ }
12710
+ if (agent === "codex") {
12711
+ return { bin: opts.bin ?? "codex", args: ["exec", prompt] };
12712
+ }
12713
+ if (agent === "deepseek") {
12714
+ return { bin: opts.bin ?? "deepseek", args: [prompt] };
12715
+ }
12716
+ if (agent === "qwen") {
12717
+ return { bin: opts.bin ?? "qwen", args: [prompt] };
12718
+ }
12719
+ if (agent === "agy" || agent === "gemini" || agent === "antigravity") {
12720
+ return { bin: opts.bin ?? "agy", args: ["-p", "--dangerously-skip-permissions", prompt] };
12721
+ }
12722
+ const hint = AGENT_ARGV_TODO[agent] ?? "unknown agent";
12723
+ throw new Error(`runner: agent '${agent}' argv not yet ported. v2 shape: ${hint}`);
12724
+ }
12725
+ function spawnAndWait(bin, args, opts) {
12397
12726
  process.stderr.write(`[runner] spawn ${bin} argv=${JSON.stringify(args.map((a) => a.length > 80 ? `${a.slice(0, 77)}...` : a))}
12398
12727
  `);
12399
- return new Promise((resolve3, reject) => {
12728
+ return new Promise((resolve3) => {
12400
12729
  const child = spawn(bin, args, {
12401
12730
  cwd: opts.cwd,
12402
12731
  env: opts.env ?? process.env,
@@ -12442,16 +12771,22 @@ var realAgentSpawn = (agent, opts) => {
12442
12771
  settle({ stdout, stderr, exitCode: code ?? 1, timedOut });
12443
12772
  });
12444
12773
  });
12774
+ }
12775
+ var realAgentSpawn = (agent, opts) => {
12776
+ const { bin, args } = buildSpawnCommand(agent, opts);
12777
+ return spawnAndWait(bin, args, opts);
12445
12778
  };
12446
12779
 
12447
12780
  // packages/cli/dist/runner/skill-body.js
12448
12781
  import { existsSync as existsSync25, readFileSync as readFileSync20 } from "node:fs";
12782
+ import { homedir as homedir10 } from "node:os";
12449
12783
  import { join as join20 } from "node:path";
12450
12784
  function readSkillBody(projectPath, opts) {
12451
12785
  const candidates = [
12452
12786
  opts.envOverride ?? "",
12453
12787
  join20(projectPath, ".roll", "skills", opts.skillName, "SKILL.md"),
12454
- join20(projectPath, "skills", opts.skillName, "SKILL.md")
12788
+ join20(projectPath, "skills", opts.skillName, "SKILL.md"),
12789
+ join20(homedir10(), ".roll", "skills", opts.skillName, "SKILL.md")
12455
12790
  ].filter((p) => p !== "");
12456
12791
  for (const p of candidates) {
12457
12792
  if (!existsSync25(p))
@@ -12527,7 +12862,7 @@ dream run-once: \u627E\u4E0D\u5230 roll-.dream SKILL.md \u2014 \u62D2\u7EDD\u76F
12527
12862
  // packages/cli/dist/commands/feedback.js
12528
12863
  import { execFileSync as execFileSync6, spawnSync as spawnSync4 } from "node:child_process";
12529
12864
  import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
12530
- import { homedir as homedir10 } from "node:os";
12865
+ import { homedir as homedir11 } from "node:os";
12531
12866
  import { basename as basename8, join as join23 } from "node:path";
12532
12867
 
12533
12868
  // packages/cli/dist/commands/version.js
@@ -12571,7 +12906,7 @@ function err8(line) {
12571
12906
  `);
12572
12907
  }
12573
12908
  function projectAgent2() {
12574
- const readField = (file, key) => {
12909
+ const readField2 = (file, key) => {
12575
12910
  if (!existsSync26(file))
12576
12911
  return void 0;
12577
12912
  let text;
@@ -12590,14 +12925,14 @@ function projectAgent2() {
12590
12925
  };
12591
12926
  const local = ".roll/local.yaml";
12592
12927
  if (existsSync26(local) && /^agent:/m.test(safeRead(local))) {
12593
- return readField(local, /^agent:/) ?? "claude";
12928
+ return readField2(local, /^agent:/) ?? "claude";
12594
12929
  }
12595
12930
  if (existsSync26(".roll.yaml") && /^agent:/m.test(safeRead(".roll.yaml"))) {
12596
- return readField(".roll.yaml", /^agent:/) ?? "claude";
12931
+ return readField2(".roll.yaml", /^agent:/) ?? "claude";
12597
12932
  }
12598
12933
  const cfg = rollConfig2();
12599
12934
  if (existsSync26(cfg) && /primary_agent:/.test(safeRead(cfg))) {
12600
- return readField(cfg, /primary_agent:/) ?? "claude";
12935
+ return readField2(cfg, /primary_agent:/) ?? "claude";
12601
12936
  }
12602
12937
  return "claude";
12603
12938
  }
@@ -12609,7 +12944,7 @@ function safeRead(p) {
12609
12944
  }
12610
12945
  }
12611
12946
  function rollConfig2() {
12612
- const rollHome4 = process.env["ROLL_HOME"] ?? join23(homedir10(), ".roll");
12947
+ const rollHome4 = process.env["ROLL_HOME"] ?? join23(homedir11(), ".roll");
12613
12948
  return join23(rollHome4, "config.yaml");
12614
12949
  }
12615
12950
  function feedbackYamlField(file, field) {
@@ -12656,7 +12991,7 @@ function feedbackDefaultRepo() {
12656
12991
  let v = feedbackYamlField(".roll/local.yaml", "feedback_repo");
12657
12992
  if (v)
12658
12993
  return v;
12659
- v = feedbackYamlField(join23(homedir10(), ".roll", "config.yaml"), "feedback_repo");
12994
+ v = feedbackYamlField(join23(homedir11(), ".roll", "config.yaml"), "feedback_repo");
12660
12995
  if (v)
12661
12996
  return v;
12662
12997
  return feedbackOriginRepo();
@@ -12922,7 +13257,8 @@ function gcCommand(args, deps = {}) {
12922
13257
  }
12923
13258
 
12924
13259
  // packages/cli/dist/commands/idea.js
12925
- import { existsSync as existsSync28 } from "node:fs";
13260
+ import { existsSync as existsSync28, mkdirSync as mkdirSync13, writeFileSync as writeFileSync13 } from "node:fs";
13261
+ import { join as join25 } from "node:path";
12926
13262
  var BACKLOG_PATH2 = ".roll/backlog.md";
12927
13263
  function label2(lang4, key, ...args) {
12928
13264
  if (v3Catalog[key] !== void 0)
@@ -12989,29 +13325,127 @@ ${c("green", "\u{1F4DD} " + label2(lang4, "ideav3.recorded", plan.id))}
12989
13325
  process.stdout.write(` ${c("dim", label2(lang4, "ideav3.text") + ":")} ${text}
12990
13326
 
12991
13327
  `);
13328
+ const projectPath = process.cwd();
13329
+ const cardDir = join25(projectPath, ".roll", "features", "uncategorized", plan.id);
13330
+ try {
13331
+ mkdirSync13(cardDir, { recursive: true });
13332
+ const card = {
13333
+ id: plan.id,
13334
+ title: text,
13335
+ type: plan.kind,
13336
+ created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
13337
+ };
13338
+ writeFileSync13(join25(cardDir, "spec.md"), renderSpecMd(card), "utf8");
13339
+ writeFileSync13(join25(cardDir, "index.html"), renderStoryPage(card), "utf8");
13340
+ } catch {
13341
+ }
12992
13342
  return 0;
12993
13343
  }
12994
13344
 
12995
13345
  // packages/cli/dist/commands/index-gen.js
13346
+ import { existsSync as existsSync29, readdirSync as readdirSync14, statSync as statSync14, writeFileSync as writeFileSync14 } from "node:fs";
13347
+ import { join as join26 } from "node:path";
12996
13348
  function indexCommand(args) {
12997
13349
  if (args.includes("--help") || args.includes("-h")) {
12998
- process.stdout.write("Usage: roll index\n Regenerate .roll/index.json (ID\u2192epic map) from .roll/backlog.md.\n");
13350
+ process.stdout.write("Usage: roll index\n Regenerate .roll/index.json + .roll/features/index.html\n");
12999
13351
  return 0;
13000
13352
  }
13001
- const stories = generateIndex(process.cwd());
13353
+ const cwd = process.cwd();
13354
+ const stories = generateIndex(cwd);
13002
13355
  const n = Object.keys(stories).length;
13003
13356
  process.stdout.write(`index.json regenerated
13004
13357
  \u7D22\u5F15\u5DF2\u91CD\u5EFA
13005
13358
  ${n} stories mapped to epics (.roll/index.json)
13006
13359
  `);
13360
+ const featuresDir = join26(cwd, ".roll", "features");
13361
+ if (existsSync29(featuresDir)) {
13362
+ const epics = /* @__PURE__ */ new Map();
13363
+ for (const [sid, epic] of Object.entries(stories)) {
13364
+ if (!epics.has(epic))
13365
+ epics.set(epic, /* @__PURE__ */ new Set());
13366
+ epics.get(epic).add(sid);
13367
+ }
13368
+ try {
13369
+ for (const d of readdirSync14(featuresDir)) {
13370
+ const epicDir = join26(featuresDir, d);
13371
+ if (!statSync14(epicDir).isDirectory())
13372
+ continue;
13373
+ if (!epics.has(d))
13374
+ epics.set(d, /* @__PURE__ */ new Set());
13375
+ for (const s of readdirSync14(epicDir)) {
13376
+ const storyDir = join26(epicDir, s);
13377
+ if (!statSync14(storyDir).isDirectory())
13378
+ continue;
13379
+ if (STORY_ID_RE.test(s))
13380
+ epics.get(d).add(s);
13381
+ }
13382
+ }
13383
+ } catch {
13384
+ }
13385
+ const rows = [];
13386
+ for (const epic of [...epics.keys()].sort()) {
13387
+ const unique = [...epics.get(epic)].sort();
13388
+ let delivered = 0;
13389
+ for (const s of unique) {
13390
+ const latest = join26(featuresDir, epic, s, "latest");
13391
+ try {
13392
+ if (statSync14(latest).isDirectory())
13393
+ delivered++;
13394
+ } catch {
13395
+ }
13396
+ }
13397
+ const links = unique.slice(0, 20).map((s) => `<a href="${epic}/${s}/index.html">${s}</a>`).join(", ");
13398
+ const more = unique.length > 20 ? ` ${bi(`\u2026 +${unique.length - 20} more`, `\u2026 \u53E6 ${unique.length - 20} \u4E2A`)}` : "";
13399
+ rows.push(`<tr>
13400
+ <td><strong><a href="${epic}/">${epic}</a></strong></td>
13401
+ <td>${bi(`${unique.length} stories (${delivered} delivered)`, `${unique.length} \u4E2A\u6545\u4E8B\uFF08\u5DF2\u4EA4\u4ED8 ${delivered}\uFF09`)}</td>
13402
+ <td style="font-size:0.85em">${links}${more}</td>
13403
+ </tr>`);
13404
+ }
13405
+ const html = `<!DOCTYPE html>
13406
+ <html lang="zh-CN">
13407
+ <head>
13408
+ <meta charset="UTF-8">
13409
+ <meta name="viewport" content="width=device-width, initial-scale=1">
13410
+ <title>Roll Features Index \xB7 \u529F\u80FD\u6863\u6848</title>
13411
+ <style>
13412
+ ${CHROME_CSS}body { max-width:1000px; }
13413
+ table { width:100%; border-collapse:collapse; position:relative; z-index:2; }
13414
+ th, td { padding:8px 12px; text-align:left; border-bottom:1px solid var(--line); vertical-align:top; }
13415
+ th { font-family:var(--serif); letter-spacing:.04em; color:var(--muted); }
13416
+ tr:hover td { background:var(--bg-raise); }
13417
+ td code, td strong a { font-family:var(--mono); }
13418
+ </style>
13419
+ ${CHROME_SCRIPT}
13420
+ </head>
13421
+ <body>
13422
+ ${CHROME_CONTROLS}
13423
+ <p class="kicker">Roll \xB7 ${bi("Delivery Dossier", "\u4EA4\u4ED8\u6863\u6848")}</p>
13424
+ <h1>${bi("Features Index", "\u529F\u80FD\u6863\u6848")}</h1>
13425
+ <p class="meta">${bi(`${epics.size} epics \xB7 ${n} stories`, `${epics.size} \u4E2A\u53F2\u8BD7 \xB7 ${n} \u4E2A\u6545\u4E8B`)}</p>
13426
+ <table><thead><tr><th>Epic</th><th>${bi("Stories", "\u6545\u4E8B")}</th><th>${bi("Recent", "\u6700\u8FD1")}</th></tr></thead>
13427
+ <tbody>
13428
+ ${rows.join("\n")}
13429
+ </tbody></table>
13430
+ <footer>${bi("Generated by", "\u751F\u6210\u81EA")} <code>roll index</code></footer>
13431
+ </body>
13432
+ </html>
13433
+ `;
13434
+ try {
13435
+ writeFileSync14(join26(featuresDir, "index.html"), html, "utf8");
13436
+ process.stdout.write(`features/index.html regenerated
13437
+ `);
13438
+ } catch {
13439
+ }
13440
+ }
13007
13441
  return 0;
13008
13442
  }
13009
13443
 
13010
13444
  // packages/cli/dist/commands/init.js
13011
13445
  import { spawnSync as spawnSync5 } from "node:child_process";
13012
- import { copyFileSync as copyFileSync2, existsSync as existsSync29, mkdirSync as mkdirSync13, readFileSync as readFileSync23, realpathSync as realpathSync4, statSync as statSync14, writeFileSync as writeFileSync13 } from "node:fs";
13013
- import { homedir as homedir11 } from "node:os";
13014
- import { dirname as dirname12, join as join25 } from "node:path";
13446
+ import { copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync14, readFileSync as readFileSync23, realpathSync as realpathSync4, statSync as statSync15, writeFileSync as writeFileSync15 } from "node:fs";
13447
+ import { homedir as homedir12 } from "node:os";
13448
+ import { dirname as dirname11, join as join27 } from "node:path";
13015
13449
  function err9(line) {
13016
13450
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
13017
13451
  const RED = noColor2 ? "" : "\x1B[0;31m";
@@ -13020,19 +13454,19 @@ function err9(line) {
13020
13454
  `);
13021
13455
  }
13022
13456
  function rollHome2() {
13023
- return process.env["ROLL_HOME"] ?? join25(homedir11(), ".roll");
13457
+ return process.env["ROLL_HOME"] ?? join27(homedir12(), ".roll");
13024
13458
  }
13025
13459
  function rollGlobal2() {
13026
- return join25(rollHome2(), "conventions", "global");
13460
+ return join27(rollHome2(), "conventions", "global");
13027
13461
  }
13028
13462
  function rollTemplates2() {
13029
- return join25(rollHome2(), "conventions", "templates");
13463
+ return join27(rollHome2(), "conventions", "templates");
13030
13464
  }
13031
13465
  function scanProjectType(dir) {
13032
13466
  let hasFrontend = false;
13033
13467
  let hasBackend = false;
13034
13468
  let hasCli = false;
13035
- const pkg = join25(dir, "package.json");
13469
+ const pkg = join27(dir, "package.json");
13036
13470
  const readPkg = () => {
13037
13471
  try {
13038
13472
  return readFileSync23(pkg, "utf8");
@@ -13040,23 +13474,23 @@ function scanProjectType(dir) {
13040
13474
  return "";
13041
13475
  }
13042
13476
  };
13043
- if (existsSync29(pkg)) {
13477
+ if (existsSync30(pkg)) {
13044
13478
  if (/"react"|"vue"|"next"|"nuxt"|"vite"|"svelte"/i.test(readPkg()))
13045
13479
  hasFrontend = true;
13046
13480
  }
13047
- if (["src", "app", "pages", "components"].some((d) => existsSync29(join25(dir, d))))
13481
+ if (["src", "app", "pages", "components"].some((d) => existsSync30(join27(dir, d))))
13048
13482
  hasFrontend = true;
13049
- if (["server", "api", "backend"].some((d) => existsSync29(join25(dir, d))))
13483
+ if (["server", "api", "backend"].some((d) => existsSync30(join27(dir, d))))
13050
13484
  hasBackend = true;
13051
- if (["go.mod", "main.go", "main.py", "app.py", "Cargo.toml", "requirements.txt", "pyproject.toml"].some((f) => existsSync29(join25(dir, f))))
13485
+ if (["go.mod", "main.go", "main.py", "app.py", "Cargo.toml", "requirements.txt", "pyproject.toml"].some((f) => existsSync30(join27(dir, f))))
13052
13486
  hasBackend = true;
13053
- if (existsSync29(pkg)) {
13487
+ if (existsSync30(pkg)) {
13054
13488
  if (/"prisma"|"@prisma\/client"|"typeorm"|"sequelize"|"mongoose"|"drizzle-orm"|"@neondatabase\/serverless"|"pg"|"mysql2"|"mongodb"|"redis"|"ioredis"|"express"|"fastify"|"koa"|"hapi"|"@hapi\/hapi"|"apollo-server"|"graphql-yoga"|"trpc"/i.test(readPkg()))
13055
13489
  hasBackend = true;
13056
13490
  }
13057
- if (existsSync29(join25(dir, "prisma", "schema.prisma")))
13491
+ if (existsSync30(join27(dir, "prisma", "schema.prisma")))
13058
13492
  hasBackend = true;
13059
- if (existsSync29(join25(dir, "bin")) || existsSync29(join25(dir, "cmd")))
13493
+ if (existsSync30(join27(dir, "bin")) || existsSync30(join27(dir, "cmd")))
13060
13494
  hasCli = true;
13061
13495
  if (hasFrontend && hasBackend)
13062
13496
  return "fullstack";
@@ -13076,8 +13510,8 @@ function countNonEmptyFiles(dir) {
13076
13510
  }
13077
13511
  function isLegacyProject(projectDir) {
13078
13512
  for (const dir of ["src", "app", "lib", "pkg", "cmd"]) {
13079
- const p = join25(projectDir, dir);
13080
- if (existsSync29(p) && statSync14(p).isDirectory()) {
13513
+ const p = join27(projectDir, dir);
13514
+ if (existsSync30(p) && statSync15(p).isDirectory()) {
13081
13515
  if (countNonEmptyFiles(p) >= 10)
13082
13516
  return true;
13083
13517
  }
@@ -13107,12 +13541,12 @@ function isLegacyProject(projectDir) {
13107
13541
  "deno.jsonc"
13108
13542
  ];
13109
13543
  for (const man of manifests)
13110
- if (existsSync29(join25(projectDir, man)))
13544
+ if (existsSync30(join27(projectDir, man)))
13111
13545
  return true;
13112
13546
  const tf = spawnSync5("bash", ["-c", `compgen -G '${projectDir.replace(/'/g, "'\\''")}/*.tf' >/dev/null 2>&1`]);
13113
13547
  if (tf.status === 0)
13114
13548
  return true;
13115
- if (existsSync29(join25(projectDir, ".git"))) {
13549
+ if (existsSync30(join27(projectDir, ".git"))) {
13116
13550
  const g = spawnSync5("git", ["rev-parse", "--verify", "HEAD"], { cwd: projectDir, stdio: "ignore" });
13117
13551
  if (g.status === 0)
13118
13552
  return true;
@@ -13120,9 +13554,9 @@ function isLegacyProject(projectDir) {
13120
13554
  return false;
13121
13555
  }
13122
13556
  function mergeGlobalToProject(projectDir, summary) {
13123
- const src = join25(rollGlobal2(), "AGENTS.md");
13124
- const dst = join25(projectDir, "AGENTS.md");
13125
- if (!existsSync29(src)) {
13557
+ const src = join27(rollGlobal2(), "AGENTS.md");
13558
+ const dst = join27(projectDir, "AGENTS.md");
13559
+ if (!existsSync30(src)) {
13126
13560
  return;
13127
13561
  }
13128
13562
  const projectType = scanProjectType(projectDir);
@@ -13131,7 +13565,7 @@ function mergeGlobalToProject(projectDir, summary) {
13131
13565
  const srcText = readFileSync23(src, "utf8");
13132
13566
  const srcLines = srcText.split("\n");
13133
13567
  const lines = srcText.endsWith("\n") ? srcLines.slice(0, -1) : srcLines;
13134
- if (!existsSync29(dst)) {
13568
+ if (!existsSync30(dst)) {
13135
13569
  let out2 = "";
13136
13570
  let fcH = "";
13137
13571
  let fcB = "";
@@ -13160,7 +13594,7 @@ ${fcB}`;
13160
13594
  }
13161
13595
  }
13162
13596
  flush();
13163
- writeFileSync13(dst, out2);
13597
+ writeFileSync15(dst, out2);
13164
13598
  summary.push("created|AGENTS.md");
13165
13599
  return;
13166
13600
  }
@@ -13194,7 +13628,7 @@ ${curB}`;
13194
13628
  }
13195
13629
  tryAppend();
13196
13630
  if (appendBuffer !== "")
13197
- writeFileSync13(dst, dstText + appendBuffer);
13631
+ writeFileSync15(dst, dstText + appendBuffer);
13198
13632
  if (added > 0)
13199
13633
  summary.push("merged|AGENTS.md");
13200
13634
  else
@@ -13202,13 +13636,13 @@ ${curB}`;
13202
13636
  }
13203
13637
  function mergeClaudeToProject(projectDir, summary) {
13204
13638
  const projectType = scanProjectType(projectDir);
13205
- const tplFile = join25(rollTemplates2(), projectType, "CLAUDE.md");
13206
- if (!existsSync29(tplFile))
13639
+ const tplFile = join27(rollTemplates2(), projectType, "CLAUDE.md");
13640
+ if (!existsSync30(tplFile))
13207
13641
  return;
13208
- const claudeDir = join25(projectDir, ".claude");
13209
- const outFile = join25(claudeDir, "CLAUDE.md");
13210
- mkdirSync13(claudeDir, { recursive: true });
13211
- if (!existsSync29(outFile)) {
13642
+ const claudeDir = join27(projectDir, ".claude");
13643
+ const outFile = join27(claudeDir, "CLAUDE.md");
13644
+ mkdirSync14(claudeDir, { recursive: true });
13645
+ if (!existsSync30(outFile)) {
13212
13646
  copyFileSync2(tplFile, outFile);
13213
13647
  summary.push("created|.claude/CLAUDE.md");
13214
13648
  return;
@@ -13240,7 +13674,7 @@ ${curB}`;
13240
13674
  }
13241
13675
  tryAppend();
13242
13676
  if (appendBuffer !== "")
13243
- writeFileSync13(outFile, outText + appendBuffer);
13677
+ writeFileSync15(outFile, outText + appendBuffer);
13244
13678
  if (added > 0)
13245
13679
  summary.push("merged|.claude/CLAUDE.md");
13246
13680
  else
@@ -13257,20 +13691,20 @@ var BACKLOG_TEMPLATE = `# Project Backlog
13257
13691
  |----|---------|--------|
13258
13692
  `;
13259
13693
  function writeBacklog(path, summary) {
13260
- if (existsSync29(path)) {
13694
+ if (existsSync30(path)) {
13261
13695
  summary.push("unchanged|.roll/backlog.md");
13262
13696
  return;
13263
13697
  }
13264
- mkdirSync13(dirname12(path), { recursive: true });
13265
- writeFileSync13(path, BACKLOG_TEMPLATE);
13698
+ mkdirSync14(dirname11(path), { recursive: true });
13699
+ writeFileSync15(path, BACKLOG_TEMPLATE);
13266
13700
  summary.push("created|.roll/backlog.md");
13267
13701
  }
13268
13702
  function ensureFeaturesDir(path, summary) {
13269
- if (existsSync29(path) && statSync14(path).isDirectory()) {
13703
+ if (existsSync30(path) && statSync15(path).isDirectory()) {
13270
13704
  summary.push("unchanged|.roll/features/");
13271
13705
  return;
13272
13706
  }
13273
- mkdirSync13(path, { recursive: true });
13707
+ mkdirSync14(path, { recursive: true });
13274
13708
  summary.push("created|.roll/features/");
13275
13709
  }
13276
13710
  var FEATURES_TEMPLATE = `# Features
@@ -13284,38 +13718,38 @@ var FEATURES_TEMPLATE = `# Features
13284
13718
  <!-- Add feature entries here as epics are completed -->
13285
13719
  `;
13286
13720
  function writeFeaturesMd(path, summary) {
13287
- if (existsSync29(path)) {
13721
+ if (existsSync30(path)) {
13288
13722
  summary.push("unchanged|.roll/features.md");
13289
13723
  return;
13290
13724
  }
13291
- mkdirSync13(dirname12(path), { recursive: true });
13292
- writeFileSync13(path, FEATURES_TEMPLATE);
13725
+ mkdirSync14(dirname11(path), { recursive: true });
13726
+ writeFileSync15(path, FEATURES_TEMPLATE);
13293
13727
  summary.push("created|.roll/features.md");
13294
13728
  }
13295
13729
  function initSeedAgentRoutes(templateName, projectDir, summary) {
13296
- const dest = join25(projectDir, ".roll", "agent-routes.yaml");
13297
- if (existsSync29(dest)) {
13730
+ const dest = join27(projectDir, ".roll", "agent-routes.yaml");
13731
+ if (existsSync30(dest)) {
13298
13732
  summary.push("unchanged|.roll/agent-routes.yaml");
13299
13733
  return 0;
13300
13734
  }
13301
- const src = join25(rollTemplates2(), "agent-routes", `${templateName}.yaml`);
13302
- if (!existsSync29(src)) {
13735
+ const src = join27(rollTemplates2(), "agent-routes", `${templateName}.yaml`);
13736
+ if (!existsSync30(src)) {
13303
13737
  return 1;
13304
13738
  }
13305
- mkdirSync13(dirname12(dest), { recursive: true });
13739
+ mkdirSync14(dirname11(dest), { recursive: true });
13306
13740
  copyFileSync2(src, dest);
13307
13741
  summary.push("created|.roll/agent-routes.yaml");
13308
13742
  return 0;
13309
13743
  }
13310
13744
  function writeVersionStamp(projectDir, summary) {
13311
- const stampPath = join25(projectDir, ".roll", ".version");
13312
- if (existsSync29(stampPath)) {
13745
+ const stampPath = join27(projectDir, ".roll", ".version");
13746
+ if (existsSync30(stampPath)) {
13313
13747
  summary.push("unchanged|.roll/.version");
13314
13748
  return;
13315
13749
  }
13316
- mkdirSync13(join25(projectDir, ".roll"), { recursive: true });
13750
+ mkdirSync14(join27(projectDir, ".roll"), { recursive: true });
13317
13751
  const installedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
13318
- writeFileSync13(stampPath, `# Roll project version stamp \u2014 written by \`roll init\` (US-ONBOARD-019).
13752
+ writeFileSync15(stampPath, `# Roll project version stamp \u2014 written by \`roll init\` (US-ONBOARD-019).
13319
13753
  # Used by \`_check_structure\` to recognise a previously-onboarded Roll project
13320
13754
  # without depending on directory-name heuristics.
13321
13755
  roll_version: "${rollVersion() || "unknown"}"
@@ -13453,7 +13887,7 @@ function initCommand(args) {
13453
13887
  return null;
13454
13888
  if (args[0] !== void 0 && args[0].startsWith("-"))
13455
13889
  return null;
13456
- if (!existsSync29(rollTemplates2()))
13890
+ if (!existsSync30(rollTemplates2()))
13457
13891
  return null;
13458
13892
  let projectDir;
13459
13893
  try {
@@ -13463,16 +13897,16 @@ function initCommand(args) {
13463
13897
  }
13464
13898
  let hasAgents = false;
13465
13899
  const summary = [];
13466
- if (existsSync29(join25(projectDir, "AGENTS.md"))) {
13900
+ if (existsSync30(join27(projectDir, "AGENTS.md"))) {
13467
13901
  hasAgents = true;
13468
13902
  } else if (isLegacyProject(projectDir)) {
13469
13903
  return null;
13470
13904
  }
13471
13905
  mergeGlobalToProject(projectDir, summary);
13472
13906
  mergeClaudeToProject(projectDir, summary);
13473
- writeBacklog(join25(projectDir, ".roll", "backlog.md"), summary);
13474
- ensureFeaturesDir(join25(projectDir, ".roll", "features"), summary);
13475
- writeFeaturesMd(join25(projectDir, ".roll", "features.md"), summary);
13907
+ writeBacklog(join27(projectDir, ".roll", "backlog.md"), summary);
13908
+ ensureFeaturesDir(join27(projectDir, ".roll", "features"), summary);
13909
+ writeFeaturesMd(join27(projectDir, ".roll", "features.md"), summary);
13476
13910
  const routesTemplate = process.env["ROLL_AGENT_ROUTES_TEMPLATE"] ?? "default";
13477
13911
  initSeedAgentRoutes(routesTemplate, projectDir, summary);
13478
13912
  writeVersionStamp(projectDir, summary);
@@ -13486,16 +13920,16 @@ function initCommand(args) {
13486
13920
 
13487
13921
  // packages/cli/dist/commands/lang.js
13488
13922
  import { execFileSync as execFileSync7 } from "node:child_process";
13489
- import { existsSync as existsSync30, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync3, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync14 } from "node:fs";
13490
- import { homedir as homedir12, tmpdir as tmpdir3 } from "node:os";
13491
- import { dirname as dirname13, join as join26 } from "node:path";
13923
+ import { existsSync as existsSync31, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync3, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync16 } from "node:fs";
13924
+ import { homedir as homedir13, tmpdir as tmpdir3 } from "node:os";
13925
+ import { dirname as dirname12, join as join28 } from "node:path";
13492
13926
  function rollConfigPath4() {
13493
- const rollHome4 = process.env["ROLL_HOME"] ?? join26(homedir12(), ".roll");
13494
- return join26(rollHome4, "config.yaml");
13927
+ const rollHome4 = process.env["ROLL_HOME"] ?? join28(homedir13(), ".roll");
13928
+ return join28(rollHome4, "config.yaml");
13495
13929
  }
13496
13930
  function configLang2() {
13497
13931
  const cfg = rollConfigPath4();
13498
- if (!existsSync30(cfg))
13932
+ if (!existsSync31(cfg))
13499
13933
  return void 0;
13500
13934
  for (const line of readFileSync24(cfg, "utf8").split("\n")) {
13501
13935
  const m7 = /^lang:\s*(.*)$/.exec(line);
@@ -13509,7 +13943,7 @@ function configLang2() {
13509
13943
  }
13510
13944
  function configHasLangLine() {
13511
13945
  const cfg = rollConfigPath4();
13512
- if (!existsSync30(cfg))
13946
+ if (!existsSync31(cfg))
13513
13947
  return false;
13514
13948
  return readFileSync24(cfg, "utf8").split("\n").some((l) => /^lang:/.test(l));
13515
13949
  }
@@ -13545,7 +13979,7 @@ function resolveSource() {
13545
13979
  const env = process.env;
13546
13980
  if ((env["ROLL_LANG"] ?? "") !== "")
13547
13981
  return "ROLL_LANG env";
13548
- if (existsSync30(rollConfigPath4()) && configHasLangLine())
13982
+ if (existsSync31(rollConfigPath4()) && configHasLangLine())
13549
13983
  return `config (${rollConfigPath4()})`;
13550
13984
  if ((env["LC_ALL"] ?? "") !== "" || (env["LANG"] ?? "") !== "")
13551
13985
  return "LC_ALL/LANG";
@@ -13570,28 +14004,28 @@ function err10(line) {
13570
14004
  }
13571
14005
  function writeLang(value) {
13572
14006
  const cfg = rollConfigPath4();
13573
- mkdirSync14(dirname13(cfg), { recursive: true });
13574
- const existing = existsSync30(cfg) ? readFileSync24(cfg, "utf8") : "";
14007
+ mkdirSync15(dirname12(cfg), { recursive: true });
14008
+ const existing = existsSync31(cfg) ? readFileSync24(cfg, "utf8") : "";
13575
14009
  const kept = existing === "" ? [] : existing.split("\n").filter((l) => !/^lang:/.test(l));
13576
14010
  if (kept.length > 0 && kept[kept.length - 1] === "")
13577
14011
  kept.pop();
13578
14012
  const body = kept.length > 0 ? kept.join("\n") + "\n" : "";
13579
- const tmp = join26(mkdtempSync3(join26(tmpdir3(), "roll-lang-")), "config.yaml");
13580
- writeFileSync14(tmp, `${body}lang: ${value}
14013
+ const tmp = join28(mkdtempSync3(join28(tmpdir3(), "roll-lang-")), "config.yaml");
14014
+ writeFileSync16(tmp, `${body}lang: ${value}
13581
14015
  `);
13582
14016
  renameSync2(tmp, cfg);
13583
14017
  }
13584
14018
  function clearLang() {
13585
14019
  const cfg = rollConfigPath4();
13586
- if (!existsSync30(cfg))
14020
+ if (!existsSync31(cfg))
13587
14021
  return;
13588
14022
  const existing = readFileSync24(cfg, "utf8");
13589
14023
  const kept = existing.split("\n").filter((l) => !/^lang:/.test(l));
13590
14024
  if (kept.length > 0 && kept[kept.length - 1] === "")
13591
14025
  kept.pop();
13592
14026
  const body = kept.length > 0 ? kept.join("\n") + "\n" : "";
13593
- const tmp = join26(mkdtempSync3(join26(tmpdir3(), "roll-lang-")), "config.yaml");
13594
- writeFileSync14(tmp, body);
14027
+ const tmp = join28(mkdtempSync3(join28(tmpdir3(), "roll-lang-")), "config.yaml");
14028
+ writeFileSync16(tmp, body);
13595
14029
  renameSync2(tmp, cfg);
13596
14030
  }
13597
14031
  function langCommand(args) {
@@ -13691,9 +14125,9 @@ async function loopFmtCommand(args) {
13691
14125
 
13692
14126
  // packages/cli/dist/commands/loop-pr-inbox.js
13693
14127
  import { spawn as spawn2 } from "node:child_process";
13694
- import { appendFileSync as appendFileSync5, existsSync as existsSync31, mkdirSync as mkdirSync15, readFileSync as readFileSync25, writeFileSync as writeFileSync15 } from "node:fs";
13695
- import { homedir as homedir13 } from "node:os";
13696
- import { dirname as dirname14, join as join27 } from "node:path";
14128
+ import { appendFileSync as appendFileSync5, existsSync as existsSync32, mkdirSync as mkdirSync16, readFileSync as readFileSync25, writeFileSync as writeFileSync17 } from "node:fs";
14129
+ import { homedir as homedir14 } from "node:os";
14130
+ import { dirname as dirname13, join as join29 } from "node:path";
13697
14131
  function reducePrView(raw) {
13698
14132
  const reviews = raw.reviews ?? [];
13699
14133
  const botReviews = reviews.filter((r) => r.authorAssociation === "BOT" || r.authorAssociation === "APP");
@@ -13776,7 +14210,7 @@ function runtimeDir() {
13776
14210
  const override = (process.env["ROLL_PROJECT_RUNTIME_DIR"] ?? "").trim();
13777
14211
  if (override !== "")
13778
14212
  return override;
13779
- return join27(process.cwd(), ".roll", "loop");
14213
+ return join29(process.cwd(), ".roll", "loop");
13780
14214
  }
13781
14215
  function projSlug() {
13782
14216
  const override = (process.env["ROLL_MAIN_SLUG"] ?? "").trim();
@@ -13785,26 +14219,26 @@ function projSlug() {
13785
14219
  return process.cwd().split("/").filter(Boolean).pop() ?? "default";
13786
14220
  }
13787
14221
  function alertPath() {
13788
- return join27(runtimeDir(), `ALERT-${projSlug()}.md`);
14222
+ return join29(runtimeDir(), `ALERT-${projSlug()}.md`);
13789
14223
  }
13790
14224
  function statePath() {
13791
- return join27(runtimeDir(), `state-${projSlug()}.yaml`);
14225
+ return join29(runtimeDir(), `state-${projSlug()}.yaml`);
13792
14226
  }
13793
14227
  function tickPath() {
13794
- return join27(runtimeDir(), "pr-tick.jsonl");
14228
+ return join29(runtimeDir(), "pr-tick.jsonl");
13795
14229
  }
13796
14230
  function engineBin() {
13797
- let dir = dirname14(new URL(import.meta.url).pathname);
14231
+ let dir = dirname13(new URL(import.meta.url).pathname);
13798
14232
  for (let i = 0; i < 10; i++) {
13799
- const candidate = join27(dir, "bin", "roll");
13800
- if (existsSync31(candidate))
14233
+ const candidate = join29(dir, "bin", "roll");
14234
+ if (existsSync32(candidate))
13801
14235
  return candidate;
13802
- const parent = dirname14(dir);
14236
+ const parent = dirname13(dir);
13803
14237
  if (parent === dir)
13804
14238
  break;
13805
14239
  dir = parent;
13806
14240
  }
13807
- return join27(homedir13(), ".local", "lib", "roll", "bin", "roll");
14241
+ return join29(homedir14(), ".local", "lib", "roll", "bin", "roll");
13808
14242
  }
13809
14243
  function pal2() {
13810
14244
  return (process.env["NO_COLOR"] ?? "") !== "" ? { yellow: "", nc: "" } : { yellow: "\x1B[0;33m", nc: "\x1B[0m" };
@@ -13814,21 +14248,21 @@ function nowSec() {
13814
14248
  }
13815
14249
  function writeTickFile(tick) {
13816
14250
  const file = tickPath();
13817
- mkdirSync15(dirname14(file), { recursive: true });
14251
+ mkdirSync16(dirname13(file), { recursive: true });
13818
14252
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
13819
14253
  appendFileSync5(file, `${JSON.stringify({ ts, ...tick })}
13820
14254
  `);
13821
14255
  try {
13822
14256
  const lines = readFileSync25(file, "utf8").split("\n").filter((l) => l !== "");
13823
14257
  if (lines.length > 500)
13824
- writeFileSync15(file, `${lines.slice(-500).join("\n")}
14258
+ writeFileSync17(file, `${lines.slice(-500).join("\n")}
13825
14259
  `);
13826
14260
  } catch {
13827
14261
  }
13828
14262
  }
13829
14263
  function appendAlert(line) {
13830
14264
  const file = alertPath();
13831
- mkdirSync15(dirname14(file), { recursive: true });
14265
+ mkdirSync16(dirname13(file), { recursive: true });
13832
14266
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
13833
14267
  appendFileSync5(file, `[${ts}] ${line}
13834
14268
  `);
@@ -13844,9 +14278,9 @@ function rebaseCircuitAllowed(num2) {
13844
14278
  writeRebaseAttempts(state, num2, verdict.freshTimestamps);
13845
14279
  if (!verdict.allowed) {
13846
14280
  const file = alertPath();
13847
- mkdirSync15(dirname14(file), { recursive: true });
14281
+ mkdirSync16(dirname13(file), { recursive: true });
13848
14282
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
13849
- writeFileSync15(file, [
14283
+ writeFileSync17(file, [
13850
14284
  `# ALERT \u2014 PR rebase circuit breaker tripped`,
13851
14285
  ``,
13852
14286
  `**Time**: ${stamp}`,
@@ -13900,13 +14334,13 @@ function upsertRebaseAttempts(stateBody, pr, value) {
13900
14334
  `;
13901
14335
  }
13902
14336
  function writeRebaseAttempts(state, pr, timestamps) {
13903
- mkdirSync15(dirname14(state), { recursive: true });
14337
+ mkdirSync16(dirname13(state), { recursive: true });
13904
14338
  let body = "";
13905
14339
  try {
13906
14340
  body = readFileSync25(state, "utf8");
13907
14341
  } catch {
13908
14342
  }
13909
- writeFileSync15(state, upsertRebaseAttempts(body, pr, renderRebaseAttempts(timestamps)));
14343
+ writeFileSync17(state, upsertRebaseAttempts(body, pr, renderRebaseAttempts(timestamps)));
13910
14344
  }
13911
14345
  function bridgeBash(args) {
13912
14346
  return new Promise((resolve3) => {
@@ -13993,19 +14427,19 @@ async function loopPrInboxCommand(_args, deps = realDeps2()) {
13993
14427
  }
13994
14428
 
13995
14429
  // packages/cli/dist/commands/loop-run-once.js
13996
- import { appendFileSync as appendFileSync7, existsSync as existsSync35, mkdirSync as mkdirSync17, readFileSync as readFileSync28 } from "node:fs";
13997
- import { dirname as dirname16, join as join31 } from "node:path";
14430
+ import { appendFileSync as appendFileSync7, existsSync as existsSync36, mkdirSync as mkdirSync18, readFileSync as readFileSync28, writeFileSync as writeFileSync19 } from "node:fs";
14431
+ import { dirname as dirname15, join as join33 } from "node:path";
13998
14432
 
13999
14433
  // packages/cli/dist/runner/executor.js
14000
14434
  import { execFile as execFile8 } from "node:child_process";
14001
- import { appendFileSync as appendFileSync6, existsSync as existsSync34, lstatSync as lstatSync2, mkdirSync as mkdirSync16, readFileSync as readFileSync27, rmSync as rmSync8, symlinkSync as symlinkSync4, unlinkSync, writeFileSync as writeFileSync16 } from "node:fs";
14002
- import { dirname as dirname15, join as join30 } from "node:path";
14435
+ import { appendFileSync as appendFileSync6, existsSync as existsSync35, lstatSync as lstatSync2, mkdirSync as mkdirSync17, readFileSync as readFileSync27, rmSync as rmSync8, symlinkSync as symlinkSync4, unlinkSync, writeFileSync as writeFileSync18 } from "node:fs";
14436
+ import { dirname as dirname14, join as join32 } from "node:path";
14003
14437
  import { promisify as promisify8 } from "node:util";
14004
14438
 
14005
14439
  // packages/cli/dist/runner/peer-gate.js
14006
14440
  import { execFile as execFile7 } from "node:child_process";
14007
- import { existsSync as existsSync32, readdirSync as readdirSync14 } from "node:fs";
14008
- import { join as join28 } from "node:path";
14441
+ import { existsSync as existsSync33, readdirSync as readdirSync15 } from "node:fs";
14442
+ import { join as join30 } from "node:path";
14009
14443
  import { promisify as promisify7 } from "node:util";
14010
14444
  var execFileAsync7 = promisify7(execFile7);
14011
14445
  var HIGH_RISK = [/^\.github\/workflows\//, /^packages\/infra\/src\/(git|github|process)\.ts$/];
@@ -14035,11 +14469,11 @@ async function cycleChangedFiles(worktreeCwd) {
14035
14469
  }
14036
14470
  }
14037
14471
  function peerEvidencePresent(runtimeDir3, cycleId) {
14038
- const dir = join28(runtimeDir3, "peer");
14039
- if (!existsSync32(dir))
14472
+ const dir = join30(runtimeDir3, "peer");
14473
+ if (!existsSync33(dir))
14040
14474
  return false;
14041
14475
  try {
14042
- return readdirSync14(dir).some((f) => f.startsWith(`cycle-${cycleId}.`));
14476
+ return readdirSync15(dir).some((f) => f.startsWith(`cycle-${cycleId}.`));
14043
14477
  } catch {
14044
14478
  return false;
14045
14479
  }
@@ -14063,24 +14497,24 @@ async function runPeerGate(worktreeCwd, runtimeDir3, cycleId, sinks) {
14063
14497
  }
14064
14498
 
14065
14499
  // packages/cli/dist/runner/attest-gate.js
14066
- import { existsSync as existsSync33, readFileSync as readFileSync26, statSync as statSync15 } from "node:fs";
14067
- import { join as join29 } from "node:path";
14500
+ import { existsSync as existsSync34, readFileSync as readFileSync26, statSync as statSync16 } from "node:fs";
14501
+ import { join as join31 } from "node:path";
14068
14502
  function reportCandidates(worktreeCwd, storyId) {
14069
14503
  return [
14070
- join29(cardArchiveDir(worktreeCwd, storyId), "latest", reportFileName(storyId)),
14071
- join29(legacyArchiveDir(worktreeCwd, storyId), "latest", "report.html")
14504
+ join31(cardArchiveDir(worktreeCwd, storyId), "latest", reportFileName(storyId)),
14505
+ join31(legacyArchiveDir(worktreeCwd, storyId), "latest", "report.html")
14072
14506
  ];
14073
14507
  }
14074
14508
  function acMapCandidates(worktreeCwd, storyId) {
14075
14509
  return [
14076
- join29(cardArchiveDir(worktreeCwd, storyId), "ac-map.json"),
14077
- join29(legacyArchiveDir(worktreeCwd, storyId), "ac-map.json")
14510
+ join31(cardArchiveDir(worktreeCwd, storyId), "ac-map.json"),
14511
+ join31(legacyArchiveDir(worktreeCwd, storyId), "ac-map.json")
14078
14512
  ];
14079
14513
  }
14080
14514
  function existingReport(worktreeCwd, storyId) {
14081
14515
  for (const p of reportCandidates(worktreeCwd, storyId)) {
14082
14516
  try {
14083
- if (statSync15(p).isFile())
14517
+ if (statSync16(p).isFile())
14084
14518
  return p;
14085
14519
  } catch {
14086
14520
  }
@@ -14094,7 +14528,7 @@ function verificationReportFresh(worktreeCwd, storyId, sinceSec) {
14094
14528
  if (p === null)
14095
14529
  return false;
14096
14530
  try {
14097
- const st = statSync15(p);
14531
+ const st = statSync16(p);
14098
14532
  if (sinceSec === void 0)
14099
14533
  return true;
14100
14534
  return st.mtimeMs / 1e3 >= sinceSec;
@@ -14111,7 +14545,7 @@ function verificationReportHasContent(worktreeCwd, storyId) {
14111
14545
  try {
14112
14546
  const html = readFileSync26(p, "utf8");
14113
14547
  const hasAc = /<section class="ac[\s">]/.test(html);
14114
- const hasMap = acMapCandidates(worktreeCwd, storyId).some((m7) => existsSync33(m7));
14548
+ const hasMap = acMapCandidates(worktreeCwd, storyId).some((m7) => existsSync34(m7));
14115
14549
  return hasAc && hasMap;
14116
14550
  } catch {
14117
14551
  return false;
@@ -14119,8 +14553,8 @@ function verificationReportHasContent(worktreeCwd, storyId) {
14119
14553
  }
14120
14554
  function readAttestGateMode(repoCwd) {
14121
14555
  try {
14122
- const p = join29(repoCwd, ".roll", "policy.yaml");
14123
- if (!existsSync33(p))
14556
+ const p = join31(repoCwd, ".roll", "policy.yaml");
14557
+ if (!existsSync34(p))
14124
14558
  return "soft";
14125
14559
  return parsePolicy(readFileSync26(p, "utf8")).loopSafety.attestGate === "hard" ? "hard" : "soft";
14126
14560
  } catch {
@@ -14136,7 +14570,7 @@ function runAttestGate(worktreeCwd, storyId, cycleId, mode, sinceSec, sinks) {
14136
14570
  return { verdict: "produced", mode, reasons: reasons2, blocked: false };
14137
14571
  }
14138
14572
  const reasons = [
14139
- fresh ? `acceptance report at .roll/verification/${storyId}/latest/report.html is an empty shell (no AC content / no ac-map)` : `no fresh acceptance report at .roll/verification/${storyId}/latest/report.html`
14573
+ fresh ? `acceptance report at .roll/features/<epic>/${storyId}/latest/${storyId}-report.html is an empty shell (no AC content / no ac-map)` : `no fresh acceptance report for ${storyId} (checked card archive + legacy verification paths)`
14140
14574
  ];
14141
14575
  const blocked = mode === "hard";
14142
14576
  const lead = fresh ? `delivery with an empty-shell acceptance report (no AC content / no ac-map)` : `delivery without a fresh acceptance report`;
@@ -14230,9 +14664,9 @@ async function executeCommand(cmd, ports, ctx) {
14230
14664
  // execute: spawn the agent (TCR commits happen inside the worktree). The
14231
14665
  // exit code + timeout feed back as agent_exited; usage is captured for cost.
14232
14666
  case "spawn_agent": {
14233
- const livePath = join30(dirname15(ports.paths.eventsPath), "live.log");
14667
+ const livePath = join32(dirname14(ports.paths.eventsPath), "live.log");
14234
14668
  try {
14235
- writeFileSync16(livePath, `\u2500\u2500 cycle ${ctx.cycleId ?? "?"} \xB7 ${ctx.storyId ?? "?"} \xB7 agent ${cmd.agent} \u2500\u2500
14669
+ writeFileSync18(livePath, `\u2500\u2500 cycle ${ctx.cycleId ?? "?"} \xB7 ${ctx.storyId ?? "?"} \xB7 agent ${cmd.agent} \u2500\u2500
14236
14670
  `);
14237
14671
  } catch {
14238
14672
  }
@@ -14250,9 +14684,9 @@ async function executeCommand(cmd, ports, ctx) {
14250
14684
  }
14251
14685
  });
14252
14686
  try {
14253
- const logDir = join30(dirname15(ports.paths.eventsPath), "cycle-logs");
14254
- mkdirSync16(logDir, { recursive: true });
14255
- writeFileSync16(join30(logDir, `${ctx.cycleId ?? "cycle"}.agent.log`), `# exit=${res.exitCode} timedOut=${res.timedOut}
14687
+ const logDir = join32(dirname14(ports.paths.eventsPath), "cycle-logs");
14688
+ mkdirSync17(logDir, { recursive: true });
14689
+ writeFileSync18(join32(logDir, `${ctx.cycleId ?? "cycle"}.agent.log`), `# exit=${res.exitCode} timedOut=${res.timedOut}
14256
14690
  --- stdout ---
14257
14691
  ${res.stdout}
14258
14692
  --- stderr ---
@@ -14299,7 +14733,7 @@ ${res.stderr}
14299
14733
  tcrCount = await ports.git.tcrCount(ports.paths.worktreePath);
14300
14734
  } catch {
14301
14735
  }
14302
- await runPeerGate(ports.paths.worktreePath, dirname15(ports.paths.eventsPath), ctx.cycleId ?? "", {
14736
+ await runPeerGate(ports.paths.worktreePath, dirname14(ports.paths.eventsPath), ctx.cycleId ?? "", {
14303
14737
  alert: (m7) => ports.events.appendAlert(ports.paths.alertsPath, m7),
14304
14738
  event: (p) => ports.events.appendEvent(ports.paths.eventsPath, {
14305
14739
  type: "peer:gate",
@@ -14375,7 +14809,7 @@ ${res.stderr}
14375
14809
  // _worktree_cleanup (tolerant). Side effect; no feedback (terminal path).
14376
14810
  case "cleanup_worktree":
14377
14811
  try {
14378
- const dst = join30(ports.paths.worktreePath, ".roll");
14812
+ const dst = join32(ports.paths.worktreePath, ".roll");
14379
14813
  if (lstatSync2(dst, { throwIfNoEntry: false })?.isSymbolicLink() === true)
14380
14814
  unlinkSync(dst);
14381
14815
  } catch {
@@ -14447,7 +14881,7 @@ function buildRunRow(cmd, ctx, nowSec2) {
14447
14881
  }
14448
14882
  function readRunsRows(runsPath) {
14449
14883
  try {
14450
- if (!existsSync34(runsPath))
14884
+ if (!existsSync35(runsPath))
14451
14885
  return [];
14452
14886
  return readFileSync27(runsPath, "utf8").split("\n").filter((l) => l.trim() !== "").map((l) => {
14453
14887
  try {
@@ -14472,15 +14906,15 @@ function sleep(ms) {
14472
14906
  }
14473
14907
  async function linkRollIntoWorktree(repoCwd, worktreePath) {
14474
14908
  try {
14475
- const src = join30(repoCwd, ".roll");
14476
- const dst = join30(worktreePath, ".roll");
14477
- if (!existsSync34(src))
14909
+ const src = join32(repoCwd, ".roll");
14910
+ const dst = join32(worktreePath, ".roll");
14911
+ if (!existsSync35(src))
14478
14912
  return;
14479
14913
  const dstStat = lstatSync2(dst, { throwIfNoEntry: false });
14480
14914
  if (dstStat) {
14481
14915
  if (dstStat.isSymbolicLink())
14482
14916
  return;
14483
- const incompleteFossil = existsSync34(join30(src, "backlog.md")) && !existsSync34(join30(dst, "backlog.md"));
14917
+ const incompleteFossil = existsSync35(join32(src, "backlog.md")) && !existsSync35(join32(dst, "backlog.md"));
14484
14918
  if (!incompleteFossil)
14485
14919
  return;
14486
14920
  rmSync8(dst, { recursive: true, force: true });
@@ -14489,10 +14923,10 @@ async function linkRollIntoWorktree(repoCwd, worktreePath) {
14489
14923
  const common = (await execFileAsync8("git", ["-C", repoCwd, "rev-parse", "--path-format=absolute", "--git-common-dir"])).stdout.trim();
14490
14924
  if (common === "")
14491
14925
  return;
14492
- const exclude = join30(common, "info", "exclude");
14493
- const cur = existsSync34(exclude) ? readFileSync27(exclude, "utf8") : "";
14926
+ const exclude = join32(common, "info", "exclude");
14927
+ const cur = existsSync35(exclude) ? readFileSync27(exclude, "utf8") : "";
14494
14928
  if (!/^\.roll$/m.test(cur)) {
14495
- mkdirSync16(dirname15(exclude), { recursive: true });
14929
+ mkdirSync17(dirname14(exclude), { recursive: true });
14496
14930
  appendFileSync6(exclude, `${cur === "" || cur.endsWith("\n") ? "" : "\n"}.roll
14497
14931
  `, "utf8");
14498
14932
  }
@@ -14581,8 +15015,8 @@ function nodePorts(opts) {
14581
15015
  },
14582
15016
  backlog: {
14583
15017
  read(projectCwd) {
14584
- const p = join30(projectCwd, ".roll", "backlog.md");
14585
- if (!existsSync34(p))
15018
+ const p = join32(projectCwd, ".roll", "backlog.md");
15019
+ if (!existsSync35(p))
14586
15020
  return [];
14587
15021
  return parseBacklog(readFileSync27(p, "utf8"));
14588
15022
  },
@@ -14592,8 +15026,8 @@ function nodePorts(opts) {
14592
15026
  // never kill the cycle, the reconcile pass is the safety net.
14593
15027
  markStatus(projectCwd, id, status2) {
14594
15028
  try {
14595
- const p = join30(projectCwd, ".roll", "backlog.md");
14596
- if (!existsSync34(p))
15029
+ const p = join32(projectCwd, ".roll", "backlog.md");
15030
+ if (!existsSync35(p))
14597
15031
  return;
14598
15032
  const store = new BacklogStore();
14599
15033
  const snap = store.readBacklog(p);
@@ -14613,7 +15047,7 @@ function nodePorts(opts) {
14613
15047
  };
14614
15048
  }
14615
15049
  function appendAlertLine(alertsPath, message) {
14616
- mkdirSync16(dirname15(alertsPath), { recursive: true });
15050
+ mkdirSync17(dirname14(alertsPath), { recursive: true });
14617
15051
  appendFileSync6(alertsPath, `${message}
14618
15052
  `, "utf8");
14619
15053
  }
@@ -14798,13 +15232,13 @@ function announceReport(projectPath, slug, storyId, opener = (p) => {
14798
15232
  }) {
14799
15233
  if (storyId === "")
14800
15234
  return null;
14801
- const report = join31(projectPath, ".roll", "verification", storyId, "latest", "report.html");
14802
- if (!existsSync35(report))
15235
+ const report = join33(projectPath, ".roll", "verification", storyId, "latest", "report.html");
15236
+ if (!existsSync36(report))
14803
15237
  return null;
14804
15238
  process.stdout.write(`evidence: ${report}
14805
15239
  \u9A8C\u6536\u62A5\u544A: ${report}
14806
15240
  `);
14807
- const muted = existsSync35(join31(projectPath, ".roll", "loop", `mute-${slug}`)) || existsSync35(join31(process.env["ROLL_SHARED_ROOT"] || join31(process.env["HOME"] ?? "", ".shared", "roll"), "loop", `mute-${slug}`));
15241
+ const muted = existsSync36(join33(projectPath, ".roll", "loop", `mute-${slug}`)) || existsSync36(join33(process.env["ROLL_SHARED_ROOT"] || join33(process.env["HOME"] ?? "", ".shared", "roll"), "loop", `mute-${slug}`));
14808
15242
  if (!muted)
14809
15243
  opener(report);
14810
15244
  return report;
@@ -14821,7 +15255,7 @@ function cycleSignalTeardown(paths, cycleId, branch, sig, deps = {}) {
14821
15255
  }
14822
15256
  let owned = false;
14823
15257
  try {
14824
- owned = existsSync35(paths.lockPath) && parseLock(readFileSync28(paths.lockPath, "utf8")).pid === pid;
15258
+ owned = existsSync36(paths.lockPath) && parseLock(readFileSync28(paths.lockPath, "utf8")).pid === pid;
14825
15259
  } catch {
14826
15260
  owned = false;
14827
15261
  }
@@ -14866,7 +15300,51 @@ function makeCycleId(now = /* @__PURE__ */ new Date(), pid = process.pid) {
14866
15300
  }
14867
15301
  function runtimeDir2(projectPath) {
14868
15302
  const env = (process.env["ROLL_PROJECT_RUNTIME_DIR"] ?? "").trim();
14869
- return env !== "" ? env : join31(projectPath, ".roll", "loop");
15303
+ return env !== "" ? env : join33(projectPath, ".roll", "loop");
15304
+ }
15305
+ var PAUSE_THRESHOLD = 3;
15306
+ function incrementConsecutiveFails(projectPath, slug, alertsPath, cycleId, storyId, terminal) {
15307
+ const rt = runtimeDir2(projectPath);
15308
+ const counterFile = join33(rt, "consecutive-fails");
15309
+ let count = 0;
15310
+ try {
15311
+ if (existsSync36(counterFile)) {
15312
+ count = parseInt(readFileSync28(counterFile, "utf8").trim(), 10) || 0;
15313
+ }
15314
+ } catch {
15315
+ }
15316
+ count += 1;
15317
+ try {
15318
+ writeFileSync19(counterFile, String(count), "utf8");
15319
+ } catch {
15320
+ }
15321
+ if (count < PAUSE_THRESHOLD)
15322
+ return;
15323
+ const pauseMarker = join33(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15324
+ const alertMsg = `# ALERT \u2014 loop auto-paused after ${count} consecutive failures
15325
+
15326
+ **Cycle**: ${cycleId}
15327
+ **Story**: ${storyId}
15328
+ **Terminal**: ${terminal}
15329
+ **Action**: ${count} consecutive cycles failed \u2014 loop paused to prevent burn.
15330
+ Resolve the root cause, then: \`roll loop resume\`
15331
+ `;
15332
+ try {
15333
+ writeFileSync19(pauseMarker, alertMsg, "utf8");
15334
+ appendFileSync7(alertsPath, `${alertMsg}
15335
+ `, "utf8");
15336
+ } catch {
15337
+ }
15338
+ process.stderr.write(`loop run-once: auto-PAUSED after ${count} consecutive failures \u2014 PAUSE marker written
15339
+ loop run-once: \u8FDE\u7EED ${count} \u6B21\u5931\u8D25\u540E\u81EA\u52A8\u6682\u505C \u2014 \u5DF2\u5199 PAUSE \u6807\u8BB0
15340
+ `);
15341
+ }
15342
+ function resetConsecutiveFails(projectPath) {
15343
+ const rt = runtimeDir2(projectPath);
15344
+ try {
15345
+ writeFileSync19(join33(rt, "consecutive-fails"), "0", "utf8");
15346
+ } catch {
15347
+ }
14870
15348
  }
14871
15349
  function readSkillBody2(projectPath) {
14872
15350
  return readSkillBody(projectPath, {
@@ -14874,6 +15352,38 @@ function readSkillBody2(projectPath) {
14874
15352
  envOverride: process.env["ROLL_LOOP_SKILL"]
14875
15353
  });
14876
15354
  }
15355
+ function buildLoopRouteDeps(projectPath) {
15356
+ function readSlot(slot) {
15357
+ const agentsYaml = join33(projectPath, ".roll", "agents.yaml");
15358
+ try {
15359
+ return readSlotFromText(readFileSync28(agentsYaml, "utf8"), slot);
15360
+ } catch {
15361
+ return void 0;
15362
+ }
15363
+ }
15364
+ function firstInstalled() {
15365
+ const fromLocal = readField(join33(projectPath, ".roll", "local.yaml"), /^agent:/);
15366
+ if (fromLocal !== void 0)
15367
+ return fromLocal;
15368
+ return firstInstalledAgent(realAgentEnv());
15369
+ }
15370
+ return { readSlot, firstInstalled };
15371
+ }
15372
+ function readField(path, re) {
15373
+ try {
15374
+ const text = readFileSync28(path, "utf8");
15375
+ for (const line of text.split("\n")) {
15376
+ const m7 = line.match(re);
15377
+ if (m7 !== null) {
15378
+ const v = line.slice((m7.index ?? 0) + m7[0].length).trim();
15379
+ if (v !== "")
15380
+ return v.replace(/^["']|["']$/g, "");
15381
+ }
15382
+ }
15383
+ } catch {
15384
+ }
15385
+ return void 0;
15386
+ }
14877
15387
  async function loopRunOnceCommand(args) {
14878
15388
  const dryRun = args.includes("--dry-run");
14879
15389
  const id = await projectIdentity();
@@ -14897,37 +15407,40 @@ async function loopRunOnceCommand(args) {
14897
15407
  return 0;
14898
15408
  }
14899
15409
  const rt = runtimeDir2(id.path);
15410
+ const alertsPath = join33(rt, `ALERT-${id.slug}.md`);
15411
+ mkdirSync18(dirname15(alertsPath), { recursive: true });
14900
15412
  const paths = {
14901
- eventsPath: join31(rt, "events.ndjson"),
14902
- runsPath: join31(rt, "runs.jsonl"),
14903
- alertsPath: join31(rt, "alerts.log"),
14904
- lockPath: join31(rt, "inner.lock"),
14905
- heartbeatPath: join31(rt, "heartbeat"),
14906
- worktreePath: join31(rt, "worktrees", `cycle-${cycleId}`)
15413
+ eventsPath: join33(rt, "events.ndjson"),
15414
+ runsPath: join33(rt, "runs.jsonl"),
15415
+ alertsPath,
15416
+ lockPath: join33(rt, "inner.lock"),
15417
+ heartbeatPath: join33(rt, "heartbeat"),
15418
+ worktreePath: join33(rt, "worktrees", `cycle-${cycleId}`)
14907
15419
  };
14908
15420
  const skillBody = readSkillBody2(id.path);
14909
15421
  if (skillBody === null) {
14910
15422
  const msg2 = `[${(/* @__PURE__ */ new Date()).toISOString()}] ALERT loop run-once: roll-loop SKILL.md not found (checked ROLL_LOOP_SKILL, .roll/skills/, skills/) \u2014 cycle ${cycleId} refused to start`;
14911
15423
  try {
14912
- mkdirSync17(dirname16(paths.alertsPath), { recursive: true });
14913
- appendFileSync7(paths.alertsPath, `${msg2}
15424
+ appendFileSync7(alertsPath, `${msg2}
14914
15425
  `, "utf8");
14915
15426
  } catch {
14916
15427
  }
14917
15428
  process.stderr.write(`loop run-once: roll-loop SKILL.md not found \u2014 refusing to spawn a blind agent (ALERT written)
14918
15429
  loop run-once: \u627E\u4E0D\u5230 roll-loop SKILL.md \u2014 \u62D2\u7EDD\u76F2\u5F00 agent(\u5DF2\u5199 ALERT)
14919
15430
  `);
15431
+ incrementConsecutiveFails(id.path, id.slug, alertsPath, cycleId, "", "skill_missing");
14920
15432
  return 1;
14921
15433
  }
14922
- const routeDeps = {
14923
- readSlot: () => void 0,
14924
- firstInstalled: () => process.env["ROLL_LOOP_AGENT"] ?? "claude"
14925
- };
15434
+ const routeDeps = buildLoopRouteDeps(id.path);
15435
+ const isInteractive = (process.env["ROLL_LOOP_FORCE"] ?? "").trim() !== "";
14926
15436
  const ports = nodePorts({
14927
15437
  repoCwd: id.path,
14928
15438
  paths,
14929
15439
  skillBody,
14930
- routeDeps
15440
+ routeDeps,
15441
+ ...isInteractive ? {
15442
+ agentSpawn: (agent, opts) => realAgentSpawn(agent, { ...opts, interactive: true })
15443
+ } : {}
14931
15444
  });
14932
15445
  const disposeSignals = installCycleSignalTeardown(paths, cycleId, branch);
14933
15446
  let result;
@@ -14946,22 +15459,28 @@ loop run-once: \u627E\u4E0D\u5230 roll-loop SKILL.md \u2014 \u62D2\u7EDD\u76F2\u
14946
15459
  if (result.terminal === "done") {
14947
15460
  const storyId = (result.state?.ctx?.storyId ?? "").trim();
14948
15461
  announceReport(id.path, id.slug, storyId);
15462
+ resetConsecutiveFails(id.path);
14949
15463
  }
14950
- return result.terminal === "failed" || result.terminal === "blocked" ? 1 : 0;
15464
+ const isFail = result.terminal === "failed" || result.terminal === "blocked";
15465
+ if (isFail) {
15466
+ const storyId = (result.state?.ctx?.storyId ?? "").trim();
15467
+ incrementConsecutiveFails(id.path, id.slug, alertsPath, cycleId, storyId, result.terminal ?? "unknown");
15468
+ }
15469
+ return isFail ? 1 : 0;
14951
15470
  }
14952
15471
 
14953
15472
  // packages/cli/dist/commands/loop-sched.js
14954
15473
  import { createHash as createHash4 } from "node:crypto";
14955
15474
  import { spawn as spawn4, spawnSync as spawnSync6 } from "node:child_process";
14956
- import { existsSync as existsSync36, mkdirSync as mkdirSync18, readFileSync as readFileSync29, rmSync as rmSync9, writeFileSync as writeFileSync17 } from "node:fs";
14957
- import { homedir as homedir14 } from "node:os";
14958
- import { dirname as dirname17, join as join32 } from "node:path";
15475
+ import { existsSync as existsSync37, mkdirSync as mkdirSync19, readFileSync as readFileSync29, rmSync as rmSync9, writeFileSync as writeFileSync20 } from "node:fs";
15476
+ import { homedir as homedir15 } from "node:os";
15477
+ import { dirname as dirname16, join as join34 } from "node:path";
14959
15478
  function realDeps3() {
14960
15479
  return {
14961
15480
  identity: () => projectIdentity(),
14962
15481
  uid: () => process.getuid?.() ?? 501,
14963
- sharedRoot: () => process.env["ROLL_SHARED_ROOT"] || join32(homedir14(), ".shared", "roll"),
14964
- launchdDir: () => join32(homedir14(), "Library", "LaunchAgents"),
15482
+ sharedRoot: () => process.env["ROLL_SHARED_ROOT"] || join34(homedir15(), ".shared", "roll"),
15483
+ launchdDir: () => join34(homedir15(), "Library", "LaunchAgents"),
14965
15484
  launchd: { reinstall, uninstall, isLoaded },
14966
15485
  execRunner: (runner) => new Promise((resolve3) => {
14967
15486
  const child = spawn4("bash", [runner], {
@@ -14981,8 +15500,8 @@ function realDeps3() {
14981
15500
  // The `loop now` inline observation: tail live.log while the cycle holds
14982
15501
  // the inner lock; Ctrl-C stops the TAIL only (the cycle lives in tmux).
14983
15502
  observe: (rt) => new Promise((resolve3) => {
14984
- const lock = join32(rt, "inner.lock");
14985
- const tail = spawn4("tail", ["-n", "+1", "-F", join32(rt, "live.log")], { stdio: "inherit" });
15503
+ const lock = join34(rt, "inner.lock");
15504
+ const tail = spawn4("tail", ["-n", "+1", "-F", join34(rt, "live.log")], { stdio: "inherit" });
14986
15505
  let sawLock = false;
14987
15506
  const t0 = Date.now();
14988
15507
  const finish = () => {
@@ -14994,9 +15513,9 @@ function realDeps3() {
14994
15513
  resolve3();
14995
15514
  };
14996
15515
  const timer = setInterval(() => {
14997
- if (existsSync36(lock))
15516
+ if (existsSync37(lock))
14998
15517
  sawLock = true;
14999
- const done = sawLock ? !existsSync36(lock) : Date.now() - t0 > 3e4;
15518
+ const done = sawLock ? !existsSync37(lock) : Date.now() - t0 > 3e4;
15000
15519
  if (done) {
15001
15520
  clearInterval(timer);
15002
15521
  finish();
@@ -15153,7 +15672,7 @@ function parseLoopPeriodMinutes(text) {
15153
15672
  return 30;
15154
15673
  }
15155
15674
  function pathValue() {
15156
- const home = homedir14();
15675
+ const home = homedir15();
15157
15676
  return [
15158
15677
  "/opt/homebrew/bin",
15159
15678
  `${home}/.local/bin`,
@@ -15165,11 +15684,11 @@ function pathValue() {
15165
15684
  ].join(":");
15166
15685
  }
15167
15686
  function writeExecutable(path, content) {
15168
- mkdirSync18(dirname17(path), { recursive: true });
15169
- writeFileSync17(path, content, { mode: 493 });
15687
+ mkdirSync19(dirname16(path), { recursive: true });
15688
+ writeFileSync20(path, content, { mode: 493 });
15170
15689
  }
15171
15690
  function pauseMarkerPath(projectPath, slug) {
15172
- return join32(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15691
+ return join34(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15173
15692
  }
15174
15693
  var LOOP_SERVICES = ["loop", "dream", "pr"];
15175
15694
  async function mountService(deps, uid, label4, plist) {
@@ -15188,16 +15707,16 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15188
15707
  const shared = deps.sharedRoot();
15189
15708
  const ld = deps.launchdDir();
15190
15709
  const uid = deps.uid();
15191
- mkdirSync18(ld, { recursive: true });
15710
+ mkdirSync19(ld, { recursive: true });
15192
15711
  let period = 30;
15193
- const localYaml = join32(id.path, ".roll", "local.yaml");
15194
- if (existsSync36(localYaml)) {
15712
+ const localYaml = join34(id.path, ".roll", "local.yaml");
15713
+ if (existsSync37(localYaml)) {
15195
15714
  try {
15196
15715
  period = parseLoopPeriodMinutes(readFileSync29(localYaml, "utf8"));
15197
15716
  } catch {
15198
15717
  }
15199
15718
  }
15200
- const loopRunner = join32(shared, "loop", `run-${id.slug}.sh`);
15719
+ const loopRunner = join34(shared, "loop", `run-${id.slug}.sh`);
15201
15720
  const rollBinOverride = (process.env["ROLL_RUNNER_ROLL_BIN"] ?? "").trim();
15202
15721
  writeExecutable(loopRunner, buildLoopRunnerScript({
15203
15722
  projectPath: id.path,
@@ -15208,7 +15727,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15208
15727
  }));
15209
15728
  const loopLabel = launchdLabel("loop", id.slug);
15210
15729
  const loopPlist = launchdPlistPath("loop", id.slug, ld);
15211
- writeFileSync17(loopPlist, plistContent({
15730
+ writeFileSync20(loopPlist, plistContent({
15212
15731
  label: loopLabel,
15213
15732
  runnerScript: loopRunner,
15214
15733
  projectPath: id.path,
@@ -15216,14 +15735,14 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15216
15735
  schedule: { kind: "interval", periodMinutes: period }
15217
15736
  }));
15218
15737
  const loopMount = await mountService(deps, uid, loopLabel, loopPlist);
15219
- const prRunner = join32(shared, "pr", `run-${id.slug}.sh`);
15738
+ const prRunner = join34(shared, "pr", `run-${id.slug}.sh`);
15220
15739
  writeExecutable(prRunner, buildPrRunnerScript({
15221
15740
  projectPath: id.path,
15222
15741
  ...rollBinOverride !== "" ? { rollBin: rollBinOverride } : {}
15223
15742
  }));
15224
15743
  const prLabel = launchdLabel("pr", id.slug);
15225
15744
  const prPlist = launchdPlistPath("pr", id.slug, ld);
15226
- writeFileSync17(prPlist, plistContent({
15745
+ writeFileSync20(prPlist, plistContent({
15227
15746
  label: prLabel,
15228
15747
  runnerScript: prRunner,
15229
15748
  projectPath: id.path,
@@ -15232,7 +15751,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15232
15751
  }));
15233
15752
  const prMount = await mountService(deps, uid, prLabel, prPlist);
15234
15753
  const dream = dreamScheduleFor(id.path);
15235
- const dreamRunner = join32(shared, "dream", `run-${id.slug}.sh`);
15754
+ const dreamRunner = join34(shared, "dream", `run-${id.slug}.sh`);
15236
15755
  writeExecutable(dreamRunner, buildDreamRunnerScript({
15237
15756
  projectPath: id.path,
15238
15757
  slug: id.slug,
@@ -15240,7 +15759,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15240
15759
  }));
15241
15760
  const dreamLabel = launchdLabel("dream", id.slug);
15242
15761
  const dreamPlist = launchdPlistPath("dream", id.slug, ld);
15243
- writeFileSync17(dreamPlist, plistContent({
15762
+ writeFileSync20(dreamPlist, plistContent({
15244
15763
  label: dreamLabel,
15245
15764
  runnerScript: dreamRunner,
15246
15765
  projectPath: id.path,
@@ -15292,10 +15811,10 @@ Loop \u5DF2\u505C\u7528(loop/dream/pr \u5747\u5DF2\u5378\u8F7D)
15292
15811
  async function loopPauseCommand(_args, deps = realDeps3()) {
15293
15812
  const id = await deps.identity();
15294
15813
  const marker = pauseMarkerPath(id.path, id.slug);
15295
- mkdirSync18(dirname17(marker), { recursive: true });
15296
- const already = existsSync36(marker);
15814
+ mkdirSync19(dirname16(marker), { recursive: true });
15815
+ const already = existsSync37(marker);
15297
15816
  if (!already)
15298
- writeFileSync17(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
15817
+ writeFileSync20(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
15299
15818
  `);
15300
15819
  process.stdout.write(already ? `Loop already paused
15301
15820
  Loop \u5DF2\u5904\u4E8E\u6682\u505C
@@ -15307,7 +15826,7 @@ Loop \u5DF2\u6682\u505C \u2014 \u540E\u7EED\u6392\u7A0B\u5468\u671F\u5C06\u8DF3\
15307
15826
  async function loopResumeCommand(_args, deps = realDeps3()) {
15308
15827
  const id = await deps.identity();
15309
15828
  const marker = pauseMarkerPath(id.path, id.slug);
15310
- const existed = existsSync36(marker);
15829
+ const existed = existsSync37(marker);
15311
15830
  rmSync9(marker, { force: true });
15312
15831
  process.stdout.write(existed ? `Loop resumed \u2014 scheduling active again
15313
15832
  Loop \u5DF2\u6062\u590D \u2014 \u6392\u7A0B\u91CD\u65B0\u751F\u6548
@@ -15325,16 +15844,16 @@ function isLegacyRunner(text) {
15325
15844
  }
15326
15845
  async function loopNowCommand(_args, deps = realDeps3()) {
15327
15846
  const id = await deps.identity();
15328
- const runner = join32(deps.sharedRoot(), "loop", `run-${id.slug}.sh`);
15847
+ const runner = join34(deps.sharedRoot(), "loop", `run-${id.slug}.sh`);
15329
15848
  let legacy = false;
15330
- if (existsSync36(runner)) {
15849
+ if (existsSync37(runner)) {
15331
15850
  try {
15332
15851
  legacy = isLegacyRunner(readFileSync29(runner, "utf8"));
15333
15852
  } catch {
15334
15853
  legacy = true;
15335
15854
  }
15336
15855
  }
15337
- if (!existsSync36(runner) || legacy) {
15856
+ if (!existsSync37(runner) || legacy) {
15338
15857
  process.stdout.write(legacy ? `Legacy v2 runner detected \u2014 regenerating templates (FIX-197)
15339
15858
  \u68C0\u6D4B\u5230 v2 \u65E7\u7248 runner \u2014 \u6B63\u5728\u518D\u751F\u6210\u6A21\u677F\uFF08FIX-197\uFF09
15340
15859
  ` : `No runner yet \u2014 generating templates
@@ -15362,7 +15881,7 @@ live transcript below \u2014 Ctrl-C stops watching, never the cycle
15362
15881
  }
15363
15882
  const rc = await exec(runner);
15364
15883
  if (rc === 0 && useTmux && deps.observe !== void 0) {
15365
- await deps.observe(join32(id.path, ".roll", "loop"));
15884
+ await deps.observe(join34(id.path, ".roll", "loop"));
15366
15885
  process.stdout.write(`
15367
15886
  cycle finished \u2014 logs: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
15368
15887
  \u5468\u671F\u7ED3\u675F \u2014 \u65E5\u5FD7: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
@@ -15373,8 +15892,8 @@ cycle finished \u2014 logs: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
15373
15892
 
15374
15893
  // packages/cli/dist/commands/migrate.js
15375
15894
  import { spawnSync as spawnSync7 } from "node:child_process";
15376
- import { existsSync as existsSync37, mkdirSync as mkdirSync19 } from "node:fs";
15377
- import { dirname as dirname18 } from "node:path";
15895
+ import { existsSync as existsSync38, mkdirSync as mkdirSync20 } from "node:fs";
15896
+ import { dirname as dirname17 } from "node:path";
15378
15897
  function colors() {
15379
15898
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
15380
15899
  return noColor2 ? { RED: "", GREEN: "", YELLOW: "", CYAN: "", NC: "" } : {
@@ -15493,9 +16012,9 @@ function migrateExecute(activeMoves) {
15493
16012
  for (const move of activeMoves) {
15494
16013
  const src = srcOf(move);
15495
16014
  const tgt = tgtOf(move);
15496
- const targetDir = dirname18(tgt);
15497
- if (!existsSync37(targetDir))
15498
- mkdirSync19(targetDir, { recursive: true });
16015
+ const targetDir = dirname17(tgt);
16016
+ if (!existsSync38(targetDir))
16017
+ mkdirSync20(targetDir, { recursive: true });
15499
16018
  const r = git3(["mv", src, tgt]);
15500
16019
  if (r.status !== 0) {
15501
16020
  err11(`git mv failed: ${src} \u2192 ${tgt}`);
@@ -15504,7 +16023,7 @@ function migrateExecute(activeMoves) {
15504
16023
  }
15505
16024
  moved += 1;
15506
16025
  }
15507
- if (existsSync37("docs")) {
16026
+ if (existsSync38("docs")) {
15508
16027
  spawnSync7("find", ["docs", "-type", "d", "-empty", "-delete"], { stdio: "ignore" });
15509
16028
  }
15510
16029
  git3([
@@ -15545,10 +16064,10 @@ function migrateCommand(args) {
15545
16064
  const moves = buildMoves();
15546
16065
  let hasNew = false;
15547
16066
  let hasOld = false;
15548
- if (existsSync37(".roll"))
16067
+ if (existsSync38(".roll"))
15549
16068
  hasNew = true;
15550
16069
  for (const move of moves) {
15551
- if (existsSync37(srcOf(move))) {
16070
+ if (existsSync38(srcOf(move))) {
15552
16071
  hasOld = true;
15553
16072
  break;
15554
16073
  }
@@ -15560,7 +16079,7 @@ function migrateCommand(args) {
15560
16079
  for (const move of moves) {
15561
16080
  const src = srcOf(move);
15562
16081
  const tgt = tgtOf(move);
15563
- if (existsSync37(src) && existsSync37(tgt)) {
16082
+ if (existsSync38(src) && existsSync38(tgt)) {
15564
16083
  process.stderr.write(` - ${src} AND ${tgt} both exist
15565
16084
  `);
15566
16085
  }
@@ -15577,7 +16096,7 @@ function migrateCommand(args) {
15577
16096
  info3(m2("migrate.no_old_structure_detected_nothing_to"));
15578
16097
  return 0;
15579
16098
  }
15580
- const activeMoves = moves.filter((move) => existsSync37(srcOf(move)));
16099
+ const activeMoves = moves.filter((move) => existsSync38(srcOf(move)));
15581
16100
  if (activeMoves.length === 0) {
15582
16101
  warn4(m2("migrate.old_structure_markers_found_but_no"));
15583
16102
  return 0;
@@ -15593,11 +16112,99 @@ function migrateCommand(args) {
15593
16112
  return migrateExecute(activeMoves);
15594
16113
  }
15595
16114
 
16115
+ // packages/cli/dist/commands/migrate-features.js
16116
+ import { existsSync as existsSync39, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readlinkSync as readlinkSync3, statSync as statSync17, writeFileSync as writeFileSync21 } from "node:fs";
16117
+ import { join as join35 } from "node:path";
16118
+ function specFrontmatter(specFile, storyId) {
16119
+ try {
16120
+ const lines = readFileSync30(specFile, "utf8").split("\n").slice(0, 12);
16121
+ const pick = (key) => {
16122
+ const row2 = lines.find((l) => l.startsWith(`${key}: `));
16123
+ return row2?.slice(key.length + 2).trim();
16124
+ };
16125
+ let title = pick("title");
16126
+ if (title === void 0) {
16127
+ const heading = lines.find((l) => l.startsWith("# "));
16128
+ const m7 = heading?.match(new RegExp(`^# ${storyId}\\s*[\u2014\u2013-]+\\s*(.+)$`));
16129
+ if (m7 !== void 0 && m7 !== null)
16130
+ title = m7[1];
16131
+ }
16132
+ return { ...title !== void 0 ? { title } : {}, ...pick("created") !== void 0 ? { created: pick("created") } : {} };
16133
+ } catch {
16134
+ return {};
16135
+ }
16136
+ }
16137
+ function migrateFeaturesCommand(args) {
16138
+ const refresh = args.includes("--refresh-html");
16139
+ const cwd = process.cwd();
16140
+ if (!existsSync39(join35(cwd, ".roll", "index.json"))) {
16141
+ process.stderr.write("migrate-features: .roll/index.json not found\n");
16142
+ return 1;
16143
+ }
16144
+ const stories = readIndex(cwd);
16145
+ const root = join35(cwd, ".roll", "features");
16146
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16147
+ let specCreated = 0;
16148
+ let htmlWritten = 0;
16149
+ let skipped = 0;
16150
+ for (const [storyId, epic] of Object.entries(stories)) {
16151
+ const storyDir = join35(root, epic, storyId);
16152
+ const specFile = join35(storyDir, "spec.md");
16153
+ if (!existsSync39(storyDir)) {
16154
+ const mdFile = join35(root, epic, `${storyId}.md`);
16155
+ if (existsSync39(mdFile)) {
16156
+ try {
16157
+ mkdirSync21(storyDir, { recursive: true });
16158
+ } catch {
16159
+ skipped++;
16160
+ continue;
16161
+ }
16162
+ } else {
16163
+ skipped++;
16164
+ continue;
16165
+ }
16166
+ }
16167
+ if (!existsSync39(specFile)) {
16168
+ writeFileSync21(specFile, renderSpecMd({
16169
+ id: storyId,
16170
+ epic,
16171
+ created: today,
16172
+ note: "Auto-generated by migrate-features (US-META-007)."
16173
+ }), "utf8");
16174
+ specCreated++;
16175
+ }
16176
+ const htmlFile = join35(storyDir, "index.html");
16177
+ if (!existsSync39(htmlFile) || refresh) {
16178
+ const fm = specFrontmatter(specFile, storyId);
16179
+ const card = { id: storyId, epic, created: fm.created ?? today, ...fm.title !== void 0 ? { title: fm.title } : {} };
16180
+ let html = renderStoryPage(card);
16181
+ const latest = join35(storyDir, "latest");
16182
+ const report = join35(latest, reportFileName(storyId));
16183
+ if (existsSync39(report)) {
16184
+ let runRel = "latest";
16185
+ try {
16186
+ runRel = readlinkSync3(latest);
16187
+ } catch {
16188
+ }
16189
+ const deliveredOn = statSync17(report).mtime.toISOString().slice(0, 10);
16190
+ html = markPhaseDone(html, "delivery", `<p><a href="${runRel}/${reportFileName(storyId)}">${bi("Attestation report", "\u9A8C\u6536\u62A5\u544A")}</a></p>
16191
+ <p class="muted">${bi("Delivered", "\u4EA4\u4ED8\u4E8E")} ${deliveredOn}</p>
16192
+ `);
16193
+ }
16194
+ writeFileSync21(htmlFile, html, "utf8");
16195
+ htmlWritten++;
16196
+ }
16197
+ }
16198
+ process.stdout.write(`migrate-features: ${specCreated} spec.md + ${htmlWritten} index.html ${refresh ? "written" : "created"}, ${skipped} skipped
16199
+ `);
16200
+ return 0;
16201
+ }
16202
+
15596
16203
  // packages/cli/dist/commands/offboard.js
15597
16204
  import { spawnSync as spawnSync8 } from "node:child_process";
15598
- import { existsSync as existsSync38, readFileSync as readFileSync30, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync18 } from "node:fs";
15599
- import { homedir as homedir15 } from "node:os";
15600
- import { join as join33, resolve as resolve2 } from "node:path";
16205
+ import { existsSync as existsSync40, readFileSync as readFileSync31, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync22 } from "node:fs";
16206
+ import { homedir as homedir16 } from "node:os";
16207
+ import { join as join36, resolve as resolve2 } from "node:path";
15601
16208
  function pal3() {
15602
16209
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
15603
16210
  return noColor2 ? { RED: "", GREEN: "", YELLOW: "", CYAN: "", BOLD: "", NC: "" } : {
@@ -15635,7 +16242,7 @@ function m3(key, ...args) {
15635
16242
  return t(v2Catalog, msgLang9(), key, ...args);
15636
16243
  }
15637
16244
  function changesetPath(projectDir) {
15638
- return join33(projectDir, ".roll", "onboard-changeset.yaml");
16245
+ return join36(projectDir, ".roll", "onboard-changeset.yaml");
15639
16246
  }
15640
16247
  function loopInCycle() {
15641
16248
  return (process.env["ROLL_LOOP_AGENT"] ?? "") !== "" || (process.env["ROLL_CYCLE_LOG_RAW"] ?? "") !== "";
@@ -15645,7 +16252,7 @@ function runLaunchctl(args) {
15645
16252
  if (args[0] !== void 0 && readOnly.has(args[0])) {
15646
16253
  return spawnSync8("launchctl", args, { stdio: "inherit" }).status ?? 1;
15647
16254
  }
15648
- const canonical = join33(homedir15(), "Library", "LaunchAgents");
16255
+ const canonical = join36(homedir16(), "Library", "LaunchAgents");
15649
16256
  const launchdDir = process.env["_LAUNCHD_DIR"] ?? canonical;
15650
16257
  if (launchdDir !== canonical)
15651
16258
  return 0;
@@ -15709,7 +16316,7 @@ function offboardCommand(args) {
15709
16316
  projectDir = process.cwd();
15710
16317
  }
15711
16318
  const changeset = changesetPath(projectDir);
15712
- if (!existsSync38(changeset)) {
16319
+ if (!existsSync40(changeset)) {
15713
16320
  err12(m3("offboard.no_changeset_en"));
15714
16321
  err12(m3("offboard.no_changeset_zh"));
15715
16322
  process.stderr.write("\n");
@@ -15723,7 +16330,7 @@ function offboardCommand(args) {
15723
16330
  `);
15724
16331
  return 1;
15725
16332
  }
15726
- const parsed = parseChangeset(readFileSync30(changeset, "utf8"));
16333
+ const parsed = parseChangeset(readFileSync31(changeset, "utf8"));
15727
16334
  if (!parsed.ok) {
15728
16335
  err12(m3("offboard.failed_to_parse_changeset"));
15729
16336
  return 1;
@@ -15799,8 +16406,8 @@ function offboardCommand(args) {
15799
16406
  process.stdout.write(m3("offboard.applying_offboard") + "\n");
15800
16407
  for (const item of files) {
15801
16408
  try {
15802
- rmSync10(join33(projectDir, item), { force: true });
15803
- if (!existsSync38(join33(projectDir, item)))
16409
+ rmSync10(join36(projectDir, item), { force: true });
16410
+ if (!existsSync40(join36(projectDir, item)))
15804
16411
  process.stdout.write(` removed file ${item}
15805
16412
  `);
15806
16413
  } catch {
@@ -15808,26 +16415,26 @@ function offboardCommand(args) {
15808
16415
  }
15809
16416
  for (const item of dirs) {
15810
16417
  try {
15811
- rmSync10(join33(projectDir, item), { recursive: true, force: true });
16418
+ rmSync10(join36(projectDir, item), { recursive: true, force: true });
15812
16419
  process.stdout.write(` removed dir ${item}
15813
16420
  `);
15814
16421
  } catch {
15815
16422
  }
15816
16423
  }
15817
16424
  for (const item of giEntries) {
15818
- const gi = join33(projectDir, ".gitignore");
15819
- if (existsSync38(gi)) {
15820
- const lines = readFileSync30(gi, "utf8").split("\n");
16425
+ const gi = join36(projectDir, ".gitignore");
16426
+ if (existsSync40(gi)) {
16427
+ const lines = readFileSync31(gi, "utf8").split("\n");
15821
16428
  if (lines.includes(item)) {
15822
16429
  const kept = lines.filter((l) => l !== item);
15823
- writeFileSync18(gi, kept.join("\n"));
16430
+ writeFileSync22(gi, kept.join("\n"));
15824
16431
  process.stdout.write(` .gitignore - ${item}
15825
16432
  `);
15826
16433
  }
15827
16434
  }
15828
16435
  }
15829
16436
  for (const item of plists) {
15830
- const plistPath = join33(homedir15(), "Library", "LaunchAgents", item);
16437
+ const plistPath = join36(homedir16(), "Library", "LaunchAgents", item);
15831
16438
  const r = runLaunchctl(["unload", "-w", plistPath]);
15832
16439
  if (r === 0)
15833
16440
  process.stdout.write(` unloaded ${item}
@@ -15840,16 +16447,16 @@ function offboardCommand(args) {
15840
16447
  }
15841
16448
 
15842
16449
  // packages/cli/dist/commands/prices.js
15843
- import { existsSync as existsSync39, readdirSync as readdirSync15, readFileSync as readFileSync31 } from "node:fs";
15844
- import { join as join34 } from "node:path";
16450
+ import { existsSync as existsSync41, readdirSync as readdirSync16, readFileSync as readFileSync32 } from "node:fs";
16451
+ import { join as join37 } from "node:path";
15845
16452
  function loadSnapshots() {
15846
- const dir = join34(repoRoot(), "lib", "prices");
15847
- if (!existsSync39(dir)) {
16453
+ const dir = join37(repoRoot(), "lib", "prices");
16454
+ if (!existsSync41(dir)) {
15848
16455
  throw new Error(`no price snapshots found in ${dir}; run \`roll prices refresh\``);
15849
16456
  }
15850
- const files = readdirSync15(dir).filter((n) => n.startsWith("snapshot-") && n.endsWith(".json")).sort();
16457
+ const files = readdirSync16(dir).filter((n) => n.startsWith("snapshot-") && n.endsWith(".json")).sort();
15851
16458
  return files.map((name) => {
15852
- const data = JSON.parse(readFileSync31(join34(dir, name), "utf8"));
16459
+ const data = JSON.parse(readFileSync32(join37(dir, name), "utf8"));
15853
16460
  for (const key of ["version", "effective_at", "source_url", "prices"]) {
15854
16461
  if (data[key] === void 0)
15855
16462
  throw new Error(`snapshot ${name} missing required key ${key}`);
@@ -15942,8 +16549,8 @@ function pricesCommand(args) {
15942
16549
  }
15943
16550
 
15944
16551
  // packages/cli/dist/commands/release.js
15945
- import { existsSync as existsSync40, readFileSync as readFileSync32 } from "node:fs";
15946
- import { join as join35 } from "node:path";
16552
+ import { existsSync as existsSync42, readFileSync as readFileSync33 } from "node:fs";
16553
+ import { join as join38 } from "node:path";
15947
16554
  function label3(lang4, key, ...args) {
15948
16555
  if (v3Catalog[key] !== void 0)
15949
16556
  return t(v3Catalog, lang4, key, ...args);
@@ -15954,17 +16561,17 @@ function dateOf(d) {
15954
16561
  }
15955
16562
  function currentVersion(cwd) {
15956
16563
  try {
15957
- const pkg = JSON.parse(readFileSync32(join35(cwd, "package.json"), "utf8"));
16564
+ const pkg = JSON.parse(readFileSync33(join38(cwd, "package.json"), "utf8"));
15958
16565
  return typeof pkg.version === "string" ? pkg.version : "";
15959
16566
  } catch {
15960
16567
  return "";
15961
16568
  }
15962
16569
  }
15963
16570
  function changelogReady(cwd) {
15964
- const path = join35(cwd, "CHANGELOG.md");
15965
- if (!existsSync40(path))
16571
+ const path = join38(cwd, "CHANGELOG.md");
16572
+ if (!existsSync42(path))
15966
16573
  return false;
15967
- const text = readFileSync32(path, "utf8");
16574
+ const text = readFileSync33(path, "utf8");
15968
16575
  const idx = text.indexOf("## Unreleased");
15969
16576
  if (idx === -1)
15970
16577
  return false;
@@ -16031,8 +16638,8 @@ function releaseCommand(args, now) {
16031
16638
 
16032
16639
  // packages/cli/dist/commands/setup.js
16033
16640
  import { spawnSync as spawnSync9 } from "node:child_process";
16034
- import { existsSync as existsSync41, lstatSync as lstatSync3, mkdirSync as mkdirSync20, readFileSync as readFileSync33, readdirSync as readdirSync16, readlinkSync as readlinkSync3, statSync as statSync16 } from "node:fs";
16035
- import { join as join36 } from "node:path";
16641
+ import { existsSync as existsSync43, lstatSync as lstatSync3, mkdirSync as mkdirSync22, readFileSync as readFileSync34, readdirSync as readdirSync17, readlinkSync as readlinkSync4, statSync as statSync18 } from "node:fs";
16642
+ import { join as join39 } from "node:path";
16036
16643
  function err13(line) {
16037
16644
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
16038
16645
  const RED = noColor2 ? "" : "\x1B[0;31m";
@@ -16057,7 +16664,7 @@ function setupSnapshot(watch) {
16057
16664
  continue;
16058
16665
  let isDir4 = false;
16059
16666
  try {
16060
- isDir4 = statSync16(d).isDirectory();
16667
+ isDir4 = statSync18(d).isDirectory();
16061
16668
  } catch {
16062
16669
  isDir4 = false;
16063
16670
  }
@@ -16070,12 +16677,12 @@ function setupSnapshot(watch) {
16070
16677
  function walk(dir, lines) {
16071
16678
  let names;
16072
16679
  try {
16073
- names = readdirSync16(dir);
16680
+ names = readdirSync17(dir);
16074
16681
  } catch {
16075
16682
  return;
16076
16683
  }
16077
16684
  for (const name of names) {
16078
- const p = join36(dir, name);
16685
+ const p = join39(dir, name);
16079
16686
  let isLink = false;
16080
16687
  try {
16081
16688
  isLink = lstatSync3(p).isSymbolicLink();
@@ -16084,14 +16691,14 @@ function walk(dir, lines) {
16084
16691
  }
16085
16692
  if (isLink) {
16086
16693
  try {
16087
- lines.push(`L ${p} -> ${readlinkSync3(p)}`);
16694
+ lines.push(`L ${p} -> ${readlinkSync4(p)}`);
16088
16695
  } catch {
16089
16696
  }
16090
16697
  continue;
16091
16698
  }
16092
16699
  let st;
16093
16700
  try {
16094
- st = statSync16(p);
16701
+ st = statSync18(p);
16095
16702
  } catch {
16096
16703
  continue;
16097
16704
  }
@@ -16105,7 +16712,7 @@ function walk(dir, lines) {
16105
16712
  }
16106
16713
  function fileFingerprint(p) {
16107
16714
  try {
16108
- const buf = readFileSync33(p);
16715
+ const buf = readFileSync34(p);
16109
16716
  let sum = 0;
16110
16717
  for (let i = 0; i < buf.length; i++)
16111
16718
  sum = sum * 31 + (buf[i] ?? 0) >>> 0;
@@ -16135,21 +16742,21 @@ function ensureHooksPath(repoPath) {
16135
16742
  }
16136
16743
  }
16137
16744
  function peerEnsureStateDir() {
16138
- const base = join36(rollHome(), ".peer-state");
16139
- mkdirSync20(base, { recursive: true });
16140
- mkdirSync20(join36(base, "logs"), { recursive: true });
16745
+ const base = join39(rollHome(), ".peer-state");
16746
+ mkdirSync22(base, { recursive: true });
16747
+ mkdirSync22(join39(base, "logs"), { recursive: true });
16141
16748
  }
16142
16749
  function tmuxPresent() {
16143
16750
  return onPath("tmux");
16144
16751
  }
16145
16752
  function submoduleGuard() {
16146
16753
  const pkg = rollPkgDir();
16147
- const skills = join36(pkg, "skills");
16148
- const hasGit = existsSync41(join36(pkg, ".git"));
16149
- const hasMods = existsSync41(join36(pkg, ".gitmodules"));
16754
+ const skills = join39(pkg, "skills");
16755
+ const hasGit = existsSync43(join39(pkg, ".git"));
16756
+ const hasMods = existsSync43(join39(pkg, ".gitmodules"));
16150
16757
  let empty = true;
16151
16758
  try {
16152
- empty = readdirSync16(skills).length === 0;
16759
+ empty = readdirSync17(skills).length === 0;
16153
16760
  } catch {
16154
16761
  empty = true;
16155
16762
  }
@@ -16239,7 +16846,7 @@ function setupCommand(args) {
16239
16846
  return 1;
16240
16847
  }
16241
16848
  }
16242
- if (!existsSync41(rollPkgConventions()))
16849
+ if (!existsSync43(rollPkgConventions()))
16243
16850
  return null;
16244
16851
  submoduleGuard();
16245
16852
  const home = rollHome();
@@ -16258,7 +16865,7 @@ function setupCommand(args) {
16258
16865
  ".qwen"
16259
16866
  ];
16260
16867
  const homeDir = process.env["HOME"] ?? "";
16261
- const aiDirs = aiDirsList.map((d) => join36(homeDir, d)).join(":");
16868
+ const aiDirs = aiDirsList.map((d) => join39(homeDir, d)).join(":");
16262
16869
  const steps = [];
16263
16870
  const s1 = runSetupStep(home, () => {
16264
16871
  installLocal(force);
@@ -16272,7 +16879,7 @@ function setupCommand(args) {
16272
16879
  syncSkills(force);
16273
16880
  });
16274
16881
  steps.push({ num: 3, label: "Install skills to ~/.claude", status: stateToMarker(s3, force) });
16275
- const s4 = runSetupStep(join36(home, ".peer-state"), () => {
16882
+ const s4 = runSetupStep(join39(home, ".peer-state"), () => {
16276
16883
  peerEnsureStateDir();
16277
16884
  });
16278
16885
  steps.push({ num: 4, label: "Initialize peer-review state directory", status: stateToMarker(s4, force) });
@@ -16293,12 +16900,12 @@ function setupCommand(args) {
16293
16900
 
16294
16901
  // packages/cli/dist/commands/slides/index.js
16295
16902
  import { spawnSync as spawnSync10 } from "node:child_process";
16296
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync36, readdirSync as readdirSync18, rmSync as rmSync11, statSync as statSync19, writeFileSync as writeFileSync19 } from "node:fs";
16297
- import { join as join38 } from "node:path";
16903
+ import { mkdirSync as mkdirSync23, readFileSync as readFileSync37, readdirSync as readdirSync19, rmSync as rmSync11, statSync as statSync21, writeFileSync as writeFileSync23 } from "node:fs";
16904
+ import { join as join41 } from "node:path";
16298
16905
 
16299
16906
  // packages/cli/dist/commands/slides/render.js
16300
- import { readFileSync as readFileSync34, existsSync as existsSync42, readdirSync as readdirSync17, statSync as statSync17 } from "node:fs";
16301
- import { join as join37, dirname as dirname19, basename as basename9 } from "node:path";
16907
+ import { readFileSync as readFileSync35, existsSync as existsSync44, readdirSync as readdirSync18, statSync as statSync19 } from "node:fs";
16908
+ import { join as join40, dirname as dirname18, basename as basename9 } from "node:path";
16302
16909
  function coerceScalar(v) {
16303
16910
  if (v.length >= 2 && v[0] === v[v.length - 1] && (v[0] === "'" || v[0] === '"')) {
16304
16911
  return v.slice(1, -1);
@@ -16504,8 +17111,8 @@ function parseSequence(block, baseIndent) {
16504
17111
  i += 1;
16505
17112
  continue;
16506
17113
  }
16507
- const bi = indentOf2(bl);
16508
- if (bi <= baseIndent)
17114
+ const bi2 = indentOf2(bl);
17115
+ if (bi2 <= baseIndent)
16509
17116
  break;
16510
17117
  sub.push(bl);
16511
17118
  i += 1;
@@ -16783,12 +17390,12 @@ var DEFAULT_LAYOUT = "plain";
16783
17390
  var LayoutResolver = class {
16784
17391
  componentsDir;
16785
17392
  constructor(componentsDir) {
16786
- this.componentsDir = componentsDir ?? join37(libDirFor(import.meta.url), "slides", "components");
17393
+ this.componentsDir = componentsDir ?? join40(libDirFor(import.meta.url), "slides", "components");
16787
17394
  }
16788
17395
  available() {
16789
17396
  if (!isDir2(this.componentsDir))
16790
17397
  return [];
16791
- const names = readdirSync17(this.componentsDir).filter((f) => f.endsWith(".html")).map((f) => basename9(f, ".html"));
17398
+ const names = readdirSync18(this.componentsDir).filter((f) => f.endsWith(".html")).map((f) => basename9(f, ".html"));
16792
17399
  const rest = names.filter((n) => n !== DEFAULT_LAYOUT).sort();
16793
17400
  const head = names.includes(DEFAULT_LAYOUT) ? [DEFAULT_LAYOUT] : [];
16794
17401
  return [...head, ...rest];
@@ -16797,7 +17404,7 @@ var LayoutResolver = class {
16797
17404
  if (!/^[a-z0-9-]+$/.test(layout)) {
16798
17405
  throw new ValueError(`Unknown layout: ${layout}; available: ${this.available().join(", ")}`);
16799
17406
  }
16800
- const path = join37(this.componentsDir, `${layout}.html`);
17407
+ const path = join40(this.componentsDir, `${layout}.html`);
16801
17408
  if (!isFile(path)) {
16802
17409
  throw new ValueError(`Unknown layout: ${layout}; available: ${this.available().join(", ")}`);
16803
17410
  }
@@ -16807,7 +17414,7 @@ var LayoutResolver = class {
16807
17414
  function renderSlideInner(slide, resolver) {
16808
17415
  const layout = typeof slide["layout"] === "string" && slide["layout"] || DEFAULT_LAYOUT;
16809
17416
  const partialPath = resolver.resolve(String(layout));
16810
- let partial = readFileSync34(partialPath, "utf8");
17417
+ let partial = readFileSync35(partialPath, "utf8");
16811
17418
  partial = partial.replace(/^\s*<!--[\s\S]*?-->(?:\s*\n)?/, "");
16812
17419
  const rendered = mustache(partial, slide);
16813
17420
  return stripNewlines(rendered);
@@ -16855,30 +17462,30 @@ function pyRepr(s) {
16855
17462
  }
16856
17463
  function isDir2(p) {
16857
17464
  try {
16858
- return statSync17(p).isDirectory();
17465
+ return statSync19(p).isDirectory();
16859
17466
  } catch {
16860
17467
  return false;
16861
17468
  }
16862
17469
  }
16863
17470
  function isFile(p) {
16864
17471
  try {
16865
- return statSync17(p).isFile();
17472
+ return statSync19(p).isFile();
16866
17473
  } catch {
16867
17474
  return false;
16868
17475
  }
16869
17476
  }
16870
17477
  function libDirFor(_metaUrl) {
16871
17478
  void _metaUrl;
16872
- void existsSync42;
16873
- void dirname19;
16874
- return join37(repoLibFallback(), "lib");
17479
+ void existsSync44;
17480
+ void dirname18;
17481
+ return join40(repoLibFallback(), "lib");
16875
17482
  }
16876
17483
  function repoLibFallback() {
16877
17484
  let dir = process.cwd();
16878
17485
  for (let i = 0; i < 12; i++) {
16879
- if (existsSync42(join37(dir, "bin", "roll")))
17486
+ if (existsSync44(join40(dir, "bin", "roll")))
16880
17487
  return dir;
16881
- const parent = dirname19(dir);
17488
+ const parent = dirname18(dir);
16882
17489
  if (parent === dir)
16883
17490
  break;
16884
17491
  dir = parent;
@@ -16887,7 +17494,7 @@ function repoLibFallback() {
16887
17494
  }
16888
17495
 
16889
17496
  // packages/cli/dist/commands/slides/validate.js
16890
- import { readFileSync as readFileSync35, statSync as statSync18 } from "node:fs";
17497
+ import { readFileSync as readFileSync36, statSync as statSync20 } from "node:fs";
16891
17498
  var REQUIRED_FRONTMATTER = [
16892
17499
  "template",
16893
17500
  "slug",
@@ -17091,7 +17698,7 @@ function validateDeckFile(path, errSink, componentsDir) {
17091
17698
  let fm;
17092
17699
  let slides;
17093
17700
  try {
17094
- src = readFileSync35(path, "utf8");
17701
+ src = readFileSync36(path, "utf8");
17095
17702
  [fm] = parseFrontmatter(src);
17096
17703
  const [, body] = parseFrontmatter(src);
17097
17704
  slides = parseSlides(body);
@@ -17125,7 +17732,7 @@ function validateDeckFile(path, errSink, componentsDir) {
17125
17732
  }
17126
17733
  function isFile2(p) {
17127
17734
  try {
17128
- return statSync18(p).isFile();
17735
+ return statSync20(p).isFile();
17129
17736
  } catch {
17130
17737
  return false;
17131
17738
  }
@@ -17204,13 +17811,13 @@ function slidesHelp(out2) {
17204
17811
  out2(SLIDES_HELP);
17205
17812
  }
17206
17813
  function slidesLib() {
17207
- return join38(rollPkgDir(), "lib");
17814
+ return join41(rollPkgDir(), "lib");
17208
17815
  }
17209
17816
  function slidesTemplatePath(name) {
17210
- const projTpl = join38(".roll", "slides", "templates", `${name}.html`);
17817
+ const projTpl = join41(".roll", "slides", "templates", `${name}.html`);
17211
17818
  if (isFile3(projTpl))
17212
17819
  return projTpl;
17213
- const tpl = join38(rollPkgDir(), "lib", "slides", "templates", `${name}.html`);
17820
+ const tpl = join41(rollPkgDir(), "lib", "slides", "templates", `${name}.html`);
17214
17821
  if (isFile3(tpl))
17215
17822
  return tpl;
17216
17823
  return null;
@@ -17218,7 +17825,7 @@ function slidesTemplatePath(name) {
17218
17825
  function slidesTemplateForDeck(deckPath) {
17219
17826
  let tpl = "";
17220
17827
  try {
17221
- const src = readFileSync36(deckPath, "utf8");
17828
+ const src = readFileSync37(deckPath, "utf8");
17222
17829
  let d = 0;
17223
17830
  for (const line of src.split("\n")) {
17224
17831
  if (/^---[ \t]*$/.test(line)) {
@@ -17244,21 +17851,21 @@ function slidesTemplateForDeck(deckPath) {
17244
17851
  return tpl;
17245
17852
  }
17246
17853
  function slidesEnsureGitignore() {
17247
- const gi = join38(".roll", ".gitignore");
17248
- mkdirSync21(".roll", { recursive: true });
17854
+ const gi = join41(".roll", ".gitignore");
17855
+ mkdirSync23(".roll", { recursive: true });
17249
17856
  if (isFile3(gi)) {
17250
- const content = readFileSync36(gi, "utf8");
17857
+ const content = readFileSync37(gi, "utf8");
17251
17858
  if (content.split("\n").some((l) => /^slides\/\*\.html$/.test(l)))
17252
17859
  return;
17253
17860
  if (content.length > 0 && !content.endsWith("\n")) {
17254
- writeFileSync19(gi, content + "\n");
17861
+ writeFileSync23(gi, content + "\n");
17255
17862
  }
17256
17863
  }
17257
17864
  appendFile(gi, "slides/*.html\n");
17258
17865
  }
17259
17866
  function appendFile(path, data) {
17260
- const prev = isFile3(path) ? readFileSync36(path, "utf8") : "";
17261
- writeFileSync19(path, prev + data);
17867
+ const prev = isFile3(path) ? readFileSync37(path, "utf8") : "";
17868
+ writeFileSync23(path, prev + data);
17262
17869
  }
17263
17870
  function slidesOpenCmd() {
17264
17871
  const sys = spawnSync10("uname", ["-s"], { encoding: "utf8" });
@@ -17317,7 +17924,7 @@ function cmdBuild(args) {
17317
17924
  process.stderr.write(m5("slides_build.usage_roll_slides_build_slug_no") + "\n");
17318
17925
  return 1;
17319
17926
  }
17320
- const deck = join38(".roll", "slides", slug, "deck.md");
17927
+ const deck = join41(".roll", "slides", slug, "deck.md");
17321
17928
  if (!isFile3(deck)) {
17322
17929
  err15(`Deck not found: ${deck}`);
17323
17930
  process.stderr.write(m5("slides_build.en_deck", deck) + "\n");
@@ -17327,14 +17934,14 @@ function cmdBuild(args) {
17327
17934
  return 1;
17328
17935
  }
17329
17936
  const libDir = slidesLib();
17330
- const validator = join38(libDir, "slides-validate.py");
17331
- const renderer = join38(libDir, "slides-render.py");
17937
+ const validator = join41(libDir, "slides-validate.py");
17938
+ const renderer = join41(libDir, "slides-render.py");
17332
17939
  if (!isFile3(validator) || !isFile3(renderer)) {
17333
17940
  err15(m5("slides_build.slides_toolchain_missing_re_run_roll"));
17334
17941
  return 1;
17335
17942
  }
17336
- const errFile = join38(".roll", "slides", slug, ".last-build.err");
17337
- const componentsDir = join38(libDir, "slides", "components");
17943
+ const errFile = join41(".roll", "slides", slug, ".last-build.err");
17944
+ const componentsDir = join41(libDir, "slides", "components");
17338
17945
  const valLines = [];
17339
17946
  const valExit = validateDeckFile(deck, (l) => valLines.push(l), componentsDir);
17340
17947
  const valOut = valLines.join("\n");
@@ -17343,8 +17950,8 @@ function cmdBuild(args) {
17343
17950
  `);
17344
17951
  } else if (valExit !== 0) {
17345
17952
  const ts = utcTs();
17346
- mkdirSync21(join38(".roll", "slides", slug), { recursive: true });
17347
- writeFileSync19(errFile, `[${ts}] stage=validate
17953
+ mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
17954
+ writeFileSync23(errFile, `[${ts}] stage=validate
17348
17955
  ${valOut}
17349
17956
  `);
17350
17957
  if (valOut !== "")
@@ -17361,15 +17968,15 @@ ${valOut}
17361
17968
  const tplPath = slidesTemplatePath(tplName);
17362
17969
  if (tplPath === null) {
17363
17970
  const ts = utcTs();
17364
- mkdirSync21(join38(".roll", "slides", slug), { recursive: true });
17365
- writeFileSync19(errFile, `[${ts}] stage=template
17971
+ mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
17972
+ writeFileSync23(errFile, `[${ts}] stage=template
17366
17973
  template not found: ${tplName}
17367
17974
  `);
17368
17975
  const { RED, NC } = pal4();
17369
17976
  process.stderr.write(`${RED}[FAIL]${NC} ${m5("slides_build.template_not_found", tplName)}
17370
17977
  `);
17371
17978
  process.stderr.write(" " + m5("slides_build.available_templates") + "\n");
17372
- const builtinDir = join38(rollPkgDir(), "lib", "slides", "templates");
17979
+ const builtinDir = join41(rollPkgDir(), "lib", "slides", "templates");
17373
17980
  if (isDir3(builtinDir)) {
17374
17981
  for (const t2 of listHtml(builtinDir)) {
17375
17982
  const n = basenameNoHtml(t2);
@@ -17377,7 +17984,7 @@ template not found: ${tplName}
17377
17984
  `);
17378
17985
  }
17379
17986
  }
17380
- const projDir = join38(".roll", "slides", "templates");
17987
+ const projDir = join41(".roll", "slides", "templates");
17381
17988
  if (isDir3(projDir)) {
17382
17989
  for (const t2 of listHtml(projDir)) {
17383
17990
  const n = basenameNoHtml(t2);
@@ -17388,18 +17995,18 @@ template not found: ${tplName}
17388
17995
  process.stderr.write(" " + m5("slides_build.templates_list_hint") + "\n");
17389
17996
  return 1;
17390
17997
  }
17391
- const out2 = join38(".roll", "slides", `${slug}.html`);
17392
- mkdirSync21(join38(".roll", "slides"), { recursive: true });
17998
+ const out2 = join41(".roll", "slides", `${slug}.html`);
17999
+ mkdirSync23(join41(".roll", "slides"), { recursive: true });
17393
18000
  let htmlOut;
17394
18001
  try {
17395
- const src = readFileSync36(deck, "utf8");
17396
- const template = readFileSync36(tplPath, "utf8");
18002
+ const src = readFileSync37(deck, "utf8");
18003
+ const template = readFileSync37(tplPath, "utf8");
17397
18004
  htmlOut = renderDeck(src, template, { componentsDir });
17398
18005
  } catch (e) {
17399
18006
  const renderOut = renderErrorBlock(e);
17400
18007
  const ts = utcTs();
17401
- mkdirSync21(join38(".roll", "slides", slug), { recursive: true });
17402
- writeFileSync19(errFile, `[${ts}] stage=render
18008
+ mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
18009
+ writeFileSync23(errFile, `[${ts}] stage=render
17403
18010
  ${renderOut}
17404
18011
  `);
17405
18012
  const { RED, NC } = pal4();
@@ -17413,7 +18020,7 @@ ${renderOut}
17413
18020
  }
17414
18021
  return 1;
17415
18022
  }
17416
- writeFileSync19(out2, htmlOut);
18023
+ writeFileSync23(out2, htmlOut);
17417
18024
  try {
17418
18025
  rmSync11(errFile, { force: true });
17419
18026
  } catch {
@@ -17438,7 +18045,7 @@ function tailLines(s, n) {
17438
18045
  }
17439
18046
  function frontmatterField2(deckPath, field) {
17440
18047
  try {
17441
- const src = readFileSync36(deckPath, "utf8");
18048
+ const src = readFileSync37(deckPath, "utf8");
17442
18049
  let d = 0;
17443
18050
  const pat = new RegExp("^" + field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[ ]*:[ ]*");
17444
18051
  for (const line of src.split("\n")) {
@@ -17481,7 +18088,7 @@ function cmdList(args) {
17481
18088
  err15(m5("slides_list.unexpected_argument_1"));
17482
18089
  return 1;
17483
18090
  }
17484
- const slidesDir = join38(".roll", "slides");
18091
+ const slidesDir = join41(".roll", "slides");
17485
18092
  if (!isDir3(slidesDir)) {
17486
18093
  info5(m5("slides_list.no_decks_found_under_roll_slides"));
17487
18094
  process.stdout.write(` Hint: run 'roll slides new "<topic>"' to create one.
@@ -17490,9 +18097,9 @@ function cmdList(args) {
17490
18097
  return 0;
17491
18098
  }
17492
18099
  const slugs = [];
17493
- for (const entry of readdirSync18(slidesDir)) {
17494
- const d = join38(slidesDir, entry);
17495
- if (isDir3(d) && isFile3(join38(d, "deck.md")))
18100
+ for (const entry of readdirSync19(slidesDir)) {
18101
+ const d = join41(slidesDir, entry);
18102
+ if (isDir3(d) && isFile3(join41(d, "deck.md")))
17496
18103
  slugs.push(entry);
17497
18104
  }
17498
18105
  if (slugs.length === 0) {
@@ -17506,9 +18113,9 @@ function cmdList(args) {
17506
18113
  process.stdout.write(rowSix("slug", "template", "total_slides", "created", "built", "size") + "\n");
17507
18114
  process.stdout.write(rowSix("----", "--------", "------------", "-------", "------", "----") + "\n");
17508
18115
  for (const s of sorted) {
17509
- const deck = join38(slidesDir, s, "deck.md");
17510
- const html = join38(slidesDir, `${s}.html`);
17511
- const errFile = join38(slidesDir, s, ".last-build.err");
18116
+ const deck = join41(slidesDir, s, "deck.md");
18117
+ const html = join41(slidesDir, `${s}.html`);
18118
+ const errFile = join41(slidesDir, s, ".last-build.err");
17512
18119
  let template = frontmatterField2(deck, "template");
17513
18120
  if (template === "")
17514
18121
  template = "-";
@@ -17531,7 +18138,7 @@ function cmdList(args) {
17531
18138
  built = "\u2713 built";
17532
18139
  let bytes = 0;
17533
18140
  try {
17534
- bytes = statSync19(html).size;
18141
+ bytes = statSync21(html).size;
17535
18142
  } catch {
17536
18143
  bytes = 0;
17537
18144
  }
@@ -17547,7 +18154,7 @@ function cmdList(args) {
17547
18154
  }
17548
18155
  function isNewer(a, b) {
17549
18156
  try {
17550
- return statSync19(a).mtimeMs > statSync19(b).mtimeMs;
18157
+ return statSync21(a).mtimeMs > statSync21(b).mtimeMs;
17551
18158
  } catch {
17552
18159
  return false;
17553
18160
  }
@@ -17582,7 +18189,7 @@ function cmdPreview(args) {
17582
18189
  process.stderr.write(m5("slides_preview.usage_roll_slides_preview_slug_no") + "\n");
17583
18190
  return 1;
17584
18191
  }
17585
- const html = join38(".roll", "slides", `${slug}.html`);
18192
+ const html = join41(".roll", "slides", `${slug}.html`);
17586
18193
  if (!isFile3(html)) {
17587
18194
  err15(`Rendered HTML not found: ${html}`);
17588
18195
  process.stderr.write(m5("slides_preview.en_html", html) + "\n");
@@ -17623,9 +18230,9 @@ function cmdLogs(args) {
17623
18230
  process.stderr.write(m5("slides_logs.usage_roll_slides_logs_slug") + "\n");
17624
18231
  return 1;
17625
18232
  }
17626
- const deckDir = join38(".roll", "slides", slug);
17627
- const errFile = join38(deckDir, ".last-build.err");
17628
- if (!isDir3(deckDir) || !isFile3(join38(deckDir, "deck.md"))) {
18233
+ const deckDir = join41(".roll", "slides", slug);
18234
+ const errFile = join41(deckDir, ".last-build.err");
18235
+ if (!isDir3(deckDir) || !isFile3(join41(deckDir, "deck.md"))) {
17629
18236
  err15(m5("slides_logs.deck_not_found", slug));
17630
18237
  return 1;
17631
18238
  }
@@ -17633,7 +18240,7 @@ function cmdLogs(args) {
17633
18240
  info5(m5("slides_logs.no_failure_records_for", slug));
17634
18241
  return 0;
17635
18242
  }
17636
- process.stdout.write(readFileSync36(errFile, "utf8"));
18243
+ process.stdout.write(readFileSync37(errFile, "utf8"));
17637
18244
  return 0;
17638
18245
  }
17639
18246
  function cmdDelete(args) {
@@ -17666,9 +18273,9 @@ function cmdDelete(args) {
17666
18273
  process.stderr.write(m5("slides_delete.usage_roll_slides_delete_slug_force") + "\n");
17667
18274
  return 1;
17668
18275
  }
17669
- const deckDir = join38(".roll", "slides", slug);
17670
- const html = join38(".roll", "slides", `${slug}.html`);
17671
- if (!isDir3(deckDir) || !isFile3(join38(deckDir, "deck.md"))) {
18276
+ const deckDir = join41(".roll", "slides", slug);
18277
+ const html = join41(".roll", "slides", `${slug}.html`);
18278
+ if (!isDir3(deckDir) || !isFile3(join41(deckDir, "deck.md"))) {
17672
18279
  err15(m5("slides_delete.deck_not_found", slug));
17673
18280
  return 1;
17674
18281
  }
@@ -17707,7 +18314,7 @@ function cmdTemplates(args) {
17707
18314
  let found = false;
17708
18315
  process.stdout.write(rowThree("name", "source", "path") + "\n");
17709
18316
  process.stdout.write(rowThree("----", "------", "----") + "\n");
17710
- const builtinDir = join38(rollPkgDir(), "lib", "slides", "templates");
18317
+ const builtinDir = join41(rollPkgDir(), "lib", "slides", "templates");
17711
18318
  if (isDir3(builtinDir)) {
17712
18319
  for (const tpl of listHtml(builtinDir)) {
17713
18320
  const name = basenameNoHtml(tpl);
@@ -17715,11 +18322,11 @@ function cmdTemplates(args) {
17715
18322
  found = true;
17716
18323
  }
17717
18324
  }
17718
- const projDir = join38(".roll", "slides", "templates");
18325
+ const projDir = join41(".roll", "slides", "templates");
17719
18326
  if (isDir3(projDir)) {
17720
18327
  for (const tpl of listHtml(projDir)) {
17721
18328
  const name = basenameNoHtml(tpl);
17722
- const source = isFile3(join38(builtinDir, `${name}.html`)) ? "project (override)" : "project";
18329
+ const source = isFile3(join41(builtinDir, `${name}.html`)) ? "project (override)" : "project";
17723
18330
  process.stdout.write(rowThree(name, source, tpl) + "\n");
17724
18331
  found = true;
17725
18332
  }
@@ -17767,20 +18374,20 @@ function slidesCommand(args) {
17767
18374
  }
17768
18375
  function isFile3(p) {
17769
18376
  try {
17770
- return statSync19(p).isFile();
18377
+ return statSync21(p).isFile();
17771
18378
  } catch {
17772
18379
  return false;
17773
18380
  }
17774
18381
  }
17775
18382
  function isDir3(p) {
17776
18383
  try {
17777
- return statSync19(p).isDirectory();
18384
+ return statSync21(p).isDirectory();
17778
18385
  } catch {
17779
18386
  return false;
17780
18387
  }
17781
18388
  }
17782
18389
  function listHtml(dir) {
17783
- return readdirSync18(dir).filter((f) => f.endsWith(".html")).sort().map((f) => join38(dir, f));
18390
+ return readdirSync19(dir).filter((f) => f.endsWith(".html")).sort().map((f) => join41(dir, f));
17784
18391
  }
17785
18392
  function basenameNoHtml(path) {
17786
18393
  const b = path.slice(path.lastIndexOf("/") + 1);
@@ -17800,15 +18407,15 @@ function rowThree(a, b, c2) {
17800
18407
  // packages/cli/dist/commands/status.js
17801
18408
  import { execFileSync as execFileSync8 } from "node:child_process";
17802
18409
  import { createHash as createHash5 } from "node:crypto";
17803
- import { existsSync as existsSync43, lstatSync as lstatSync4, readdirSync as readdirSync19, readFileSync as readFileSync37, realpathSync as realpathSync6, statSync as statSync20 } from "node:fs";
17804
- import { homedir as homedir16 } from "node:os";
17805
- import { basename as basename10, dirname as dirname20, join as join39 } from "node:path";
18410
+ import { existsSync as existsSync45, lstatSync as lstatSync4, readdirSync as readdirSync20, readFileSync as readFileSync38, realpathSync as realpathSync6, statSync as statSync22 } from "node:fs";
18411
+ import { homedir as homedir17 } from "node:os";
18412
+ import { basename as basename10, dirname as dirname19, join as join42 } from "node:path";
17806
18413
  function rollHome3() {
17807
- return process.env["ROLL_HOME"] ?? join39(homedir16(), ".roll");
18414
+ return process.env["ROLL_HOME"] ?? join42(homedir17(), ".roll");
17808
18415
  }
17809
- var globalDir = () => join39(rollHome3(), "conventions", "global");
17810
- var templatesDir = () => join39(rollHome3(), "conventions", "templates");
17811
- var configPath = () => join39(rollHome3(), "config.yaml");
18416
+ var globalDir = () => join42(rollHome3(), "conventions", "global");
18417
+ var templatesDir = () => join42(rollHome3(), "conventions", "templates");
18418
+ var configPath = () => join42(rollHome3(), "config.yaml");
17812
18419
  function projectSlugPy() {
17813
18420
  let path = realpathSync6(process.cwd());
17814
18421
  try {
@@ -17827,18 +18434,18 @@ function projectSlugPy() {
17827
18434
  var CONVENTION_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursor-rules", "project_rules.md"];
17828
18435
  var TEMPLATES = ["fullstack", "frontend-only", "backend-service", "cli"];
17829
18436
  function globalConventions() {
17830
- return CONVENTION_FILES.map((f) => [f, existsSync43(join39(globalDir(), f))]);
18437
+ return CONVENTION_FILES.map((f) => [f, existsSync45(join42(globalDir(), f))]);
17831
18438
  }
17832
18439
  function parseAiEntries() {
17833
18440
  const cfg = configPath();
17834
- if (!existsSync43(cfg))
18441
+ if (!existsSync45(cfg))
17835
18442
  return [];
17836
18443
  const entries = [];
17837
- for (const line of readFileSync37(cfg, "utf8").split("\n")) {
18444
+ for (const line of readFileSync38(cfg, "utf8").split("\n")) {
17838
18445
  const m7 = /^ai_[a-z]+:\s*(.+)/.exec(line);
17839
18446
  if (m7 === null)
17840
18447
  continue;
17841
- const val = (m7[1] ?? "").trim().replaceAll("~", homedir16());
18448
+ const val = (m7[1] ?? "").trim().replaceAll("~", homedir17());
17842
18449
  const parts = val.split("|");
17843
18450
  if (parts.length < 3)
17844
18451
  continue;
@@ -17847,28 +18454,28 @@ function parseAiEntries() {
17847
18454
  const srcFile = (parts[2] ?? "").trim();
17848
18455
  let name = basename10(aiDir).replace(/^\.+/, "");
17849
18456
  if (name === "workspace" || name === "agent") {
17850
- name = basename10(dirname20(aiDir)).replace(/^\.+/, "");
18457
+ name = basename10(dirname19(aiDir)).replace(/^\.+/, "");
17851
18458
  }
17852
18459
  entries.push({ name, ai_dir: aiDir, cfg_file: cfgFile, src_file: srcFile });
17853
18460
  }
17854
18461
  return entries;
17855
18462
  }
17856
18463
  function aiSyncStatus(e) {
17857
- const cfgFile = join39(e.ai_dir, e.cfg_file);
17858
- const rollMd = join39(e.ai_dir, "roll.md");
17859
- const src = join39(globalDir(), e.src_file);
17860
- if (!existsSync43(cfgFile))
18464
+ const cfgFile = join42(e.ai_dir, e.cfg_file);
18465
+ const rollMd = join42(e.ai_dir, "roll.md");
18466
+ const src = join42(globalDir(), e.src_file);
18467
+ if (!existsSync45(cfgFile))
17861
18468
  return "missing";
17862
- if (!existsSync43(rollMd))
18469
+ if (!existsSync45(rollMd))
17863
18470
  return "out-of-sync";
17864
18471
  try {
17865
- if (existsSync43(src) && !readFileSync37(rollMd).equals(readFileSync37(src)))
18472
+ if (existsSync45(src) && !readFileSync38(rollMd).equals(readFileSync38(src)))
17866
18473
  return "out-of-sync";
17867
18474
  } catch {
17868
18475
  return "out-of-sync";
17869
18476
  }
17870
18477
  try {
17871
- if (!readFileSync37(cfgFile, "utf8").includes("@roll.md"))
18478
+ if (!readFileSync38(cfgFile, "utf8").includes("@roll.md"))
17872
18479
  return "out-of-sync";
17873
18480
  } catch {
17874
18481
  return "out-of-sync";
@@ -17876,15 +18483,15 @@ function aiSyncStatus(e) {
17876
18483
  return "sync";
17877
18484
  }
17878
18485
  function aiSkillCount(e) {
17879
- const skillsDir2 = join39(e.ai_dir, "skills");
17880
- if (!existsSync43(skillsDir2))
18486
+ const skillsDir2 = join42(e.ai_dir, "skills");
18487
+ if (!existsSync45(skillsDir2))
17881
18488
  return 0;
17882
18489
  try {
17883
18490
  let n = 0;
17884
- for (const name of readdirSync19(skillsDir2)) {
18491
+ for (const name of readdirSync20(skillsDir2)) {
17885
18492
  if (!name.startsWith("roll-"))
17886
18493
  continue;
17887
- const p = join39(skillsDir2, name);
18494
+ const p = join42(skillsDir2, name);
17888
18495
  const st = lstatSync4(p);
17889
18496
  if (st.isSymbolicLink() || st.isDirectory())
17890
18497
  n++;
@@ -17896,9 +18503,9 @@ function aiSkillCount(e) {
17896
18503
  }
17897
18504
  function countFilesRecursive(dir) {
17898
18505
  let n = 0;
17899
- for (const name of readdirSync19(dir)) {
17900
- const p = join39(dir, name);
17901
- const st = statSync20(p);
18506
+ for (const name of readdirSync20(dir)) {
18507
+ const p = join42(dir, name);
18508
+ const st = statSync22(p);
17902
18509
  if (st.isDirectory())
17903
18510
  n += countFilesRecursive(p);
17904
18511
  else if (st.isFile())
@@ -17907,8 +18514,8 @@ function countFilesRecursive(dir) {
17907
18514
  return n;
17908
18515
  }
17909
18516
  function templateCount(tpl) {
17910
- const d = join39(templatesDir(), tpl);
17911
- if (!existsSync43(d))
18517
+ const d = join42(templatesDir(), tpl);
18518
+ if (!existsSync45(d))
17912
18519
  return 0;
17913
18520
  try {
17914
18521
  return countFilesRecursive(d);
@@ -17917,19 +18524,19 @@ function templateCount(tpl) {
17917
18524
  }
17918
18525
  }
17919
18526
  function skillsInstalled() {
17920
- const sd = join39(rollHome3(), "skills");
17921
- if (!existsSync43(sd))
18527
+ const sd = join42(rollHome3(), "skills");
18528
+ if (!existsSync45(sd))
17922
18529
  return 0;
17923
18530
  try {
17924
- return readdirSync19(sd).filter((n) => statSync20(join39(sd, n)).isDirectory()).length;
18531
+ return readdirSync20(sd).filter((n) => statSync22(join42(sd, n)).isDirectory()).length;
17925
18532
  } catch {
17926
18533
  return 0;
17927
18534
  }
17928
18535
  }
17929
18536
  function launchdState(service, slug) {
17930
18537
  const label4 = `com.roll.${service}.${slug}`;
17931
- const plist = join39(homedir16(), "Library", "LaunchAgents", `${label4}.plist`);
17932
- if (!existsSync43(plist))
18538
+ const plist = join42(homedir17(), "Library", "LaunchAgents", `${label4}.plist`);
18539
+ if (!existsSync45(plist))
17933
18540
  return "not-installed";
17934
18541
  try {
17935
18542
  const out2 = execFileSync8("launchctl", ["list", label4], {
@@ -18063,26 +18670,26 @@ function renderThisProject(out2, d) {
18063
18670
  }
18064
18671
  function liveData() {
18065
18672
  const slug = projectSlugPy();
18066
- const home = homedir16();
18673
+ const home = homedir17();
18067
18674
  const aiClients = parseAiEntries().map((e) => ({
18068
18675
  name: e.name,
18069
18676
  cfg_file: e.cfg_file,
18070
- path: join39(e.ai_dir, e.cfg_file).replaceAll(home, "~"),
18677
+ path: join42(e.ai_dir, e.cfg_file).replaceAll(home, "~"),
18071
18678
  sync: aiSyncStatus(e),
18072
18679
  skills: aiSkillCount(e)
18073
18680
  }));
18074
18681
  const featDir = ".roll/features";
18075
18682
  let featCount = 0;
18076
- if (existsSync43(featDir)) {
18077
- featCount = readdirSync19(featDir).filter((n) => n.endsWith(".md")).length;
18683
+ if (existsSync45(featDir)) {
18684
+ featCount = readdirSync20(featDir).filter((n) => n.endsWith(".md")).length;
18078
18685
  }
18079
18686
  return {
18080
18687
  conventions: globalConventions(),
18081
18688
  ai_clients: aiClients,
18082
18689
  templates: TEMPLATES.map((t2) => [t2, templateCount(t2)]),
18083
18690
  skills_installed: skillsInstalled(),
18084
- project_has_agents: existsSync43("AGENTS.md"),
18085
- project_has_backlog: existsSync43(".roll/backlog.md"),
18691
+ project_has_agents: existsSync45("AGENTS.md"),
18692
+ project_has_backlog: existsSync45(".roll/backlog.md"),
18086
18693
  project_features_count: featCount,
18087
18694
  loop_state: launchdState("loop", slug),
18088
18695
  dream_state: launchdState("dream", slug)
@@ -18106,8 +18713,8 @@ function statusCommand(args) {
18106
18713
 
18107
18714
  // packages/cli/dist/commands/test.js
18108
18715
  import { spawnSync as spawnSync11 } from "node:child_process";
18109
- import { existsSync as existsSync44, mkdirSync as mkdirSync22, readFileSync as readFileSync38, rmSync as rmSync12, writeFileSync as writeFileSync20 } from "node:fs";
18110
- import { dirname as dirname21, join as join40 } from "node:path";
18716
+ import { existsSync as existsSync46, mkdirSync as mkdirSync24, readFileSync as readFileSync39, rmSync as rmSync12, writeFileSync as writeFileSync24 } from "node:fs";
18717
+ import { dirname as dirname20, join as join43 } from "node:path";
18111
18718
  function pal5() {
18112
18719
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
18113
18720
  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" };
@@ -18124,12 +18731,12 @@ function err16(line) {
18124
18731
  }
18125
18732
  var SUPPORTED_TYPES = ["none"];
18126
18733
  function isolationGetType() {
18127
- const file = join40(process.cwd(), ".roll", "local.yaml");
18128
- if (!existsSync44(file))
18734
+ const file = join43(process.cwd(), ".roll", "local.yaml");
18735
+ if (!existsSync46(file))
18129
18736
  return "none";
18130
18737
  let text;
18131
18738
  try {
18132
- text = readFileSync38(file, "utf8");
18739
+ text = readFileSync39(file, "utf8");
18133
18740
  } catch {
18134
18741
  return "none";
18135
18742
  }
@@ -18177,14 +18784,14 @@ function resetLockPath() {
18177
18784
  return ".roll/.iso-reset.lock";
18178
18785
  }
18179
18786
  function resetLockHeld() {
18180
- return existsSync44(resetLockPath());
18787
+ return existsSync46(resetLockPath());
18181
18788
  }
18182
18789
  function resetAcquireLock() {
18183
18790
  const lock = resetLockPath();
18184
- if (existsSync44(lock))
18791
+ if (existsSync46(lock))
18185
18792
  return false;
18186
- mkdirSync22(dirname21(lock), { recursive: true });
18187
- writeFileSync20(lock, `${process.pid}
18793
+ mkdirSync24(dirname20(lock), { recursive: true });
18794
+ writeFileSync24(lock, `${process.pid}
18188
18795
  `);
18189
18796
  return true;
18190
18797
  }
@@ -18201,7 +18808,7 @@ function runForward(cmd, argv) {
18201
18808
  }
18202
18809
  function isolationDispatch(method, args) {
18203
18810
  const type = isolationGetType();
18204
- if (type === "none" && !existsSync44(join40(process.cwd(), ".roll", "local.yaml"))) {
18811
+ if (type === "none" && !existsSync46(join43(process.cwd(), ".roll", "local.yaml"))) {
18205
18812
  infoErr("isolation: no test_isolation config, falling back to type=none (host)");
18206
18813
  }
18207
18814
  if (!SUPPORTED_TYPES.includes(type)) {
@@ -18295,9 +18902,9 @@ function testCommand(args) {
18295
18902
 
18296
18903
  // packages/cli/dist/commands/update.js
18297
18904
  import { spawnSync as spawnSync12 } from "node:child_process";
18298
- import { existsSync as existsSync45, mkdtempSync as mkdtempSync4, readFileSync as readFileSync39, rmSync as rmSync13 } from "node:fs";
18905
+ import { existsSync as existsSync47, mkdtempSync as mkdtempSync4, readFileSync as readFileSync40, rmSync as rmSync13 } from "node:fs";
18299
18906
  import { tmpdir as tmpdir4 } from "node:os";
18300
- import { join as join41 } from "node:path";
18907
+ import { join as join44 } from "node:path";
18301
18908
  function pal6() {
18302
18909
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
18303
18910
  return noColor2 ? { CYAN: "", GREEN: "", YELLOW: "", RED: "", BOLD: "", NC: "" } : {
@@ -18361,20 +18968,20 @@ function resolveRemoteVersion() {
18361
18968
  }
18362
18969
  function downloadAndInstallCurl(tag) {
18363
18970
  const url = `https://github.com/seanyao/roll/archive/refs/tags/${tag}.tar.gz`;
18364
- const tmpDir = mkdtempSync4(join41(tmpdir4(), "roll-update-"));
18971
+ const tmpDir = mkdtempSync4(join44(tmpdir4(), "roll-update-"));
18365
18972
  try {
18366
18973
  info6(`[roll] Downloading roll ${tag} ...`);
18367
- const dl = runForward2("curl", ["-fsSL", url, "-o", join41(tmpDir, "roll.tar.gz")]);
18974
+ const dl = runForward2("curl", ["-fsSL", url, "-o", join44(tmpDir, "roll.tar.gz")]);
18368
18975
  if (dl !== 0) {
18369
18976
  err17(m6("update.curl_download_failed"));
18370
18977
  return { ok: false };
18371
18978
  }
18372
18979
  info6("[roll] Extracting ...");
18373
- const extractDir = join41(tmpDir, "extract");
18980
+ const extractDir = join44(tmpDir, "extract");
18374
18981
  spawnSync12("mkdir", ["-p", extractDir], { stdio: "ignore" });
18375
18982
  const ex = runForward2("tar", [
18376
18983
  "-xzf",
18377
- join41(tmpDir, "roll.tar.gz"),
18984
+ join44(tmpDir, "roll.tar.gz"),
18378
18985
  "--strip-components=1",
18379
18986
  "-C",
18380
18987
  extractDir
@@ -18391,7 +18998,7 @@ function downloadAndInstallCurl(tag) {
18391
18998
  function checkInstalledVersionOrRetry() {
18392
18999
  const expected = (spawnSync12("npm", ["view", "@seanyao/roll", "version"], { encoding: "utf8" }).stdout ?? "").trim();
18393
19000
  const pkgRoot = (spawnSync12("npm", ["root", "-g"], { encoding: "utf8" }).stdout ?? "").trim();
18394
- const installedTree = join41(pkgRoot, "@seanyao", "roll");
19001
+ const installedTree = join44(pkgRoot, "@seanyao", "roll");
18395
19002
  const installed = treeVersion(installedTree);
18396
19003
  if (expected === "" || installed === "")
18397
19004
  return;
@@ -18405,18 +19012,18 @@ function checkInstalledVersionOrRetry() {
18405
19012
  }
18406
19013
  }
18407
19014
  function invalidateUpdateCache() {
18408
- rmSync13(join41(rollHome(), ".update-check"), { force: true });
19015
+ rmSync13(join44(rollHome(), ".update-check"), { force: true });
18409
19016
  }
18410
19017
  function showChangelog() {
18411
- const changelog = join41(rollPkgDir(), "CHANGELOG.md");
18412
- if (!existsSync45(changelog))
19018
+ const changelog = join44(rollPkgDir(), "CHANGELOG.md");
19019
+ if (!existsSync47(changelog))
18413
19020
  return;
18414
19021
  const { BOLD: BOLD2, CYAN, NC } = pal6();
18415
19022
  process.stdout.write(`${BOLD2}${m6("changelog.heading")}:${NC}
18416
19023
  `);
18417
19024
  let count = 0;
18418
19025
  let inSection = false;
18419
- for (const line of readFileSync39(changelog, "utf8").split("\n")) {
19026
+ for (const line of readFileSync40(changelog, "utf8").split("\n")) {
18420
19027
  if (/^## /.test(line)) {
18421
19028
  count += 1;
18422
19029
  if (count > 3)
@@ -18436,9 +19043,9 @@ function updateCommand(args) {
18436
19043
  void args;
18437
19044
  info6(m6("update.current_version", rollVersion()));
18438
19045
  let installMethod = "npm";
18439
- const methodFile = join41(rollPkgDir(), ".install-method");
18440
- if (existsSync45(methodFile)) {
18441
- installMethod = readFileSync39(methodFile, "utf8").trim() || "npm";
19046
+ const methodFile = join44(rollPkgDir(), ".install-method");
19047
+ if (existsSync47(methodFile)) {
19048
+ installMethod = readFileSync40(methodFile, "utf8").trim() || "npm";
18442
19049
  }
18443
19050
  if (installMethod === "curl") {
18444
19051
  info6(m6("update.upgrading_via_curl"));
@@ -18528,6 +19135,7 @@ function registerAll() {
18528
19135
  const r = initCommand(args);
18529
19136
  return r ?? fallbackToBash(["init", ...args]).status;
18530
19137
  });
19138
+ registerPorted("migrate-features", migrateFeaturesCommand);
18531
19139
  registerPorted("migrate", migrateCommand);
18532
19140
  registerPorted("offboard", offboardCommand);
18533
19141
  registerPorted("setup", (args) => {