@seanyao/roll 3.607.2 → 3.608.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/roll.mjs CHANGED
@@ -6112,11 +6112,11 @@ import { spawnSync as spawnSync2 } from "node:child_process";
6112
6112
  import { dirname as dirname5, join as join7 } from "node:path";
6113
6113
 
6114
6114
  // packages/cli/dist/lib/archive-migrate.js
6115
- import { existsSync as existsSync7, readdirSync as readdirSync3, readlinkSync, statSync as statSync4 } from "node:fs";
6115
+ import { existsSync as existsSync7, readdirSync as readdirSync3, readlinkSync, statSync as statSync5 } from "node:fs";
6116
6116
  import { join as join6 } from "node:path";
6117
6117
 
6118
6118
  // packages/cli/dist/lib/archive.js
6119
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
6119
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
6120
6120
  import { basename as basename3, dirname as dirname4, join as join5 } from "node:path";
6121
6121
  var UNCATEGORIZED = "uncategorized";
6122
6122
  function findFeatureFile(projectPath, storyId) {
@@ -6130,7 +6130,8 @@ function findFeatureFile(projectPath, storyId) {
6130
6130
  if (e.isDirectory())
6131
6131
  walk2(p);
6132
6132
  else if (e.isFile() && e.name.endsWith(".md")) {
6133
- if (e.name === `${storyId}.md`)
6133
+ const idOwned = e.name === `${storyId}.md` || e.name === "spec.md" && basename3(dir) === storyId;
6134
+ if (idOwned)
6134
6135
  hits.unshift(p);
6135
6136
  else {
6136
6137
  try {
@@ -6210,9 +6211,6 @@ function cardArchiveDir(projectPath, storyId) {
6210
6211
  const epic = epicForStory(projectPath, storyId) ?? UNCATEGORIZED;
6211
6212
  return join5(projectPath, ".roll", "features", epic, storyId);
6212
6213
  }
6213
- function legacyArchiveDir(projectPath, storyId) {
6214
- return join5(projectPath, ".roll", "verification", storyId);
6215
- }
6216
6214
  function buildStoryIndex(ids, epicOf) {
6217
6215
  const out2 = {};
6218
6216
  for (const id of ids) {
@@ -6228,6 +6226,69 @@ function serializeIndex(stories) {
6228
6226
  sorted[k] = stories[k];
6229
6227
  return JSON.stringify({ stories: sorted }, null, 2) + "\n";
6230
6228
  }
6229
+ function specMeta(specPath) {
6230
+ let text;
6231
+ try {
6232
+ text = readFileSync6(specPath, "utf8");
6233
+ } catch {
6234
+ return {};
6235
+ }
6236
+ const out2 = {};
6237
+ const fm = /^---\n([\s\S]*?)\n---/.exec(text);
6238
+ if (fm !== null) {
6239
+ const t2 = /^title:\s*(.+)$/m.exec(fm[1] ?? "");
6240
+ const c2 = /^created:\s*(.+)$/m.exec(fm[1] ?? "");
6241
+ if (t2 !== null)
6242
+ out2.title = (t2[1] ?? "").trim();
6243
+ if (c2 !== null)
6244
+ out2.created = (c2[1] ?? "").trim();
6245
+ }
6246
+ if (out2.title === void 0) {
6247
+ const h1 = /^#\s+\S+\s*(?:[—·:-]\s*)?(.*)$/m.exec(text);
6248
+ if (h1 !== null && (h1[1] ?? "").trim() !== "")
6249
+ out2.title = (h1[1] ?? "").trim().replace(/\s*[✅🚫🔨].*$/u, "");
6250
+ }
6251
+ return out2;
6252
+ }
6253
+ function collectDossier(projectPath) {
6254
+ const root = join5(projectPath, ".roll", "features");
6255
+ if (!existsSync6(root))
6256
+ return [];
6257
+ const epics = [];
6258
+ let epicDirs = [];
6259
+ try {
6260
+ epicDirs = readdirSync2(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
6261
+ } catch {
6262
+ return [];
6263
+ }
6264
+ for (const epic of epicDirs) {
6265
+ const stories = [];
6266
+ let entries = [];
6267
+ try {
6268
+ entries = readdirSync2(join5(root, epic), { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Z][A-Z0-9]*-/.test(e.name)).map((e) => e.name).sort();
6269
+ } catch {
6270
+ continue;
6271
+ }
6272
+ for (const id of entries) {
6273
+ const dir = join5(root, epic, id);
6274
+ let delivered = false;
6275
+ try {
6276
+ delivered = statSync4(join5(dir, "latest")).isDirectory();
6277
+ } catch {
6278
+ }
6279
+ stories.push({
6280
+ id,
6281
+ epic,
6282
+ type: (id.split("-")[0] ?? id).toUpperCase(),
6283
+ ...specMeta(join5(dir, "spec.md")),
6284
+ delivered
6285
+ });
6286
+ }
6287
+ if (stories.length > 0)
6288
+ epics.push({ name: epic, stories, delivered: stories.filter((s) => s.delivered).length });
6289
+ }
6290
+ return epics;
6291
+ }
6231
6292
 
6232
6293
  // packages/cli/dist/lib/archive-migrate.js
6233
6294
  var RUN_ID_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/;
@@ -6243,7 +6304,7 @@ function runsInDir(absDir) {
6243
6304
  if (!e.isDirectory() || !RUN_ID_RE.test(e.name))
6244
6305
  continue;
6245
6306
  try {
6246
- out2.push({ runId: e.name, mtimeSec: Math.floor(statSync4(join6(absDir, e.name)).mtimeMs / 1e3) });
6307
+ out2.push({ runId: e.name, mtimeSec: Math.floor(statSync5(join6(absDir, e.name)).mtimeMs / 1e3) });
6247
6308
  } catch {
6248
6309
  }
6249
6310
  }
@@ -6950,7 +7011,7 @@ async function runPublishPlan(plan, opts = {}) {
6950
7011
  }
6951
7012
 
6952
7013
  // packages/infra/dist/process.js
6953
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync4, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
7014
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync4, statSync as statSync6, writeFileSync as writeFileSync5 } from "node:fs";
6954
7015
  import { dirname as dirname8 } from "node:path";
6955
7016
  var OUTER_LOCK_STALE_SEC = 900;
6956
7017
  var INNER_LOCK_STALE_SEC = 14400;
@@ -7126,7 +7187,7 @@ import { promisify as promisify4 } from "node:util";
7126
7187
  var execFileAsync4 = promisify4(execFile4);
7127
7188
 
7128
7189
  // packages/infra/dist/evidence.js
7129
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readdirSync as readdirSync5, statSync as statSync6, writeFileSync as writeFileSync6 } from "node:fs";
7190
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readdirSync as readdirSync5, statSync as statSync7, writeFileSync as writeFileSync6 } from "node:fs";
7130
7191
  import { join as join9 } from "node:path";
7131
7192
  import { execFile as execFile5 } from "node:child_process";
7132
7193
  import { promisify as promisify5 } from "node:util";
@@ -7183,13 +7244,13 @@ async function collectEvidence(opts) {
7183
7244
  const proof = join9(opts.projectPath, ".roll", "last-test-pass");
7184
7245
  if (existsSync12(proof)) {
7185
7246
  try {
7186
- const age = Math.max(0, Math.round((Date.parse(opts.now()) - statSync6(proof).mtimeMs) / 1e3));
7247
+ const age = Math.max(0, Math.round((Date.parse(opts.now()) - statSync7(proof).mtimeMs) / 1e3));
7187
7248
  testPass = { present: true, age_seconds: age };
7188
7249
  } catch {
7189
7250
  testPass = { present: true, age_seconds: -1 };
7190
7251
  }
7191
7252
  }
7192
- const listDir = (sub, ext) => {
7253
+ const listDir2 = (sub, ext) => {
7193
7254
  const dir = join9(opts.runDir, sub);
7194
7255
  if (!existsSync12(dir))
7195
7256
  return [];
@@ -7206,8 +7267,8 @@ async function collectEvidence(opts) {
7206
7267
  ci,
7207
7268
  deploy,
7208
7269
  test_pass: testPass,
7209
- screenshots: listDir("screenshots", /\.png$/i),
7210
- texts: listDir("evidence", /\.(txt|log)$/i)
7270
+ screenshots: listDir2("screenshots", /\.png$/i),
7271
+ texts: listDir2("evidence", /\.(txt|log)$/i)
7211
7272
  };
7212
7273
  }
7213
7274
  function writeEvidenceJson(manifest, runDir) {
@@ -7218,7 +7279,7 @@ function writeEvidenceJson(manifest, runDir) {
7218
7279
  }
7219
7280
 
7220
7281
  // packages/infra/dist/screenshot.js
7221
- import { existsSync as existsSync13, statSync as statSync7 } from "node:fs";
7282
+ import { existsSync as existsSync13, statSync as statSync8 } from "node:fs";
7222
7283
  import { execFile as execFile6 } from "node:child_process";
7223
7284
  import { promisify as promisify6 } from "node:util";
7224
7285
 
@@ -7280,7 +7341,7 @@ var defaultRun2 = async (cmd, argv) => {
7280
7341
  };
7281
7342
  function fileNonEmpty(p) {
7282
7343
  try {
7283
- return existsSync13(p) && statSync7(p).size > 0;
7344
+ return existsSync13(p) && statSync8(p).size > 0;
7284
7345
  } catch {
7285
7346
  return false;
7286
7347
  }
@@ -7296,11 +7357,11 @@ function parseRegion(region) {
7296
7357
  return { x, y, w, h };
7297
7358
  }
7298
7359
  function terminalOpenScript(line, r) {
7299
- const esc2 = line.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7360
+ const esc5 = line.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7300
7361
  return [
7301
7362
  'tell application "Terminal"',
7302
7363
  " activate",
7303
- ` do script "${esc2}"`,
7364
+ ` do script "${esc5}"`,
7304
7365
  " delay 1.5",
7305
7366
  ` set bounds of front window to {${r.x}, ${r.y}, ${r.x + r.w}, ${r.y + r.h}}`,
7306
7367
  "end tell"
@@ -7841,6 +7902,10 @@ async function attestCommand(args, deps = {}) {
7841
7902
  \u9A8C\u6536\u62A5\u544A\u5DF2\u751F\u6210
7842
7903
  ${relative(projectPath, reportPath)}
7843
7904
  `);
7905
+ try {
7906
+ generateIndex(projectPath);
7907
+ } catch {
7908
+ }
7844
7909
  if (!smoke.ok) {
7845
7910
  for (const p of smoke.problems)
7846
7911
  warn2(`render smoke: ${p}`);
@@ -8573,7 +8638,7 @@ function writeUnreleased(draft, cl) {
8573
8638
  import { spawnSync as spawnSync3 } from "node:child_process";
8574
8639
 
8575
8640
  // packages/cli/dist/commands/setup-shared.js
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";
8641
+ 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 statSync9, symlinkSync as symlinkSync3, writeFileSync as writeFileSync9 } from "node:fs";
8577
8642
  import { homedir as homedir5 } from "node:os";
8578
8643
  import { basename as basename5, dirname as dirname9, join as join12 } from "node:path";
8579
8644
  function rollHome() {
@@ -8616,7 +8681,7 @@ function aiField(entry, idx) {
8616
8681
  }
8617
8682
  function canonicalDir(path) {
8618
8683
  try {
8619
- if (!statSync8(path).isDirectory())
8684
+ if (!statSync9(path).isDirectory())
8620
8685
  return null;
8621
8686
  return realpathSync2(path);
8622
8687
  } catch {
@@ -8652,7 +8717,7 @@ function onPath(bin) {
8652
8717
  continue;
8653
8718
  const p = join12(dir, bin);
8654
8719
  try {
8655
- const st = statSync8(p);
8720
+ const st = statSync9(p);
8656
8721
  if (st.isFile() && (st.mode & 73) !== 0)
8657
8722
  return true;
8658
8723
  } catch {
@@ -8668,7 +8733,7 @@ function agentInstalledByName3(agent, dir = "") {
8668
8733
  case "opencode": {
8669
8734
  const p = join12(home, ".opencode", "bin", "opencode");
8670
8735
  try {
8671
- return existsSync18(p) && (statSync8(p).mode & 73) !== 0;
8736
+ return existsSync18(p) && (statSync9(p).mode & 73) !== 0;
8672
8737
  } catch {
8673
8738
  return false;
8674
8739
  }
@@ -8713,7 +8778,7 @@ function pruneDir(installedDir, sourceDir) {
8713
8778
  const installedF = join12(installedDir, name);
8714
8779
  let isFile4 = false;
8715
8780
  try {
8716
- isFile4 = statSync8(installedF).isFile();
8781
+ isFile4 = statSync9(installedF).isFile();
8717
8782
  } catch {
8718
8783
  isFile4 = false;
8719
8784
  }
@@ -8751,7 +8816,7 @@ function listFiles(dir, includeDot) {
8751
8816
  continue;
8752
8817
  const p = join12(dir, name);
8753
8818
  try {
8754
- if (statSync8(p).isFile())
8819
+ if (statSync9(p).isFile())
8755
8820
  out2.push(name);
8756
8821
  } catch {
8757
8822
  }
@@ -8778,7 +8843,7 @@ function pullConventions(force) {
8778
8843
  for (const tplName of listDirEntries(tplSrcRoot)) {
8779
8844
  const tplDir = join12(tplSrcRoot, tplName);
8780
8845
  try {
8781
- if (!statSync8(tplDir).isDirectory())
8846
+ if (!statSync9(tplDir).isDirectory())
8782
8847
  continue;
8783
8848
  } catch {
8784
8849
  continue;
@@ -8802,7 +8867,7 @@ function pullSkills() {
8802
8867
  for (const skillName of listDirEntries(pkgSkills)) {
8803
8868
  const skillDir = join12(pkgSkills, skillName);
8804
8869
  try {
8805
- if (!statSync8(skillDir).isDirectory())
8870
+ if (!statSync9(skillDir).isDirectory())
8806
8871
  continue;
8807
8872
  } catch {
8808
8873
  continue;
@@ -8812,7 +8877,7 @@ function pullSkills() {
8812
8877
  for (const name of listDirEntries(skillDir)) {
8813
8878
  const f = join12(skillDir, name);
8814
8879
  try {
8815
- if (statSync8(f).isFile())
8880
+ if (statSync9(f).isFile())
8816
8881
  copyFileSync(f, join12(destDir, name));
8817
8882
  } catch {
8818
8883
  }
@@ -8822,7 +8887,7 @@ function pullSkills() {
8822
8887
  for (const installedName of listDirEntries(homeSkills)) {
8823
8888
  const installedDir = join12(homeSkills, installedName);
8824
8889
  try {
8825
- if (!statSync8(installedDir).isDirectory())
8890
+ if (!statSync9(installedDir).isDirectory())
8826
8891
  continue;
8827
8892
  } catch {
8828
8893
  continue;
@@ -9049,7 +9114,7 @@ function linkSkills() {
9049
9114
  for (const skillName of listDirEntries(homeSkills)) {
9050
9115
  const skillDir = `${join12(homeSkills, skillName)}/`;
9051
9116
  try {
9052
- if (!statSync8(join12(homeSkills, skillName)).isDirectory())
9117
+ if (!statSync9(join12(homeSkills, skillName)).isDirectory())
9053
9118
  continue;
9054
9119
  } catch {
9055
9120
  continue;
@@ -9467,9 +9532,9 @@ function configCommand(args) {
9467
9532
  }
9468
9533
 
9469
9534
  // packages/cli/dist/commands/consistency.js
9470
- import { existsSync as existsSync19, readFileSync as readFileSync14, readdirSync as readdirSync8, statSync as statSync9 } from "node:fs";
9535
+ import { existsSync as existsSync19, readFileSync as readFileSync14, readdirSync as readdirSync8, statSync as statSync10 } from "node:fs";
9471
9536
  import { join as join14 } from "node:path";
9472
- var DIMENSIONS2 = ["code", "docs", "i18n", "tests", "site"];
9537
+ var DIMENSIONS2 = ["code", "cards", "docs", "i18n", "tests", "site"];
9473
9538
  function err6(line) {
9474
9539
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
9475
9540
  const RED = noColor2 ? "" : "\x1B[0;31m";
@@ -9530,6 +9595,64 @@ function checkFeaturesCatalog(projectDir) {
9530
9595
  }
9531
9596
  return { status: gaps.length === 0 ? "pass" : "fail", gaps };
9532
9597
  }
9598
+ function checkCards(projectDir) {
9599
+ const backlog = join14(projectDir, ".roll", "backlog.md");
9600
+ const featuresDir = join14(projectDir, ".roll", "features");
9601
+ if (!existsSync19(backlog) || !existsSync19(featuresDir))
9602
+ return { status: "pass", gaps: [] };
9603
+ const cardEpic = /* @__PURE__ */ new Map();
9604
+ try {
9605
+ for (const epic of readdirSync8(featuresDir, { withFileTypes: true })) {
9606
+ if (!epic.isDirectory())
9607
+ continue;
9608
+ for (const card of readdirSync8(join14(featuresDir, epic.name), { withFileTypes: true })) {
9609
+ if (!card.isDirectory())
9610
+ continue;
9611
+ if (existsSync19(join14(featuresDir, epic.name, card.name, "spec.md")) && !cardEpic.has(card.name)) {
9612
+ cardEpic.set(card.name, epic.name);
9613
+ }
9614
+ }
9615
+ }
9616
+ } catch {
9617
+ return { status: "pass", gaps: [], note: "features/ unreadable \u2014 skipped" };
9618
+ }
9619
+ const gaps = [];
9620
+ let doneNoReport = 0;
9621
+ let doneNoFolder = 0;
9622
+ for (const line of readText(backlog).split("\n")) {
9623
+ const row2 = /^\|\s*\[?((?:US|FIX|REFACTOR|IDEA)-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\]?/.exec(line);
9624
+ if (row2 === null)
9625
+ continue;
9626
+ const id = row2[1] ?? "";
9627
+ if (!cardEpic.has(id)) {
9628
+ if (line.includes("\u2705 Done"))
9629
+ doneNoFolder += 1;
9630
+ else
9631
+ gaps.push(`Live backlog row ${id} has no card folder (features/<epic>/${id}/spec.md)`);
9632
+ continue;
9633
+ }
9634
+ const ev = /\[evidence\]\(([^)]+)\)/.exec(line);
9635
+ if (ev !== null) {
9636
+ const target = (ev[1] ?? "").replace(/^\.roll\//, "");
9637
+ if (!existsSync19(join14(projectDir, ".roll", target))) {
9638
+ gaps.push(`Backlog row ${id} evidence link is broken: ${ev[1] ?? ""}`);
9639
+ }
9640
+ } else if (line.includes("\u2705 Done")) {
9641
+ const epic = cardEpic.get(id) ?? "";
9642
+ if (!existsSync19(join14(featuresDir, epic, id, "latest", `${id}-report.html`)))
9643
+ doneNoReport += 1;
9644
+ }
9645
+ }
9646
+ const result = { status: gaps.length === 0 ? "pass" : "fail", gaps };
9647
+ const notes = [];
9648
+ if (doneNoFolder > 0)
9649
+ notes.push(`${doneNoFolder} pre-card-era Done rows without a card folder`);
9650
+ if (doneNoReport > 0)
9651
+ notes.push(`${doneNoReport} Done rows without an attest report`);
9652
+ if (notes.length > 0)
9653
+ result.note = `${notes.join("; ")} (informational)`;
9654
+ return result;
9655
+ }
9533
9656
  var SITE_INTERNAL_FEATURES = /* @__PURE__ */ new Set([
9534
9657
  "cycle-meta-sync",
9535
9658
  "loop-log-locality",
@@ -9628,7 +9751,7 @@ function listFiles2(dir) {
9628
9751
  try {
9629
9752
  return readdirSync8(dir).filter((n) => {
9630
9753
  try {
9631
- return statSync9(join14(dir, n)).isFile();
9754
+ return statSync10(join14(dir, n)).isFile();
9632
9755
  } catch {
9633
9756
  return false;
9634
9757
  }
@@ -9705,7 +9828,7 @@ function rglobBats(dir) {
9705
9828
  const full = join14(d, e);
9706
9829
  let st;
9707
9830
  try {
9708
- st = statSync9(full);
9831
+ st = statSync10(full);
9709
9832
  } catch {
9710
9833
  continue;
9711
9834
  }
@@ -9782,6 +9905,8 @@ function runAll(projectDir) {
9782
9905
  let result;
9783
9906
  if (dim === "code")
9784
9907
  result = checkFeaturesCatalog(projectDir);
9908
+ else if (dim === "cards")
9909
+ result = checkCards(projectDir);
9785
9910
  else if (dim === "i18n")
9786
9911
  result = checkI18n(projectDir);
9787
9912
  else if (dim === "tests")
@@ -9896,7 +10021,7 @@ function consistencyCommand(args) {
9896
10021
  // packages/cli/dist/commands/dashboard.js
9897
10022
  import { execFileSync as execFileSync3 } from "node:child_process";
9898
10023
  import { createHash as createHash3 } from "node:crypto";
9899
- import { existsSync as existsSync20, readFileSync as readFileSync15, readdirSync as readdirSync9, realpathSync as realpathSync3, statSync as statSync10 } from "node:fs";
10024
+ import { existsSync as existsSync20, readFileSync as readFileSync15, readdirSync as readdirSync9, realpathSync as realpathSync3, statSync as statSync11 } from "node:fs";
9900
10025
  import { homedir as homedir7, platform } from "node:os";
9901
10026
  import { basename as basename6, join as join15 } from "node:path";
9902
10027
  var TZ_OFFSET_MS2 = 8 * 3600 * 1e3;
@@ -9989,7 +10114,7 @@ function sharedRoot() {
9989
10114
  }
9990
10115
  function isDir(p) {
9991
10116
  try {
9992
- return statSync10(p).isDirectory();
10117
+ return statSync11(p).isDirectory();
9993
10118
  } catch {
9994
10119
  return false;
9995
10120
  }
@@ -10564,8 +10689,8 @@ function loadClaudeSessionUsage(label4, slug) {
10564
10689
  if (entries.length === 0)
10565
10690
  return null;
10566
10691
  entries.sort((a, b) => {
10567
- const sa = statSync10(join15(projDir, a)).size;
10568
- const sb = statSync10(join15(projDir, b)).size;
10692
+ const sa = statSync11(join15(projDir, a)).size;
10693
+ const sb = statSync11(join15(projDir, b)).size;
10569
10694
  return sb - sa;
10570
10695
  });
10571
10696
  const path = join15(projDir, entries[0] ?? "");
@@ -10717,17 +10842,34 @@ function rollupForDay(dayCycles) {
10717
10842
  }
10718
10843
  return r;
10719
10844
  }
10720
- function selfScoreSummaryLine(notesDir = ".roll/notes", windowN = 14) {
10721
- if (!existsSync20(notesDir))
10722
- return "";
10723
- let files;
10845
+ function selfScoreSummaryLine(notesDir = ".roll/notes", windowN = 14, featuresDir = ".roll/features") {
10846
+ const entries = [];
10724
10847
  try {
10725
- files = readdirSync9(notesDir).filter((n) => n.endsWith(".md"));
10848
+ for (const n of readdirSync9(notesDir))
10849
+ if (n.endsWith(".md"))
10850
+ entries.push({ name: n, path: join15(notesDir, n) });
10851
+ } catch {
10852
+ }
10853
+ try {
10854
+ for (const epic of readdirSync9(featuresDir, { withFileTypes: true })) {
10855
+ if (!epic.isDirectory())
10856
+ continue;
10857
+ for (const card of readdirSync9(join15(featuresDir, epic.name), { withFileTypes: true })) {
10858
+ if (!card.isDirectory())
10859
+ continue;
10860
+ const nd = join15(featuresDir, epic.name, card.name, "notes");
10861
+ try {
10862
+ for (const n of readdirSync9(nd))
10863
+ if (n.endsWith(".md"))
10864
+ entries.push({ name: n, path: join15(nd, n) });
10865
+ } catch {
10866
+ }
10867
+ }
10868
+ }
10726
10869
  } catch {
10727
- return "";
10728
10870
  }
10729
- files.sort();
10730
- files = files.slice(-windowN);
10871
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
10872
+ const files = entries.slice(-windowN);
10731
10873
  if (files.length === 0)
10732
10874
  return "";
10733
10875
  let total = 0;
@@ -10739,7 +10881,7 @@ function selfScoreSummaryLine(notesDir = ".roll/notes", windowN = 14) {
10739
10881
  let verdict = null;
10740
10882
  let content;
10741
10883
  try {
10742
- content = readFileSync15(join15(notesDir, f), "utf8");
10884
+ content = readFileSync15(f.path, "utf8");
10743
10885
  } catch {
10744
10886
  content = "";
10745
10887
  }
@@ -11304,7 +11446,7 @@ function render(out2, events, cron, state, backlog, args) {
11304
11446
  out2.push(" " + c("dim", agentLine));
11305
11447
  let skillLine = "";
11306
11448
  try {
11307
- skillLine = selfScoreSummaryLine(process.env["ROLL_NOTES_DIR"]);
11449
+ skillLine = selfScoreSummaryLine(process.env["ROLL_NOTES_DIR"], 14, process.env["ROLL_FEATURES_DIR"]);
11308
11450
  } catch {
11309
11451
  skillLine = "";
11310
11452
  }
@@ -12168,13 +12310,13 @@ function loopAttachRetired() {
12168
12310
 
12169
12311
  // packages/cli/dist/commands/doctor.js
12170
12312
  import { execFileSync as execFileSync5 } from "node:child_process";
12171
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync24, mkdtempSync as mkdtempSync2, readFileSync as readFileSync19, readdirSync as readdirSync12, statSync as statSync12, writeFileSync as writeFileSync12 } from "node:fs";
12313
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync24, mkdtempSync as mkdtempSync2, readFileSync as readFileSync19, readdirSync as readdirSync12, statSync as statSync13, writeFileSync as writeFileSync12 } from "node:fs";
12172
12314
  import { homedir as homedir9, tmpdir as tmpdir2 } from "node:os";
12173
12315
  import { delimiter as delimiter2, join as join19 } from "node:path";
12174
12316
 
12175
12317
  // packages/cli/dist/commands/skills.js
12176
12318
  import { execFileSync as execFileSync4 } from "node:child_process";
12177
- import { existsSync as existsSync23, mkdtempSync, readFileSync as readFileSync18, readdirSync as readdirSync11, statSync as statSync11, writeFileSync as writeFileSync11 } from "node:fs";
12319
+ import { existsSync as existsSync23, mkdtempSync, readFileSync as readFileSync18, readdirSync as readdirSync11, statSync as statSync12, writeFileSync as writeFileSync11 } from "node:fs";
12178
12320
  import { tmpdir } from "node:os";
12179
12321
  import { join as join18 } from "node:path";
12180
12322
  function pkgDir() {
@@ -12249,7 +12391,7 @@ function generateCatalog() {
12249
12391
  for (const name of entries) {
12250
12392
  const skillDir = join18(dir, name);
12251
12393
  try {
12252
- if (!statSync11(skillDir).isDirectory())
12394
+ if (!statSync12(skillDir).isDirectory())
12253
12395
  continue;
12254
12396
  } catch {
12255
12397
  continue;
@@ -12382,7 +12524,7 @@ function commandOnPath2(bin) {
12382
12524
  if (dir === "")
12383
12525
  continue;
12384
12526
  try {
12385
- const st = statSync12(join19(dir, bin));
12527
+ const st = statSync13(join19(dir, bin));
12386
12528
  if (!st.isFile())
12387
12529
  continue;
12388
12530
  accessSync2(join19(dir, bin), constants2.X_OK);
@@ -12401,7 +12543,7 @@ function agentInstalledByName4(agent, dir) {
12401
12543
  const p = join19(home, ".opencode", "bin", "opencode");
12402
12544
  try {
12403
12545
  accessSync2(p, constants2.X_OK);
12404
- return statSync12(p).isFile();
12546
+ return statSync13(p).isFile();
12405
12547
  } catch {
12406
12548
  return false;
12407
12549
  }
@@ -12420,7 +12562,7 @@ function agentInstalledByName4(agent, dir) {
12420
12562
  }
12421
12563
  function safeIsDir(p) {
12422
12564
  try {
12423
- return statSync12(p).isDirectory();
12565
+ return statSync13(p).isDirectory();
12424
12566
  } catch {
12425
12567
  return false;
12426
12568
  }
@@ -12656,7 +12798,7 @@ function killLiveAgents(signal2 = "SIGKILL") {
12656
12798
  let n = 0;
12657
12799
  for (const c2 of liveAgents) {
12658
12800
  try {
12659
- if (c2.kill(signal2))
12801
+ if (killHard(c2, signal2))
12660
12802
  n += 1;
12661
12803
  } catch {
12662
12804
  }
@@ -12664,6 +12806,16 @@ function killLiveAgents(signal2 = "SIGKILL") {
12664
12806
  liveAgents.clear();
12665
12807
  return n;
12666
12808
  }
12809
+ function killHard(c2, signal2) {
12810
+ if (c2.pid !== void 0) {
12811
+ try {
12812
+ process.kill(-c2.pid, signal2);
12813
+ return true;
12814
+ } catch {
12815
+ }
12816
+ }
12817
+ return c2.kill(signal2);
12818
+ }
12667
12819
  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: ";
12668
12820
  var AGENT_ARGV_TODO = {
12669
12821
  openai: "codex exec <prompt>",
@@ -12722,14 +12874,22 @@ function buildSpawnCommand(agent, opts) {
12722
12874
  const hint = AGENT_ARGV_TODO[agent] ?? "unknown agent";
12723
12875
  throw new Error(`runner: agent '${agent}' argv not yet ported. v2 shape: ${hint}`);
12724
12876
  }
12725
- function spawnAndWait(bin, args, opts) {
12877
+ function withPtyWrap(cmd, agent, platform3 = process.platform) {
12878
+ if (agent === "claude" || platform3 !== "darwin")
12879
+ return { ...cmd, pty: false };
12880
+ return { bin: "script", args: ["-q", "/dev/null", cmd.bin, ...cmd.args], pty: true };
12881
+ }
12882
+ function spawnAndWait(bin, args, opts, pty = false) {
12726
12883
  process.stderr.write(`[runner] spawn ${bin} argv=${JSON.stringify(args.map((a) => a.length > 80 ? `${a.slice(0, 77)}...` : a))}
12727
12884
  `);
12728
12885
  return new Promise((resolve3) => {
12729
12886
  const child = spawn(bin, args, {
12730
12887
  cwd: opts.cwd,
12731
12888
  env: opts.env ?? process.env,
12732
- stdio: ["ignore", "pipe", "pipe"]
12889
+ stdio: ["ignore", "pipe", "pipe"],
12890
+ // FIX-224: the PTY-wrapped `script` leads its own process group so the
12891
+ // timeout/teardown can reap script AND the agent under it (killHard).
12892
+ detached: pty
12733
12893
  });
12734
12894
  let stdout = "";
12735
12895
  let stderr = "";
@@ -12738,7 +12898,7 @@ function spawnAndWait(bin, args, opts) {
12738
12898
  if (opts.timeoutMs !== void 0 && opts.timeoutMs > 0) {
12739
12899
  timer = setTimeout(() => {
12740
12900
  timedOut = true;
12741
- child.kill("SIGKILL");
12901
+ killHard(child, "SIGKILL");
12742
12902
  }, opts.timeoutMs);
12743
12903
  }
12744
12904
  child.stdout?.on("data", (d) => {
@@ -12773,8 +12933,8 @@ function spawnAndWait(bin, args, opts) {
12773
12933
  });
12774
12934
  }
12775
12935
  var realAgentSpawn = (agent, opts) => {
12776
- const { bin, args } = buildSpawnCommand(agent, opts);
12777
- return spawnAndWait(bin, args, opts);
12936
+ const { bin, args, pty } = withPtyWrap(buildSpawnCommand(agent, opts), agent);
12937
+ return spawnAndWait(bin, args, opts, pty);
12778
12938
  };
12779
12939
 
12780
12940
  // packages/cli/dist/runner/skill-body.js
@@ -13151,7 +13311,7 @@ function feedbackCommand(args) {
13151
13311
  }
13152
13312
 
13153
13313
  // packages/cli/dist/commands/gc.js
13154
- import { existsSync as existsSync27, readdirSync as readdirSync13, rmSync as rmSync7, statSync as statSync13 } from "node:fs";
13314
+ import { existsSync as existsSync27, readdirSync as readdirSync13, rmSync as rmSync7, statSync as statSync14 } from "node:fs";
13155
13315
  import { join as join24 } from "node:path";
13156
13316
  var RUN_ID_RE2 = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/;
13157
13317
  function runsInDir2(dir) {
@@ -13166,7 +13326,7 @@ function runsInDir2(dir) {
13166
13326
  if (!e.isDirectory() || !RUN_ID_RE2.test(e.name))
13167
13327
  continue;
13168
13328
  try {
13169
- out2.push({ runId: e.name, mtimeSec: Math.floor(statSync13(join24(dir, e.name)).mtimeMs / 1e3) });
13329
+ out2.push({ runId: e.name, mtimeSec: Math.floor(statSync14(join24(dir, e.name)).mtimeMs / 1e3) });
13170
13330
  } catch {
13171
13331
  }
13172
13332
  }
@@ -13313,14 +13473,14 @@ ${label2(lang4, "ideav3.usage")}
13313
13473
  throw e;
13314
13474
  }
13315
13475
  const kindLabel = label2(lang4, plan.kind === "bug" ? "ideav3.kind_bug" : "ideav3.kind_idea");
13316
- const section = IDEA_SECTIONS[plan.kind].replace(/^#+\s*/, "");
13476
+ const section2 = IDEA_SECTIONS[plan.kind].replace(/^#+\s*/, "");
13317
13477
  process.stdout.write(`
13318
13478
  ${c("green", "\u{1F4DD} " + label2(lang4, "ideav3.recorded", plan.id))}
13319
13479
 
13320
13480
  `);
13321
13481
  process.stdout.write(` ${c("dim", label2(lang4, "ideav3.type") + ":")} ${kindLabel}
13322
13482
  `);
13323
- process.stdout.write(` ${c("dim", label2(lang4, "ideav3.section") + ":")} ${section}
13483
+ process.stdout.write(` ${c("dim", label2(lang4, "ideav3.section") + ":")} ${section2}
13324
13484
  `);
13325
13485
  process.stdout.write(` ${c("dim", label2(lang4, "ideav3.text") + ":")} ${text}
13326
13486
 
@@ -13343,109 +13503,546 @@ ${c("green", "\u{1F4DD} " + label2(lang4, "ideav3.recorded", plan.id))}
13343
13503
  }
13344
13504
 
13345
13505
  // packages/cli/dist/commands/index-gen.js
13346
- import { existsSync as existsSync29, readdirSync as readdirSync14, statSync as statSync14, writeFileSync as writeFileSync14 } from "node:fs";
13506
+ import { existsSync as existsSync29, writeFileSync as writeFileSync14 } from "node:fs";
13347
13507
  import { join as join26 } from "node:path";
13348
- function indexCommand(args) {
13349
- if (args.includes("--help") || args.includes("-h")) {
13350
- process.stdout.write("Usage: roll index\n Regenerate .roll/index.json + .roll/features/index.html\n");
13351
- return 0;
13352
- }
13353
- const cwd = process.cwd();
13354
- const stories = generateIndex(cwd);
13355
- const n = Object.keys(stories).length;
13356
- process.stdout.write(`index.json regenerated
13357
- \u7D22\u5F15\u5DF2\u91CD\u5EFA
13358
- ${n} stories mapped to epics (.roll/index.json)
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
- }
13508
+
13509
+ // packages/cli/dist/lib/dossier-css.js
13510
+ var DOSSIER_CSS = `
13511
+ /* \u2500\u2500 masthead \u2500\u2500 */
13512
+ .lede { font:16.5px/1.75 var(--serif); color:var(--fg); max-width:640px; margin:6px 0 18px; }
13513
+ .lede em { font-style:italic; color:var(--accent); }
13514
+ .masthead .crumb { font:12px/1 var(--mono); color:var(--muted); margin:0 0 14px; }
13515
+ .masthead .crumb a { color:var(--muted); }
13516
+ .kv { display:flex; gap:18px; flex-wrap:wrap; font-size:12.5px; color:var(--muted); margin:8px 0 0; }
13517
+ .kv b { color:var(--fg); font-weight:600; }
13518
+
13519
+ /* \u2500\u2500 ledger: four figures + wish\u2192truth bar \u2500\u2500 */
13520
+ .ledger { border:1px solid var(--line); border-radius:8px; background:var(--bg-raise);
13521
+ padding:16px 18px; margin:18px 0; position:relative; z-index:2; }
13522
+ .figures { display:grid; grid-template-columns:repeat(4, 1fr); gap:12px; }
13523
+ .figure .num { font:600 30px/1.1 var(--serif); letter-spacing:.01em; }
13524
+ .figure .num.truth { color:var(--pass); }
13525
+ .figure .lbl { font-size:11.5px; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); }
13526
+ .wt-bar { margin-top:14px; height:10px; border:1px solid var(--line); border-radius:999px; overflow:hidden;
13527
+ background:repeating-linear-gradient(135deg, transparent 0 4px, color-mix(in srgb, var(--muted) 28%, transparent) 4px 5px); }
13528
+ .wt-bar .truth { display:block; height:100%; background:var(--pass); border-radius:999px 0 0 999px; }
13529
+ .wt-legend { display:flex; justify-content:space-between; font-size:11.5px; color:var(--muted); margin-top:5px; }
13530
+
13531
+ /* \u2500\u2500 lifecycle spine (full) + mini-spine (rows) \u2500\u2500 */
13532
+ .spine { display:flex; align-items:center; margin:18px 0; position:relative; z-index:2; }
13533
+ .spine .node { display:flex; flex-direction:column; align-items:center; gap:6px; flex:0 0 auto; text-align:center; }
13534
+ .spine .dot { width:13px; height:13px; border-radius:50%; border:2px solid var(--line); background:var(--bg-raise); box-sizing:border-box; }
13535
+ .spine .node.done .dot { border-color:var(--accent); background:var(--accent); }
13536
+ .spine .node.truth .dot { border-color:var(--pass); background:var(--pass); }
13537
+ .spine .node .tag { font-size:10.5px; letter-spacing:.05em; text-transform:uppercase; color:var(--muted); white-space:nowrap; }
13538
+ .spine .node.done .tag, .spine .node.truth .tag { color:var(--fg); }
13539
+ .spine .seg { flex:1 1 0; height:2px; background:var(--line); margin:0 6px 18px; }
13540
+ .spine .seg.done { background:var(--accent); }
13541
+ .mini-spine { display:inline-flex; align-items:center; vertical-align:middle; }
13542
+ .mini-spine i { width:7px; height:7px; border-radius:50%; background:none; border:1.5px solid var(--line); box-sizing:border-box; }
13543
+ .mini-spine i.done { border-color:var(--accent); background:var(--accent); }
13544
+ .mini-spine i.truth { border-color:var(--pass); background:var(--pass); }
13545
+ .mini-spine s { width:8px; height:1.5px; background:var(--line); text-decoration:none; }
13546
+ .mini-spine s.done { background:var(--accent); }
13547
+
13548
+ /* \u2500\u2500 toolbar: search + only-shipping \u2500\u2500 */
13549
+ .toolbar { display:flex; gap:10px; align-items:center; margin:18px 0 8px; position:relative; z-index:2; }
13550
+ .toolbar input[type="search"] { flex:1 1 auto; font:13.5px/1 var(--sans); color:var(--fg);
13551
+ background:var(--bg-raise); border:1px solid var(--line); border-radius:999px; padding:8px 14px; outline:none; }
13552
+ .toolbar input[type="search"]:focus { border-color:var(--accent); }
13553
+ .toolbar label.only { display:flex; gap:6px; align-items:center; font-size:12.5px; color:var(--muted);
13554
+ cursor:pointer; white-space:nowrap; }
13555
+ [data-filtered="1"] .filtered-out { display:none; }
13556
+
13557
+ /* \u2500\u2500 epic cards + chips \u2500\u2500 */
13558
+ .epic-grid { display:grid; grid-template-columns:repeat(2, 1fr); gap:14px; position:relative; z-index:2; }
13559
+ .epic-card { border:1px solid var(--line); border-radius:8px; background:var(--bg-raise); padding:14px 16px; }
13560
+ .epic-card h3 { font:600 15.5px/1.3 var(--serif); margin:0 0 2px; }
13561
+ .epic-card h3 a { color:var(--fg); text-decoration:none; }
13562
+ .epic-card h3 a:hover { color:var(--accent); }
13563
+ .epic-card .stat { font-size:12px; color:var(--muted); }
13564
+ .epic-bar { height:7px; border:1px solid var(--line); border-radius:999px; overflow:hidden; margin:8px 0;
13565
+ background:repeating-linear-gradient(135deg, transparent 0 4px, color-mix(in srgb, var(--muted) 28%, transparent) 4px 5px); }
13566
+ .epic-bar .truth { display:block; height:100%; background:var(--pass); }
13567
+ .chips { display:flex; flex-wrap:wrap; gap:5px; margin-top:8px; }
13568
+ .chip { font:11px/1 var(--mono); border:1px solid var(--line); border-radius:5px; padding:4px 7px;
13569
+ color:var(--muted); text-decoration:none; }
13570
+ .chip:hover { border-color:var(--accent); color:var(--accent); }
13571
+ .chip.truth { border-color:color-mix(in srgb, var(--pass) 55%, transparent); color:var(--pass); }
13572
+
13573
+ /* \u2500\u2500 type chips + status pills \u2500\u2500 */
13574
+ .type { font:10.5px/1 var(--mono); letter-spacing:.05em; border-radius:4px; padding:3px 6px; }
13575
+ .type-US { color:var(--info); border:1px solid color-mix(in srgb, var(--info) 45%, transparent); }
13576
+ .type-FIX { color:var(--claim); border:1px solid color-mix(in srgb, var(--claim) 45%, transparent); }
13577
+ .type-REFACTOR { color:var(--block); border:1px solid color-mix(in srgb, var(--block) 45%, transparent); }
13578
+ .type-IDEA { color:var(--warn); border:1px solid color-mix(in srgb, var(--warn) 45%, transparent); }
13579
+ .pill { font-size:11px; line-height:1; border-radius:999px; padding:4px 9px; white-space:nowrap; }
13580
+ .pill.merged { color:var(--pass); border:1px solid color-mix(in srgb, var(--pass) 50%, transparent); }
13581
+ .pill.cycle { color:var(--warn); border:1px solid color-mix(in srgb, var(--warn) 50%, transparent); }
13582
+ .pill.backlog { color:var(--muted); border:1px solid var(--line); }
13583
+
13584
+ /* \u2500\u2500 story rows (epic page) \u2500\u2500 */
13585
+ .story-rows { position:relative; z-index:2; }
13586
+ .story-row { display:flex; gap:10px; align-items:center; padding:9px 6px; border-bottom:1px solid var(--line);
13587
+ text-decoration:none; color:var(--fg); }
13588
+ .story-row:hover { background:var(--bg-raise); }
13589
+ .story-row .id { font:12.5px/1 var(--mono); min-width:130px; }
13590
+ .story-row .title { flex:1 1 auto; font-size:13.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
13591
+
13592
+ /* \u2500\u2500 story dossier blocks \u2500\u2500 */
13593
+ .wish-quote { border-left:3px solid var(--accent); padding:6px 14px; margin:10px 0;
13594
+ font:15.5px/1.75 var(--serif); font-style:italic; }
13595
+ .attest-banner { display:flex; gap:12px; align-items:center; border:1px solid color-mix(in srgb, var(--pass) 45%, transparent);
13596
+ border-radius:8px; padding:10px 14px; margin:10px 0; }
13597
+ .attest-banner .mark { font-size:18px; color:var(--pass); }
13598
+ .ac-table { width:100%; border-collapse:collapse; font-size:13px; }
13599
+ .ac-table th, .ac-table td { text-align:left; padding:6px 8px; border-bottom:1px solid var(--line); vertical-align:top; }
13600
+ .ac-table th { font-family:var(--serif); color:var(--muted); letter-spacing:.04em; }
13601
+
13602
+ /* \u2500\u2500 responsive \u2500\u2500 */
13603
+ @media (max-width:680px) {
13604
+ .figures { grid-template-columns:repeat(2, 1fr); }
13605
+ .epic-grid { grid-template-columns:1fr; }
13606
+ .story-row .id { min-width:0; }
13607
+ .story-row .title { white-space:normal; }
13608
+ }
13609
+ `;
13610
+ var DOSSIER_FILTER_SCRIPT = `<script>
13611
+ (function () {
13612
+ function norm(s) { return (s || "").toLowerCase(); }
13613
+ document.addEventListener("DOMContentLoaded", function () {
13614
+ var q = document.querySelector("[data-dossier-search]");
13615
+ var only = document.querySelector("[data-dossier-only]");
13616
+ var rows = document.querySelectorAll("[data-search]");
13617
+ function apply() {
13618
+ var needle = norm(q && q.value);
13619
+ var ship = !!(only && only.checked);
13620
+ for (var i = 0; i < rows.length; i++) {
13621
+ var r = rows[i];
13622
+ var hit = !needle || norm(r.getAttribute("data-search")).indexOf(needle) !== -1;
13623
+ if (ship && r.getAttribute("data-truth") !== "1") hit = false;
13624
+ r.classList.toggle("filtered-out", !hit);
13382
13625
  }
13383
- } catch {
13626
+ document.documentElement.setAttribute("data-filtered", needle || ship ? "1" : "0");
13384
13627
  }
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>
13628
+ if (q) q.addEventListener("input", apply);
13629
+ if (only) only.addEventListener("change", apply);
13630
+ apply();
13631
+ });
13632
+ })();
13633
+ </script>`;
13634
+
13635
+ // packages/cli/dist/lib/dossier-index.js
13636
+ var SPINE_STAGES = [
13637
+ { key: "definition", en: "Definition", zh: "\u7ACB\u9879" },
13638
+ { key: "design", en: "Design", zh: "\u8BBE\u8BA1" },
13639
+ { key: "execution", en: "Execution", zh: "\u6267\u884C" },
13640
+ { key: "delivery", en: "Delivery", zh: "\u4EA4\u4ED8" },
13641
+ { key: "retrospective", en: "Retrospective", zh: "\u590D\u76D8" }
13642
+ ];
13643
+ var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13644
+ function spineMotif() {
13645
+ const nodes = SPINE_STAGES.map((s, i) => {
13646
+ const cls = s.key === "delivery" ? " truth" : "";
13647
+ const seg = i < SPINE_STAGES.length - 1 ? `<span class="seg"></span>` : "";
13648
+ return `<span class="node${cls}"><span class="dot"></span><span class="tag">${bi(s.en, s.zh)}</span></span>${seg}`;
13649
+ });
13650
+ return `<div class="spine" aria-hidden="true">${nodes.join("")}</div>`;
13651
+ }
13652
+ function ledger(epics) {
13653
+ const total = epics.reduce((n, e) => n + e.stories.length, 0);
13654
+ const truth = epics.reduce((n, e) => n + e.delivered, 0);
13655
+ const shipping = epics.filter((e) => e.delivered > 0).length;
13656
+ const pct = total > 0 ? Math.round(truth / total * 100) : 0;
13657
+ const fig = (num2, en, zh, truthy = false) => `<div class="figure"><div class="num${truthy ? " truth" : ""}">${num2}</div><div class="lbl">${bi(en, zh)}</div></div>`;
13658
+ return `<div class="ledger"><div class="figures">` + fig(String(epics.length), "Epics", "\u53F2\u8BD7") + fig(String(total), "Stories tracked", "\u5728\u518C\u6545\u4E8B") + fig(String(truth), "Merged to main", "\u5DF2\u5408\u4E3B\u5E72", true) + fig(String(shipping), "Epics shipping", "\u4EA4\u4ED8\u4E2D\u53F2\u8BD7") + `</div><div class="wt-bar" role="img" aria-label="${pct}% merged"><span class="truth" style="width:${pct}%"></span></div><div class="wt-legend"><span>${bi("wish \u2014 backlog", "\u613F\u671B \xB7 \u5F85\u529E")}</span><span>${pct}%</span><span>${bi("truth \u2014 merged", "\u4E8B\u5B9E \xB7 \u5DF2\u5408")}</span></div></div>`;
13659
+ }
13660
+ function epicCard(e) {
13661
+ const pct = e.stories.length > 0 ? Math.round(e.delivered / e.stories.length * 100) : 0;
13662
+ const chips = e.stories.map((s) => `<a class="chip${s.delivered ? " truth" : ""}" href="${encodeURIComponent(e.name)}/${encodeURIComponent(s.id)}/index.html" title="${esc2(s.title ?? s.id)}">${esc2(s.id)}</a>`).join("");
13663
+ return `<div class="epic-card" data-search="${esc2(`${e.name} ${e.stories.map((s) => `${s.id} ${s.title ?? ""}`).join(" ")}`)}" data-truth="${e.delivered > 0 ? "1" : "0"}"><h3><a href="${encodeURIComponent(e.name)}/index.html">${esc2(e.name)}</a></h3><div class="stat">${bi(`${e.delivered} / ${e.stories.length} delivered`, `${e.delivered} / ${e.stories.length} \u5DF2\u4EA4\u4ED8`)}</div><div class="epic-bar"><span class="truth" style="width:${pct}%"></span></div><div class="chips">${chips}</div></div>`;
13664
+ }
13665
+ function renderFeaturesIndex(epics) {
13666
+ const shipping = epics.filter((e) => e.delivered > 0);
13667
+ const backlog = epics.filter((e) => e.delivered === 0);
13668
+ const group = (title, zh, list) => list.length === 0 ? "" : `<h2>${bi(title, zh)}</h2>
13669
+ <div class="epic-grid">${list.map(epicCard).join("\n")}</div>
13670
+ `;
13671
+ return `<!DOCTYPE html>
13406
13672
  <html lang="zh-CN">
13407
13673
  <head>
13408
13674
  <meta charset="UTF-8">
13409
13675
  <meta name="viewport" content="width=device-width, initial-scale=1">
13410
- <title>Roll Features Index \xB7 \u529F\u80FD\u6863\u6848</title>
13676
+ <title>Roll \xB7 Delivery Dossier</title>
13411
13677
  <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); }
13678
+ ${CHROME_CSS}${DOSSIER_CSS}body { max-width:1000px; }
13418
13679
  </style>
13419
13680
  ${CHROME_SCRIPT}
13681
+ ${DOSSIER_FILTER_SCRIPT}
13420
13682
  </head>
13421
13683
  <body>
13422
13684
  ${CHROME_CONTROLS}
13685
+ <div class="masthead">
13423
13686
  <p class="kicker">Roll \xB7 ${bi("Delivery Dossier", "\u4EA4\u4ED8\u6863\u6848")}</p>
13424
13687
  <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>
13688
+ <p class="lede">${bi("The backlog is a <em>wish</em>; main is the <em>truth</em>. A story is done only when it has merged \u2014 this ledger keeps the two honest.", "\u5F85\u529E\u662F<em>\u613F\u671B</em>\uFF0C\u4E3B\u5E72\u662F<em>\u4E8B\u5B9E</em>\u3002\u6545\u4E8B\u53EA\u6709\u5408\u5165\u4E3B\u5E72\u624D\u7B97\u5B8C\u6210\u2014\u2014\u8FD9\u672C\u8D26\u8BA9\u4E24\u8005\u4E92\u76F8\u5BF9\u5F97\u4E0A\u3002")}</p>
13689
+ </div>
13690
+ ` + ledger(epics) + spineMotif() + `<div class="toolbar"><input type="search" data-dossier-search placeholder="Search \xB7 \u641C\u7D22" aria-label="search"><label class="only"><input type="checkbox" data-dossier-only>${bi("Only shipping", "\u53EA\u770B\u4EA4\u4ED8\u4E2D")}</label></div>
13691
+ ` + group("Shipping to main", "\u4EA4\u4ED8\u4E2D", shipping) + group("In backlog", "\u4ECD\u5728\u5F85\u529E", backlog) + `<footer>${bi("Generated by", "\u751F\u6210\u81EA")} <code>roll index</code></footer>
13692
+ </body>
13693
+ </html>
13694
+ `;
13695
+ }
13696
+
13697
+ // packages/cli/dist/lib/epic-page.js
13698
+ var esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13699
+ function storyState(s) {
13700
+ if (s.delivered)
13701
+ return "merged";
13702
+ if (s.inCycle === true)
13703
+ return "cycle";
13704
+ return "backlog";
13705
+ }
13706
+ function miniSpine(s) {
13707
+ const done = s.delivered ? SPINE_STAGES.length : 1 + (s.phasesDone ?? 0);
13708
+ const bits = SPINE_STAGES.map((st, i) => {
13709
+ const cls = s.delivered && st.key === "delivery" ? "truth" : i < done ? "done" : "";
13710
+ const seg = i < SPINE_STAGES.length - 1 ? `<s${i < done - 1 ? ' class="done"' : ""}></s>` : "";
13711
+ return `<i${cls !== "" ? ` class="${cls}"` : ""}></i>${seg}`;
13712
+ });
13713
+ return `<span class="mini-spine" aria-hidden="true">${bits.join("")}</span>`;
13714
+ }
13715
+ function pill(state) {
13716
+ if (state === "merged")
13717
+ return `<span class="pill merged">${bi("merged", "\u5DF2\u5408\u4E3B\u5E72")}</span>`;
13718
+ if (state === "cycle")
13719
+ return `<span class="pill cycle">${bi("in a cycle", "\u5468\u671F\u4E2D")}</span>`;
13720
+ return `<span class="pill backlog">${bi("in backlog", "\u5F85\u529E")}</span>`;
13721
+ }
13722
+ function storyRow(s) {
13723
+ const st = storyState(s);
13724
+ return `<a class="story-row" href="${encodeURIComponent(s.id)}/index.html" data-search="${esc3(`${s.id} ${s.title ?? ""}`)}" data-truth="${s.delivered ? "1" : "0"}"><span class="id">${esc3(s.id)}</span><span class="type type-${esc3(s.type)}">${esc3(s.type)}</span><span class="title">${esc3(s.title ?? "")}</span>` + miniSpine(s) + pill(st) + `</a>`;
13725
+ }
13726
+ function epicLedger(e) {
13727
+ const total = e.stories.length;
13728
+ const pct = total > 0 ? Math.round(e.delivered / total * 100) : 0;
13729
+ const byType = new Set(e.stories.map((s) => s.type)).size;
13730
+ const fig = (num2, en, zh, truthy = false) => `<div class="figure"><div class="num${truthy ? " truth" : ""}">${num2}</div><div class="lbl">${bi(en, zh)}</div></div>`;
13731
+ return `<div class="ledger"><div class="figures">` + fig(String(total), "Stories", "\u6545\u4E8B") + fig(String(e.delivered), "Merged to main", "\u5DF2\u5408\u4E3B\u5E72", true) + fig(String(total - e.delivered), "Still wish", "\u4ECD\u662F\u613F\u671B") + fig(String(byType), "Work types", "\u5DE5\u79CD") + `</div><div class="wt-bar" role="img" aria-label="${pct}% merged"><span class="truth" style="width:${pct}%"></span></div><div class="wt-legend"><span>${bi("wish \u2014 backlog", "\u613F\u671B \xB7 \u5F85\u529E")}</span><span>${pct}%</span><span>${bi("truth \u2014 merged", "\u4E8B\u5B9E \xB7 \u5DF2\u5408")}</span></div></div>`;
13732
+ }
13733
+ function renderEpicPage(e) {
13734
+ const merged = e.stories.filter((s) => storyState(s) === "merged");
13735
+ const cycle = e.stories.filter((s) => storyState(s) === "cycle");
13736
+ const backlog = e.stories.filter((s) => storyState(s) === "backlog");
13737
+ const group = (en, zh, list) => list.length === 0 ? "" : `<h2>${bi(en, zh)}</h2>
13738
+ <div class="story-rows">${list.map(storyRow).join("\n")}</div>
13739
+ `;
13740
+ return `<!DOCTYPE html>
13741
+ <html lang="zh-CN">
13742
+ <head>
13743
+ <meta charset="UTF-8">
13744
+ <meta name="viewport" content="width=device-width, initial-scale=1">
13745
+ <title>${esc3(e.name)} \xB7 Delivery Dossier</title>
13746
+ <style>
13747
+ ${CHROME_CSS}${DOSSIER_CSS}body { max-width:1000px; }
13748
+ </style>
13749
+ ${CHROME_SCRIPT}
13750
+ </head>
13751
+ <body>
13752
+ ${CHROME_CONTROLS}
13753
+ <div class="masthead">
13754
+ <p class="crumb"><a href="../index.html">${bi("Features Index", "\u529F\u80FD\u6863\u6848")}</a> / ${esc3(e.name)}</p>
13755
+ <p class="kicker">Roll \xB7 ${bi("Epic Dossier", "\u53F2\u8BD7\u6863\u6848")}</p>
13756
+ <h1>${esc3(e.name)}</h1>
13757
+ </div>
13758
+ ` + epicLedger(e) + group("Merged to main", "\u5DF2\u5408\u4E3B\u5E72", merged) + group("In a cycle", "\u5468\u671F\u4E2D", cycle) + group("In backlog", "\u4ECD\u5728\u5F85\u529E", backlog) + `<footer>${bi("Generated by", "\u751F\u6210\u81EA")} <code>roll index</code></footer>
13431
13759
  </body>
13432
13760
  </html>
13433
13761
  `;
13762
+ }
13763
+
13764
+ // packages/cli/dist/lib/story-dossier.js
13765
+ import { execFileSync as execFileSync7 } from "node:child_process";
13766
+ import { readFileSync as readFileSync23, readdirSync as readdirSync14, statSync as statSync15 } from "node:fs";
13767
+ import { join as joinPath2 } from "node:path";
13768
+ var esc4 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13769
+ function stationsDone(d) {
13770
+ const done = /* @__PURE__ */ new Set(["definition"]);
13771
+ if ((d.design?.length ?? 0) > 0)
13772
+ done.add("design");
13773
+ if ((d.commits?.length ?? 0) > 0)
13774
+ done.add("execution");
13775
+ if (d.story.delivered)
13776
+ done.add("delivery");
13777
+ if (d.retro !== void 0 && d.retro !== "")
13778
+ done.add("retrospective");
13779
+ return done;
13780
+ }
13781
+ function storySpine(d) {
13782
+ const done = stationsDone(d);
13783
+ const nodes = SPINE_STAGES.map((s, i) => {
13784
+ const cls = done.has(s.key) ? s.key === "delivery" ? " truth" : " done" : "";
13785
+ const segDone = done.has(s.key) && i < SPINE_STAGES.length - 1 && done.has(SPINE_STAGES[i + 1].key);
13786
+ const seg = i < SPINE_STAGES.length - 1 ? `<span class="seg${segDone ? " done" : ""}"></span>` : "";
13787
+ return `<span class="node${cls}"><span class="dot"></span><span class="tag">${bi(s.en, s.zh)}</span></span>${seg}`;
13788
+ });
13789
+ return `<div class="spine">${nodes.join("")}</div>`;
13790
+ }
13791
+ var AC_BADGE = {
13792
+ pass: ["\u2713 pass", "\u2713 \u901A\u8FC7"],
13793
+ readonly: ["\u25C6 readonly", "\u25C6 \u53EA\u8BFB"],
13794
+ partial: ["\u25D1 partial", "\u25D1 \u90E8\u5206"],
13795
+ fail: ["\u2717 fail", "\u2717 \u5931\u8D25"],
13796
+ blocked: ["\u25A0 blocked", "\u25A0 \u53D7\u963B"],
13797
+ claimed: ["\u25B3 claimed", "\u25B3 \u4EC5\u58F0\u79F0"],
13798
+ missing: ["\u25CB missing", "\u25CB \u7F3A\u5931"]
13799
+ };
13800
+ function section(en, zh, body, empty) {
13801
+ return `<section class="phase ${empty ? "phase-pending" : "phase-done"}"><h2>${bi(en, zh)}</h2>${body}</section>`;
13802
+ }
13803
+ function renderStoryDossier(d) {
13804
+ const s = d.story;
13805
+ const definition = d.wish !== void 0 && d.wish !== "" ? `<div class="wish-quote">${esc4(d.wish)}</div>` : `<p class="empty">${bi("No wish recorded in spec.md", "spec.md \u672A\u8BB0\u5F55\u613F\u671B")}</p>`;
13806
+ const design = (d.design?.length ?? 0) > 0 ? `<ul>${(d.design ?? []).map((b) => `<li>${esc4(b)}</li>`).join("")}</ul>` : `<p class="empty">${bi("Not yet designed", "\u5C1A\u672A\u8BBE\u8BA1")}</p>`;
13807
+ const execution = (d.commits?.length ?? 0) > 0 ? `<p>${bi(`${(d.commits ?? []).length} TCR commits`, `${(d.commits ?? []).length} \u4E2A TCR \u63D0\u4EA4`)}</p><ul class="muted">${(d.commits ?? []).map((c2) => `<li><code>${esc4(c2)}</code></li>`).join("")}</ul>` : `<p class="empty">${bi("No cycles yet", "\u6682\u65E0\u5468\u671F")}</p>`;
13808
+ const banner = s.delivered ? `<div class="attest-banner"><span class="mark">\u2713</span><div>${bi("Merged to main \u2014 attested", "\u5DF2\u5408\u4E3B\u5E72 \xB7 \u5DF2\u9A8C\u6536")}` + (d.reportHref !== void 0 ? ` \xB7 <a href="${esc4(d.reportHref)}">${bi("Attestation report", "\u9A8C\u6536\u62A5\u544A")}</a>` : "") + `</div></div>` : "";
13809
+ const acTable = (d.acRows?.length ?? 0) > 0 ? `<table class="ac-table"><thead><tr><th>AC</th><th>${bi("Status", "\u72B6\u6001")}</th><th>${bi("Note", "\u5907\u6CE8")}</th></tr></thead><tbody>` + (d.acRows ?? []).map((r) => {
13810
+ const [en, zh] = AC_BADGE[r.status] ?? [r.status, r.status];
13811
+ return `<tr><td><code>${esc4(r.ac)}</code></td><td>${bi(en, zh)}</td><td>${esc4(r.note ?? "")}</td></tr>`;
13812
+ }).join("") + `</tbody></table>` : "";
13813
+ const delivery = s.delivered || (d.acRows?.length ?? 0) > 0 ? banner + acTable : `<p class="empty">${bi("Not yet delivered", "\u5C1A\u672A\u4EA4\u4ED8")}</p>`;
13814
+ const retro = d.retro !== void 0 && d.retro !== "" ? `<p>${esc4(d.retro)}</p>` : `<p class="empty">${bi("Not yet written", "\u5C1A\u672A\u64B0\u5199")}</p>`;
13815
+ return `<!DOCTYPE html>
13816
+ <html lang="zh-CN">
13817
+ <head>
13818
+ <meta charset="UTF-8">
13819
+ <meta name="viewport" content="width=device-width, initial-scale=1">
13820
+ <title>${esc4(s.id)}${s.title !== void 0 ? ` \u2014 ${esc4(s.title)}` : ""}</title>
13821
+ <style>
13822
+ ${CHROME_CSS}${DOSSIER_CSS}.phase-done { border-left:4px solid var(--pass); } .phase-pending { border-left:4px solid var(--line); }
13823
+ </style>
13824
+ ${CHROME_SCRIPT}
13825
+ </head>
13826
+ <body>
13827
+ ${CHROME_CONTROLS}
13828
+ <div class="masthead">
13829
+ <p class="crumb"><a href="../../index.html">${bi("Features Index", "\u529F\u80FD\u6863\u6848")}</a> / <a href="../index.html">${esc4(s.epic)}</a> / ${esc4(s.id)}</p>
13830
+ <p class="kicker">Roll \xB7 ${bi("Story Dossier", "\u6545\u4E8B\u6863\u6848")}</p>
13831
+ <h1><code>${esc4(s.id)}</code></h1>
13832
+ ` + (s.title !== void 0 ? `<p class="lede">${esc4(s.title)}</p>
13833
+ ` : "") + `<div class="kv"><span class="type type-${esc4(s.type)}">${esc4(s.type)}</span><span>${bi("epic", "\u53F2\u8BD7")} <b>${esc4(s.epic)}</b></span>` + (s.created !== void 0 ? `<span>${bi("created", "\u521B\u5EFA")} <b>${esc4(s.created)}</b></span>` : "") + `</div>
13834
+ </div>
13835
+ ` + storySpine(d) + section("Definition", "\u7ACB\u9879", definition, d.wish === void 0 || d.wish === "") + section("Design", "\u8BBE\u8BA1", design, (d.design?.length ?? 0) === 0) + section("Execution", "\u6267\u884C", execution, (d.commits?.length ?? 0) === 0) + section("Delivery", "\u4EA4\u4ED8", delivery, !(s.delivered || (d.acRows?.length ?? 0) > 0)) + section("Retrospective", "\u590D\u76D8", retro, d.retro === void 0 || d.retro === "") + `<footer>Roll \xB7 <a href="spec.md">spec.md</a></footer>
13836
+ </body>
13837
+ </html>
13838
+ `;
13839
+ }
13840
+ function collectStoryDossierInput(projectPath, story) {
13841
+ const dir = joinPath2(projectPath, ".roll", "features", story.epic, story.id);
13842
+ const out2 = { story };
13843
+ try {
13844
+ const spec = readFile(joinPath2(dir, "spec.md"));
13845
+ const quote = [];
13846
+ for (const l of spec.split("\n")) {
13847
+ const m7 = /^>\s?(.*)$/.exec(l);
13848
+ if (m7 !== null)
13849
+ quote.push((m7[1] ?? "").trim());
13850
+ else if (quote.length > 0)
13851
+ break;
13852
+ }
13853
+ const wish = quote.join(" ").replace(/\s+/g, " ").trim();
13854
+ if (wish !== "")
13855
+ out2.wish = wish;
13856
+ const design = [];
13857
+ let inDesign = false;
13858
+ for (const l of spec.split("\n")) {
13859
+ if (/^#{2,3}\s*(方案|Solution|Design)/i.test(l))
13860
+ inDesign = true;
13861
+ else if (/^#{1,3}\s/.test(l))
13862
+ inDesign = false;
13863
+ else if (inDesign) {
13864
+ const m7 = /^[-*]\s+(.*)$/.exec(l.trim());
13865
+ if (m7 !== null)
13866
+ design.push((m7[1] ?? "").trim());
13867
+ }
13868
+ }
13869
+ if (design.length > 0)
13870
+ out2.design = design;
13871
+ } catch {
13872
+ }
13873
+ try {
13874
+ const rows = JSON.parse(readFile(joinPath2(dir, "ac-map.json")));
13875
+ if (Array.isArray(rows)) {
13876
+ const acRows = rows.filter((r) => typeof r.ac === "string" && typeof r.status === "string").map((r) => ({ ac: r.ac, status: r.status, ...r.note !== void 0 ? { note: r.note } : {} }));
13877
+ if (acRows.length > 0)
13878
+ out2.acRows = acRows;
13879
+ }
13880
+ } catch {
13881
+ }
13882
+ try {
13883
+ if (statDir(joinPath2(dir, "latest", `${story.id}-report.html`)))
13884
+ out2.reportHref = `latest/${story.id}-report.html`;
13885
+ } catch {
13886
+ }
13887
+ try {
13888
+ const cardNotes = joinPath2(dir, "notes");
13889
+ const legacyNotes = joinPath2(projectPath, ".roll", "notes");
13890
+ let notesDir = cardNotes;
13891
+ let notes = [];
13434
13892
  try {
13435
- writeFileSync14(join26(featuresDir, "index.html"), html, "utf8");
13436
- process.stdout.write(`features/index.html regenerated
13893
+ notes = listDir(cardNotes).filter((f) => f.endsWith(".md")).sort();
13894
+ } catch {
13895
+ }
13896
+ if (notes.length === 0) {
13897
+ notesDir = legacyNotes;
13898
+ notes = listDir(legacyNotes).filter((f) => f.includes(`-${story.id}-`) && f.endsWith(".md")).sort();
13899
+ }
13900
+ const last = notes[notes.length - 1];
13901
+ if (last !== void 0) {
13902
+ const body = readFile(joinPath2(notesDir, last));
13903
+ const score = /^score:\s*(.+)$/m.exec(body);
13904
+ const verdict = /^verdict:\s*(.+)$/m.exec(body);
13905
+ const para = body.split(/\n{2,}/).map((p) => p.trim()).find((p) => p !== "" && !p.startsWith("---") && !/^score:|^verdict:/m.test(p));
13906
+ out2.retro = [
13907
+ score !== null ? `score ${(score[1] ?? "").trim()}` : "",
13908
+ verdict !== null ? `\xB7 ${(verdict[1] ?? "").trim()}` : "",
13909
+ para !== void 0 ? `\u2014 ${para.replace(/\s+/g, " ").slice(0, 240)}` : ""
13910
+ ].join(" ").trim();
13911
+ }
13912
+ } catch {
13913
+ }
13914
+ try {
13915
+ const log = execGit(projectPath, ["log", "--format=%s", "--fixed-strings", "--grep", story.id, "--reverse"]);
13916
+ const commits = log.split("\n").map((l) => l.trim()).filter((l) => l !== "").slice(0, 40);
13917
+ if (commits.length > 0)
13918
+ out2.commits = commits;
13919
+ } catch {
13920
+ }
13921
+ return out2;
13922
+ }
13923
+ function readFile(p) {
13924
+ return readFileSync23(p, "utf8");
13925
+ }
13926
+ function listDir(p) {
13927
+ return readdirSync14(p);
13928
+ }
13929
+ function statDir(p) {
13930
+ return statSync15(p).isFile();
13931
+ }
13932
+ function execGit(cwd, args) {
13933
+ return execFileSync7("git", args, { cwd, encoding: "utf8", timeout: 1e4 });
13934
+ }
13935
+
13936
+ // packages/cli/dist/commands/index-gen.js
13937
+ function indexCommand(args) {
13938
+ if (args.includes("--help") || args.includes("-h")) {
13939
+ process.stdout.write("Usage: roll index\n Regenerate .roll/index.json + the Delivery Dossier pages\n (features/index.html, every epic page, every story dossier)\n");
13940
+ return 0;
13941
+ }
13942
+ const cwd = process.cwd();
13943
+ const stories = generateIndex(cwd);
13944
+ const n = Object.keys(stories).length;
13945
+ process.stdout.write(`index.json regenerated
13946
+ \u7D22\u5F15\u5DF2\u91CD\u5EFA
13947
+ ${n} stories mapped to epics (.roll/index.json)
13437
13948
  `);
13949
+ const featuresDir = join26(cwd, ".roll", "features");
13950
+ if (existsSync29(featuresDir)) {
13951
+ const epics = collectDossier(cwd);
13952
+ let pages = 0;
13953
+ try {
13954
+ writeFileSync14(join26(featuresDir, "index.html"), renderFeaturesIndex(epics), "utf8");
13955
+ pages += 1;
13438
13956
  } catch {
13439
13957
  }
13958
+ for (const epic of epics) {
13959
+ try {
13960
+ writeFileSync14(join26(featuresDir, epic.name, "index.html"), renderEpicPage(epic), "utf8");
13961
+ pages += 1;
13962
+ } catch {
13963
+ }
13964
+ for (const story of epic.stories) {
13965
+ try {
13966
+ writeFileSync14(join26(featuresDir, epic.name, story.id, "index.html"), renderStoryDossier(collectStoryDossierInput(cwd, story)), "utf8");
13967
+ pages += 1;
13968
+ } catch {
13969
+ }
13970
+ }
13971
+ }
13972
+ process.stdout.write(`Delivery Dossier regenerated (${pages} pages)
13973
+ \u4EA4\u4ED8\u6863\u6848\u5DF2\u91CD\u5EFA\uFF08${pages} \u9875\uFF09
13974
+ `);
13975
+ }
13976
+ return 0;
13977
+ }
13978
+
13979
+ // packages/cli/dist/commands/story-new.js
13980
+ import { existsSync as existsSync30, mkdirSync as mkdirSync14, writeFileSync as writeFileSync15 } from "node:fs";
13981
+ import { join as join27 } from "node:path";
13982
+ function todayYmd() {
13983
+ const d = /* @__PURE__ */ new Date();
13984
+ const p = (n) => String(n).padStart(2, "0");
13985
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
13986
+ }
13987
+ function flagValue(args, flag) {
13988
+ const i = args.indexOf(flag);
13989
+ if (i === -1)
13990
+ return void 0;
13991
+ return args[i + 1];
13992
+ }
13993
+ function storyNewCommand(args) {
13994
+ if (args[0] === "--help" || args[0] === "-h" || args[0] === void 0) {
13995
+ process.stdout.write("Usage: roll story new <ID> --title <text> [--epic <epic>] [--note <text>]\n Mint the card folder: features/<epic>/<ID>/spec.md + index.html, refresh index.json\n");
13996
+ return args[0] === void 0 ? 1 : 0;
13997
+ }
13998
+ const id = args[0];
13999
+ if (!STORY_ID_RE.test(id)) {
14000
+ process.stderr.write(`story new: '${id}' is not a story id (US-/FIX-/REFACTOR-/IDEA-\u2026)
14001
+ story new: '${id}' \u4E0D\u662F\u5408\u6CD5\u6545\u4E8B ID
14002
+ `);
14003
+ return 2;
14004
+ }
14005
+ const title = flagValue(args, "--title");
14006
+ if (title === void 0 || title === "") {
14007
+ process.stderr.write("story new: --title is required\nstory new: \u5FC5\u987B\u63D0\u4F9B --title\n");
14008
+ return 2;
14009
+ }
14010
+ const epic = flagValue(args, "--epic") ?? UNCATEGORIZED;
14011
+ const note = flagValue(args, "--note");
14012
+ const cwd = process.cwd();
14013
+ const dir = join27(cwd, ".roll", "features", epic, id);
14014
+ if (existsSync30(join27(dir, "spec.md"))) {
14015
+ process.stderr.write(`story new: ${epic}/${id}/spec.md already exists \u2014 cards are born once
14016
+ story new: \u5361\u5DF2\u5B58\u5728\uFF0C\u4E0D\u53EF\u8986\u76D6
14017
+ `);
14018
+ return 1;
14019
+ }
14020
+ const meta = {
14021
+ id,
14022
+ title,
14023
+ created: todayYmd(),
14024
+ ...epic !== UNCATEGORIZED ? { epic } : {},
14025
+ ...note !== void 0 && note !== "" ? { note } : {}
14026
+ };
14027
+ mkdirSync14(dir, { recursive: true });
14028
+ writeFileSync15(join27(dir, "spec.md"), renderSpecMd(meta), "utf8");
14029
+ writeFileSync15(join27(dir, "index.html"), renderStoryPage(meta), "utf8");
14030
+ try {
14031
+ generateIndex(cwd);
14032
+ } catch {
13440
14033
  }
14034
+ process.stdout.write(`card minted
14035
+ \u5361\u5DF2\u5EFA\u6863
14036
+ .roll/features/${epic}/${id}/spec.md
14037
+ `);
13441
14038
  return 0;
13442
14039
  }
13443
14040
 
13444
14041
  // packages/cli/dist/commands/init.js
13445
14042
  import { spawnSync as spawnSync5 } from "node:child_process";
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";
14043
+ import { copyFileSync as copyFileSync2, existsSync as existsSync31, mkdirSync as mkdirSync15, readFileSync as readFileSync24, realpathSync as realpathSync4, statSync as statSync16, writeFileSync as writeFileSync16 } from "node:fs";
13447
14044
  import { homedir as homedir12 } from "node:os";
13448
- import { dirname as dirname11, join as join27 } from "node:path";
14045
+ import { dirname as dirname11, join as join28 } from "node:path";
13449
14046
  function err9(line) {
13450
14047
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
13451
14048
  const RED = noColor2 ? "" : "\x1B[0;31m";
@@ -13454,43 +14051,43 @@ function err9(line) {
13454
14051
  `);
13455
14052
  }
13456
14053
  function rollHome2() {
13457
- return process.env["ROLL_HOME"] ?? join27(homedir12(), ".roll");
14054
+ return process.env["ROLL_HOME"] ?? join28(homedir12(), ".roll");
13458
14055
  }
13459
14056
  function rollGlobal2() {
13460
- return join27(rollHome2(), "conventions", "global");
14057
+ return join28(rollHome2(), "conventions", "global");
13461
14058
  }
13462
14059
  function rollTemplates2() {
13463
- return join27(rollHome2(), "conventions", "templates");
14060
+ return join28(rollHome2(), "conventions", "templates");
13464
14061
  }
13465
14062
  function scanProjectType(dir) {
13466
14063
  let hasFrontend = false;
13467
14064
  let hasBackend = false;
13468
14065
  let hasCli = false;
13469
- const pkg = join27(dir, "package.json");
14066
+ const pkg = join28(dir, "package.json");
13470
14067
  const readPkg = () => {
13471
14068
  try {
13472
- return readFileSync23(pkg, "utf8");
14069
+ return readFileSync24(pkg, "utf8");
13473
14070
  } catch {
13474
14071
  return "";
13475
14072
  }
13476
14073
  };
13477
- if (existsSync30(pkg)) {
14074
+ if (existsSync31(pkg)) {
13478
14075
  if (/"react"|"vue"|"next"|"nuxt"|"vite"|"svelte"/i.test(readPkg()))
13479
14076
  hasFrontend = true;
13480
14077
  }
13481
- if (["src", "app", "pages", "components"].some((d) => existsSync30(join27(dir, d))))
14078
+ if (["src", "app", "pages", "components"].some((d) => existsSync31(join28(dir, d))))
13482
14079
  hasFrontend = true;
13483
- if (["server", "api", "backend"].some((d) => existsSync30(join27(dir, d))))
14080
+ if (["server", "api", "backend"].some((d) => existsSync31(join28(dir, d))))
13484
14081
  hasBackend = true;
13485
- if (["go.mod", "main.go", "main.py", "app.py", "Cargo.toml", "requirements.txt", "pyproject.toml"].some((f) => existsSync30(join27(dir, f))))
14082
+ if (["go.mod", "main.go", "main.py", "app.py", "Cargo.toml", "requirements.txt", "pyproject.toml"].some((f) => existsSync31(join28(dir, f))))
13486
14083
  hasBackend = true;
13487
- if (existsSync30(pkg)) {
14084
+ if (existsSync31(pkg)) {
13488
14085
  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()))
13489
14086
  hasBackend = true;
13490
14087
  }
13491
- if (existsSync30(join27(dir, "prisma", "schema.prisma")))
14088
+ if (existsSync31(join28(dir, "prisma", "schema.prisma")))
13492
14089
  hasBackend = true;
13493
- if (existsSync30(join27(dir, "bin")) || existsSync30(join27(dir, "cmd")))
14090
+ if (existsSync31(join28(dir, "bin")) || existsSync31(join28(dir, "cmd")))
13494
14091
  hasCli = true;
13495
14092
  if (hasFrontend && hasBackend)
13496
14093
  return "fullstack";
@@ -13510,8 +14107,8 @@ function countNonEmptyFiles(dir) {
13510
14107
  }
13511
14108
  function isLegacyProject(projectDir) {
13512
14109
  for (const dir of ["src", "app", "lib", "pkg", "cmd"]) {
13513
- const p = join27(projectDir, dir);
13514
- if (existsSync30(p) && statSync15(p).isDirectory()) {
14110
+ const p = join28(projectDir, dir);
14111
+ if (existsSync31(p) && statSync16(p).isDirectory()) {
13515
14112
  if (countNonEmptyFiles(p) >= 10)
13516
14113
  return true;
13517
14114
  }
@@ -13541,12 +14138,12 @@ function isLegacyProject(projectDir) {
13541
14138
  "deno.jsonc"
13542
14139
  ];
13543
14140
  for (const man of manifests)
13544
- if (existsSync30(join27(projectDir, man)))
14141
+ if (existsSync31(join28(projectDir, man)))
13545
14142
  return true;
13546
14143
  const tf = spawnSync5("bash", ["-c", `compgen -G '${projectDir.replace(/'/g, "'\\''")}/*.tf' >/dev/null 2>&1`]);
13547
14144
  if (tf.status === 0)
13548
14145
  return true;
13549
- if (existsSync30(join27(projectDir, ".git"))) {
14146
+ if (existsSync31(join28(projectDir, ".git"))) {
13550
14147
  const g = spawnSync5("git", ["rev-parse", "--verify", "HEAD"], { cwd: projectDir, stdio: "ignore" });
13551
14148
  if (g.status === 0)
13552
14149
  return true;
@@ -13554,18 +14151,18 @@ function isLegacyProject(projectDir) {
13554
14151
  return false;
13555
14152
  }
13556
14153
  function mergeGlobalToProject(projectDir, summary) {
13557
- const src = join27(rollGlobal2(), "AGENTS.md");
13558
- const dst = join27(projectDir, "AGENTS.md");
13559
- if (!existsSync30(src)) {
14154
+ const src = join28(rollGlobal2(), "AGENTS.md");
14155
+ const dst = join28(projectDir, "AGENTS.md");
14156
+ if (!existsSync31(src)) {
13560
14157
  return;
13561
14158
  }
13562
14159
  const projectType = scanProjectType(projectDir);
13563
14160
  const skipFrontend = ["cli", "backend-service", "unknown"].includes(projectType);
13564
14161
  const FRONTEND_HEAD = "## 7. Frontend Default Stack";
13565
- const srcText = readFileSync23(src, "utf8");
14162
+ const srcText = readFileSync24(src, "utf8");
13566
14163
  const srcLines = srcText.split("\n");
13567
14164
  const lines = srcText.endsWith("\n") ? srcLines.slice(0, -1) : srcLines;
13568
- if (!existsSync30(dst)) {
14165
+ if (!existsSync31(dst)) {
13569
14166
  let out2 = "";
13570
14167
  let fcH = "";
13571
14168
  let fcB = "";
@@ -13594,11 +14191,11 @@ ${fcB}`;
13594
14191
  }
13595
14192
  }
13596
14193
  flush();
13597
- writeFileSync15(dst, out2);
14194
+ writeFileSync16(dst, out2);
13598
14195
  summary.push("created|AGENTS.md");
13599
14196
  return;
13600
14197
  }
13601
- const dstText = readFileSync23(dst, "utf8");
14198
+ const dstText = readFileSync24(dst, "utf8");
13602
14199
  let added = 0;
13603
14200
  let curH = "";
13604
14201
  let curB = "";
@@ -13628,7 +14225,7 @@ ${curB}`;
13628
14225
  }
13629
14226
  tryAppend();
13630
14227
  if (appendBuffer !== "")
13631
- writeFileSync15(dst, dstText + appendBuffer);
14228
+ writeFileSync16(dst, dstText + appendBuffer);
13632
14229
  if (added > 0)
13633
14230
  summary.push("merged|AGENTS.md");
13634
14231
  else
@@ -13636,20 +14233,20 @@ ${curB}`;
13636
14233
  }
13637
14234
  function mergeClaudeToProject(projectDir, summary) {
13638
14235
  const projectType = scanProjectType(projectDir);
13639
- const tplFile = join27(rollTemplates2(), projectType, "CLAUDE.md");
13640
- if (!existsSync30(tplFile))
14236
+ const tplFile = join28(rollTemplates2(), projectType, "CLAUDE.md");
14237
+ if (!existsSync31(tplFile))
13641
14238
  return;
13642
- const claudeDir = join27(projectDir, ".claude");
13643
- const outFile = join27(claudeDir, "CLAUDE.md");
13644
- mkdirSync14(claudeDir, { recursive: true });
13645
- if (!existsSync30(outFile)) {
14239
+ const claudeDir = join28(projectDir, ".claude");
14240
+ const outFile = join28(claudeDir, "CLAUDE.md");
14241
+ mkdirSync15(claudeDir, { recursive: true });
14242
+ if (!existsSync31(outFile)) {
13646
14243
  copyFileSync2(tplFile, outFile);
13647
14244
  summary.push("created|.claude/CLAUDE.md");
13648
14245
  return;
13649
14246
  }
13650
- const tplText = readFileSync23(tplFile, "utf8");
14247
+ const tplText = readFileSync24(tplFile, "utf8");
13651
14248
  const lines = tplText.endsWith("\n") ? tplText.split("\n").slice(0, -1) : tplText.split("\n");
13652
- const outText = readFileSync23(outFile, "utf8");
14249
+ const outText = readFileSync24(outFile, "utf8");
13653
14250
  let added = 0;
13654
14251
  let curH = "";
13655
14252
  let curB = "";
@@ -13674,7 +14271,7 @@ ${curB}`;
13674
14271
  }
13675
14272
  tryAppend();
13676
14273
  if (appendBuffer !== "")
13677
- writeFileSync15(outFile, outText + appendBuffer);
14274
+ writeFileSync16(outFile, outText + appendBuffer);
13678
14275
  if (added > 0)
13679
14276
  summary.push("merged|.claude/CLAUDE.md");
13680
14277
  else
@@ -13691,20 +14288,20 @@ var BACKLOG_TEMPLATE = `# Project Backlog
13691
14288
  |----|---------|--------|
13692
14289
  `;
13693
14290
  function writeBacklog(path, summary) {
13694
- if (existsSync30(path)) {
14291
+ if (existsSync31(path)) {
13695
14292
  summary.push("unchanged|.roll/backlog.md");
13696
14293
  return;
13697
14294
  }
13698
- mkdirSync14(dirname11(path), { recursive: true });
13699
- writeFileSync15(path, BACKLOG_TEMPLATE);
14295
+ mkdirSync15(dirname11(path), { recursive: true });
14296
+ writeFileSync16(path, BACKLOG_TEMPLATE);
13700
14297
  summary.push("created|.roll/backlog.md");
13701
14298
  }
13702
14299
  function ensureFeaturesDir(path, summary) {
13703
- if (existsSync30(path) && statSync15(path).isDirectory()) {
14300
+ if (existsSync31(path) && statSync16(path).isDirectory()) {
13704
14301
  summary.push("unchanged|.roll/features/");
13705
14302
  return;
13706
14303
  }
13707
- mkdirSync14(path, { recursive: true });
14304
+ mkdirSync15(path, { recursive: true });
13708
14305
  summary.push("created|.roll/features/");
13709
14306
  }
13710
14307
  var FEATURES_TEMPLATE = `# Features
@@ -13718,38 +14315,38 @@ var FEATURES_TEMPLATE = `# Features
13718
14315
  <!-- Add feature entries here as epics are completed -->
13719
14316
  `;
13720
14317
  function writeFeaturesMd(path, summary) {
13721
- if (existsSync30(path)) {
14318
+ if (existsSync31(path)) {
13722
14319
  summary.push("unchanged|.roll/features.md");
13723
14320
  return;
13724
14321
  }
13725
- mkdirSync14(dirname11(path), { recursive: true });
13726
- writeFileSync15(path, FEATURES_TEMPLATE);
14322
+ mkdirSync15(dirname11(path), { recursive: true });
14323
+ writeFileSync16(path, FEATURES_TEMPLATE);
13727
14324
  summary.push("created|.roll/features.md");
13728
14325
  }
13729
14326
  function initSeedAgentRoutes(templateName, projectDir, summary) {
13730
- const dest = join27(projectDir, ".roll", "agent-routes.yaml");
13731
- if (existsSync30(dest)) {
14327
+ const dest = join28(projectDir, ".roll", "agent-routes.yaml");
14328
+ if (existsSync31(dest)) {
13732
14329
  summary.push("unchanged|.roll/agent-routes.yaml");
13733
14330
  return 0;
13734
14331
  }
13735
- const src = join27(rollTemplates2(), "agent-routes", `${templateName}.yaml`);
13736
- if (!existsSync30(src)) {
14332
+ const src = join28(rollTemplates2(), "agent-routes", `${templateName}.yaml`);
14333
+ if (!existsSync31(src)) {
13737
14334
  return 1;
13738
14335
  }
13739
- mkdirSync14(dirname11(dest), { recursive: true });
14336
+ mkdirSync15(dirname11(dest), { recursive: true });
13740
14337
  copyFileSync2(src, dest);
13741
14338
  summary.push("created|.roll/agent-routes.yaml");
13742
14339
  return 0;
13743
14340
  }
13744
14341
  function writeVersionStamp(projectDir, summary) {
13745
- const stampPath = join27(projectDir, ".roll", ".version");
13746
- if (existsSync30(stampPath)) {
14342
+ const stampPath = join28(projectDir, ".roll", ".version");
14343
+ if (existsSync31(stampPath)) {
13747
14344
  summary.push("unchanged|.roll/.version");
13748
14345
  return;
13749
14346
  }
13750
- mkdirSync14(join27(projectDir, ".roll"), { recursive: true });
14347
+ mkdirSync15(join28(projectDir, ".roll"), { recursive: true });
13751
14348
  const installedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
13752
- writeFileSync15(stampPath, `# Roll project version stamp \u2014 written by \`roll init\` (US-ONBOARD-019).
14349
+ writeFileSync16(stampPath, `# Roll project version stamp \u2014 written by \`roll init\` (US-ONBOARD-019).
13753
14350
  # Used by \`_check_structure\` to recognise a previously-onboarded Roll project
13754
14351
  # without depending on directory-name heuristics.
13755
14352
  roll_version: "${rollVersion() || "unknown"}"
@@ -13887,7 +14484,7 @@ function initCommand(args) {
13887
14484
  return null;
13888
14485
  if (args[0] !== void 0 && args[0].startsWith("-"))
13889
14486
  return null;
13890
- if (!existsSync30(rollTemplates2()))
14487
+ if (!existsSync31(rollTemplates2()))
13891
14488
  return null;
13892
14489
  let projectDir;
13893
14490
  try {
@@ -13897,16 +14494,16 @@ function initCommand(args) {
13897
14494
  }
13898
14495
  let hasAgents = false;
13899
14496
  const summary = [];
13900
- if (existsSync30(join27(projectDir, "AGENTS.md"))) {
14497
+ if (existsSync31(join28(projectDir, "AGENTS.md"))) {
13901
14498
  hasAgents = true;
13902
14499
  } else if (isLegacyProject(projectDir)) {
13903
14500
  return null;
13904
14501
  }
13905
14502
  mergeGlobalToProject(projectDir, summary);
13906
14503
  mergeClaudeToProject(projectDir, summary);
13907
- writeBacklog(join27(projectDir, ".roll", "backlog.md"), summary);
13908
- ensureFeaturesDir(join27(projectDir, ".roll", "features"), summary);
13909
- writeFeaturesMd(join27(projectDir, ".roll", "features.md"), summary);
14504
+ writeBacklog(join28(projectDir, ".roll", "backlog.md"), summary);
14505
+ ensureFeaturesDir(join28(projectDir, ".roll", "features"), summary);
14506
+ writeFeaturesMd(join28(projectDir, ".roll", "features.md"), summary);
13910
14507
  const routesTemplate = process.env["ROLL_AGENT_ROUTES_TEMPLATE"] ?? "default";
13911
14508
  initSeedAgentRoutes(routesTemplate, projectDir, summary);
13912
14509
  writeVersionStamp(projectDir, summary);
@@ -13919,19 +14516,19 @@ function initCommand(args) {
13919
14516
  }
13920
14517
 
13921
14518
  // packages/cli/dist/commands/lang.js
13922
- import { execFileSync as execFileSync7 } from "node:child_process";
13923
- import { existsSync as existsSync31, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync3, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync16 } from "node:fs";
14519
+ import { execFileSync as execFileSync8 } from "node:child_process";
14520
+ import { existsSync as existsSync32, mkdirSync as mkdirSync16, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, renameSync as renameSync2, writeFileSync as writeFileSync17 } from "node:fs";
13924
14521
  import { homedir as homedir13, tmpdir as tmpdir3 } from "node:os";
13925
- import { dirname as dirname12, join as join28 } from "node:path";
14522
+ import { dirname as dirname12, join as join29 } from "node:path";
13926
14523
  function rollConfigPath4() {
13927
- const rollHome4 = process.env["ROLL_HOME"] ?? join28(homedir13(), ".roll");
13928
- return join28(rollHome4, "config.yaml");
14524
+ const rollHome4 = process.env["ROLL_HOME"] ?? join29(homedir13(), ".roll");
14525
+ return join29(rollHome4, "config.yaml");
13929
14526
  }
13930
14527
  function configLang2() {
13931
14528
  const cfg = rollConfigPath4();
13932
- if (!existsSync31(cfg))
14529
+ if (!existsSync32(cfg))
13933
14530
  return void 0;
13934
- for (const line of readFileSync24(cfg, "utf8").split("\n")) {
14531
+ for (const line of readFileSync25(cfg, "utf8").split("\n")) {
13935
14532
  const m7 = /^lang:\s*(.*)$/.exec(line);
13936
14533
  if (m7 !== null) {
13937
14534
  const v = (m7[1] ?? "").replace(/\s*#.*$/, "").trim();
@@ -13943,15 +14540,15 @@ function configLang2() {
13943
14540
  }
13944
14541
  function configHasLangLine() {
13945
14542
  const cfg = rollConfigPath4();
13946
- if (!existsSync31(cfg))
14543
+ if (!existsSync32(cfg))
13947
14544
  return false;
13948
- return readFileSync24(cfg, "utf8").split("\n").some((l) => /^lang:/.test(l));
14545
+ return readFileSync25(cfg, "utf8").split("\n").some((l) => /^lang:/.test(l));
13949
14546
  }
13950
14547
  function appleLang2() {
13951
14548
  if (process.platform !== "darwin")
13952
14549
  return void 0;
13953
14550
  try {
13954
- const out2 = execFileSync7("defaults", ["read", "-g", "AppleLanguages"], {
14551
+ const out2 = execFileSync8("defaults", ["read", "-g", "AppleLanguages"], {
13955
14552
  encoding: "utf8",
13956
14553
  stdio: ["ignore", "pipe", "ignore"]
13957
14554
  });
@@ -13979,7 +14576,7 @@ function resolveSource() {
13979
14576
  const env = process.env;
13980
14577
  if ((env["ROLL_LANG"] ?? "") !== "")
13981
14578
  return "ROLL_LANG env";
13982
- if (existsSync31(rollConfigPath4()) && configHasLangLine())
14579
+ if (existsSync32(rollConfigPath4()) && configHasLangLine())
13983
14580
  return `config (${rollConfigPath4()})`;
13984
14581
  if ((env["LC_ALL"] ?? "") !== "" || (env["LANG"] ?? "") !== "")
13985
14582
  return "LC_ALL/LANG";
@@ -14004,28 +14601,28 @@ function err10(line) {
14004
14601
  }
14005
14602
  function writeLang(value) {
14006
14603
  const cfg = rollConfigPath4();
14007
- mkdirSync15(dirname12(cfg), { recursive: true });
14008
- const existing = existsSync31(cfg) ? readFileSync24(cfg, "utf8") : "";
14604
+ mkdirSync16(dirname12(cfg), { recursive: true });
14605
+ const existing = existsSync32(cfg) ? readFileSync25(cfg, "utf8") : "";
14009
14606
  const kept = existing === "" ? [] : existing.split("\n").filter((l) => !/^lang:/.test(l));
14010
14607
  if (kept.length > 0 && kept[kept.length - 1] === "")
14011
14608
  kept.pop();
14012
14609
  const body = kept.length > 0 ? kept.join("\n") + "\n" : "";
14013
- const tmp = join28(mkdtempSync3(join28(tmpdir3(), "roll-lang-")), "config.yaml");
14014
- writeFileSync16(tmp, `${body}lang: ${value}
14610
+ const tmp = join29(mkdtempSync3(join29(tmpdir3(), "roll-lang-")), "config.yaml");
14611
+ writeFileSync17(tmp, `${body}lang: ${value}
14015
14612
  `);
14016
14613
  renameSync2(tmp, cfg);
14017
14614
  }
14018
14615
  function clearLang() {
14019
14616
  const cfg = rollConfigPath4();
14020
- if (!existsSync31(cfg))
14617
+ if (!existsSync32(cfg))
14021
14618
  return;
14022
- const existing = readFileSync24(cfg, "utf8");
14619
+ const existing = readFileSync25(cfg, "utf8");
14023
14620
  const kept = existing.split("\n").filter((l) => !/^lang:/.test(l));
14024
14621
  if (kept.length > 0 && kept[kept.length - 1] === "")
14025
14622
  kept.pop();
14026
14623
  const body = kept.length > 0 ? kept.join("\n") + "\n" : "";
14027
- const tmp = join28(mkdtempSync3(join28(tmpdir3(), "roll-lang-")), "config.yaml");
14028
- writeFileSync16(tmp, body);
14624
+ const tmp = join29(mkdtempSync3(join29(tmpdir3(), "roll-lang-")), "config.yaml");
14625
+ writeFileSync17(tmp, body);
14029
14626
  renameSync2(tmp, cfg);
14030
14627
  }
14031
14628
  function langCommand(args) {
@@ -14125,9 +14722,9 @@ async function loopFmtCommand(args) {
14125
14722
 
14126
14723
  // packages/cli/dist/commands/loop-pr-inbox.js
14127
14724
  import { spawn as spawn2 } from "node:child_process";
14128
- import { appendFileSync as appendFileSync5, existsSync as existsSync32, mkdirSync as mkdirSync16, readFileSync as readFileSync25, writeFileSync as writeFileSync17 } from "node:fs";
14725
+ import { appendFileSync as appendFileSync5, existsSync as existsSync33, mkdirSync as mkdirSync17, readFileSync as readFileSync26, writeFileSync as writeFileSync18 } from "node:fs";
14129
14726
  import { homedir as homedir14 } from "node:os";
14130
- import { dirname as dirname13, join as join29 } from "node:path";
14727
+ import { dirname as dirname13, join as join30 } from "node:path";
14131
14728
  function reducePrView(raw) {
14132
14729
  const reviews = raw.reviews ?? [];
14133
14730
  const botReviews = reviews.filter((r) => r.authorAssociation === "BOT" || r.authorAssociation === "APP");
@@ -14210,7 +14807,7 @@ function runtimeDir() {
14210
14807
  const override = (process.env["ROLL_PROJECT_RUNTIME_DIR"] ?? "").trim();
14211
14808
  if (override !== "")
14212
14809
  return override;
14213
- return join29(process.cwd(), ".roll", "loop");
14810
+ return join30(process.cwd(), ".roll", "loop");
14214
14811
  }
14215
14812
  function projSlug() {
14216
14813
  const override = (process.env["ROLL_MAIN_SLUG"] ?? "").trim();
@@ -14219,26 +14816,26 @@ function projSlug() {
14219
14816
  return process.cwd().split("/").filter(Boolean).pop() ?? "default";
14220
14817
  }
14221
14818
  function alertPath() {
14222
- return join29(runtimeDir(), `ALERT-${projSlug()}.md`);
14819
+ return join30(runtimeDir(), `ALERT-${projSlug()}.md`);
14223
14820
  }
14224
14821
  function statePath() {
14225
- return join29(runtimeDir(), `state-${projSlug()}.yaml`);
14822
+ return join30(runtimeDir(), `state-${projSlug()}.yaml`);
14226
14823
  }
14227
14824
  function tickPath() {
14228
- return join29(runtimeDir(), "pr-tick.jsonl");
14825
+ return join30(runtimeDir(), "pr-tick.jsonl");
14229
14826
  }
14230
14827
  function engineBin() {
14231
14828
  let dir = dirname13(new URL(import.meta.url).pathname);
14232
14829
  for (let i = 0; i < 10; i++) {
14233
- const candidate = join29(dir, "bin", "roll");
14234
- if (existsSync32(candidate))
14830
+ const candidate = join30(dir, "bin", "roll");
14831
+ if (existsSync33(candidate))
14235
14832
  return candidate;
14236
14833
  const parent = dirname13(dir);
14237
14834
  if (parent === dir)
14238
14835
  break;
14239
14836
  dir = parent;
14240
14837
  }
14241
- return join29(homedir14(), ".local", "lib", "roll", "bin", "roll");
14838
+ return join30(homedir14(), ".local", "lib", "roll", "bin", "roll");
14242
14839
  }
14243
14840
  function pal2() {
14244
14841
  return (process.env["NO_COLOR"] ?? "") !== "" ? { yellow: "", nc: "" } : { yellow: "\x1B[0;33m", nc: "\x1B[0m" };
@@ -14248,21 +14845,21 @@ function nowSec() {
14248
14845
  }
14249
14846
  function writeTickFile(tick) {
14250
14847
  const file = tickPath();
14251
- mkdirSync16(dirname13(file), { recursive: true });
14848
+ mkdirSync17(dirname13(file), { recursive: true });
14252
14849
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
14253
14850
  appendFileSync5(file, `${JSON.stringify({ ts, ...tick })}
14254
14851
  `);
14255
14852
  try {
14256
- const lines = readFileSync25(file, "utf8").split("\n").filter((l) => l !== "");
14853
+ const lines = readFileSync26(file, "utf8").split("\n").filter((l) => l !== "");
14257
14854
  if (lines.length > 500)
14258
- writeFileSync17(file, `${lines.slice(-500).join("\n")}
14855
+ writeFileSync18(file, `${lines.slice(-500).join("\n")}
14259
14856
  `);
14260
14857
  } catch {
14261
14858
  }
14262
14859
  }
14263
14860
  function appendAlert(line) {
14264
14861
  const file = alertPath();
14265
- mkdirSync16(dirname13(file), { recursive: true });
14862
+ mkdirSync17(dirname13(file), { recursive: true });
14266
14863
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
14267
14864
  appendFileSync5(file, `[${ts}] ${line}
14268
14865
  `);
@@ -14271,16 +14868,16 @@ function rebaseCircuitAllowed(num2) {
14271
14868
  const state = statePath();
14272
14869
  let body = "";
14273
14870
  try {
14274
- body = readFileSync25(state, "utf8");
14871
+ body = readFileSync26(state, "utf8");
14275
14872
  } catch {
14276
14873
  }
14277
14874
  const verdict = rebaseCircuitVerdict(parseRebaseAttempts(body, num2), nowSec());
14278
14875
  writeRebaseAttempts(state, num2, verdict.freshTimestamps);
14279
14876
  if (!verdict.allowed) {
14280
14877
  const file = alertPath();
14281
- mkdirSync16(dirname13(file), { recursive: true });
14878
+ mkdirSync17(dirname13(file), { recursive: true });
14282
14879
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
14283
- writeFileSync17(file, [
14880
+ writeFileSync18(file, [
14284
14881
  `# ALERT \u2014 PR rebase circuit breaker tripped`,
14285
14882
  ``,
14286
14883
  `**Time**: ${stamp}`,
@@ -14334,13 +14931,13 @@ function upsertRebaseAttempts(stateBody, pr, value) {
14334
14931
  `;
14335
14932
  }
14336
14933
  function writeRebaseAttempts(state, pr, timestamps) {
14337
- mkdirSync16(dirname13(state), { recursive: true });
14934
+ mkdirSync17(dirname13(state), { recursive: true });
14338
14935
  let body = "";
14339
14936
  try {
14340
- body = readFileSync25(state, "utf8");
14937
+ body = readFileSync26(state, "utf8");
14341
14938
  } catch {
14342
14939
  }
14343
- writeFileSync17(state, upsertRebaseAttempts(body, pr, renderRebaseAttempts(timestamps)));
14940
+ writeFileSync18(state, upsertRebaseAttempts(body, pr, renderRebaseAttempts(timestamps)));
14344
14941
  }
14345
14942
  function bridgeBash(args) {
14346
14943
  return new Promise((resolve3) => {
@@ -14427,19 +15024,19 @@ async function loopPrInboxCommand(_args, deps = realDeps2()) {
14427
15024
  }
14428
15025
 
14429
15026
  // packages/cli/dist/commands/loop-run-once.js
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";
15027
+ import { appendFileSync as appendFileSync7, existsSync as existsSync37, mkdirSync as mkdirSync19, readFileSync as readFileSync29, writeFileSync as writeFileSync20 } from "node:fs";
15028
+ import { dirname as dirname15, join as join34 } from "node:path";
14432
15029
 
14433
15030
  // packages/cli/dist/runner/executor.js
14434
15031
  import { execFile as execFile8 } from "node:child_process";
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";
15032
+ import { appendFileSync as appendFileSync6, existsSync as existsSync36, lstatSync as lstatSync2, mkdirSync as mkdirSync18, readFileSync as readFileSync28, rmSync as rmSync8, symlinkSync as symlinkSync4, unlinkSync, writeFileSync as writeFileSync19 } from "node:fs";
15033
+ import { dirname as dirname14, join as join33 } from "node:path";
14437
15034
  import { promisify as promisify8 } from "node:util";
14438
15035
 
14439
15036
  // packages/cli/dist/runner/peer-gate.js
14440
15037
  import { execFile as execFile7 } from "node:child_process";
14441
- import { existsSync as existsSync33, readdirSync as readdirSync15 } from "node:fs";
14442
- import { join as join30 } from "node:path";
15038
+ import { existsSync as existsSync34, readdirSync as readdirSync15 } from "node:fs";
15039
+ import { join as join31 } from "node:path";
14443
15040
  import { promisify as promisify7 } from "node:util";
14444
15041
  var execFileAsync7 = promisify7(execFile7);
14445
15042
  var HIGH_RISK = [/^\.github\/workflows\//, /^packages\/infra\/src\/(git|github|process)\.ts$/];
@@ -14469,8 +15066,8 @@ async function cycleChangedFiles(worktreeCwd) {
14469
15066
  }
14470
15067
  }
14471
15068
  function peerEvidencePresent(runtimeDir3, cycleId) {
14472
- const dir = join30(runtimeDir3, "peer");
14473
- if (!existsSync33(dir))
15069
+ const dir = join31(runtimeDir3, "peer");
15070
+ if (!existsSync34(dir))
14474
15071
  return false;
14475
15072
  try {
14476
15073
  return readdirSync15(dir).some((f) => f.startsWith(`cycle-${cycleId}.`));
@@ -14497,24 +15094,18 @@ async function runPeerGate(worktreeCwd, runtimeDir3, cycleId, sinks) {
14497
15094
  }
14498
15095
 
14499
15096
  // packages/cli/dist/runner/attest-gate.js
14500
- import { existsSync as existsSync34, readFileSync as readFileSync26, statSync as statSync16 } from "node:fs";
14501
- import { join as join31 } from "node:path";
15097
+ import { existsSync as existsSync35, readFileSync as readFileSync27, statSync as statSync17 } from "node:fs";
15098
+ import { join as join32 } from "node:path";
14502
15099
  function reportCandidates(worktreeCwd, storyId) {
14503
- return [
14504
- join31(cardArchiveDir(worktreeCwd, storyId), "latest", reportFileName(storyId)),
14505
- join31(legacyArchiveDir(worktreeCwd, storyId), "latest", "report.html")
14506
- ];
15100
+ return [join32(cardArchiveDir(worktreeCwd, storyId), "latest", reportFileName(storyId))];
14507
15101
  }
14508
15102
  function acMapCandidates(worktreeCwd, storyId) {
14509
- return [
14510
- join31(cardArchiveDir(worktreeCwd, storyId), "ac-map.json"),
14511
- join31(legacyArchiveDir(worktreeCwd, storyId), "ac-map.json")
14512
- ];
15103
+ return [join32(cardArchiveDir(worktreeCwd, storyId), "ac-map.json")];
14513
15104
  }
14514
15105
  function existingReport(worktreeCwd, storyId) {
14515
15106
  for (const p of reportCandidates(worktreeCwd, storyId)) {
14516
15107
  try {
14517
- if (statSync16(p).isFile())
15108
+ if (statSync17(p).isFile())
14518
15109
  return p;
14519
15110
  } catch {
14520
15111
  }
@@ -14528,7 +15119,7 @@ function verificationReportFresh(worktreeCwd, storyId, sinceSec) {
14528
15119
  if (p === null)
14529
15120
  return false;
14530
15121
  try {
14531
- const st = statSync16(p);
15122
+ const st = statSync17(p);
14532
15123
  if (sinceSec === void 0)
14533
15124
  return true;
14534
15125
  return st.mtimeMs / 1e3 >= sinceSec;
@@ -14543,9 +15134,9 @@ function verificationReportHasContent(worktreeCwd, storyId) {
14543
15134
  if (p === null)
14544
15135
  return false;
14545
15136
  try {
14546
- const html = readFileSync26(p, "utf8");
15137
+ const html = readFileSync27(p, "utf8");
14547
15138
  const hasAc = /<section class="ac[\s">]/.test(html);
14548
- const hasMap = acMapCandidates(worktreeCwd, storyId).some((m7) => existsSync34(m7));
15139
+ const hasMap = acMapCandidates(worktreeCwd, storyId).some((m7) => existsSync35(m7));
14549
15140
  return hasAc && hasMap;
14550
15141
  } catch {
14551
15142
  return false;
@@ -14553,10 +15144,10 @@ function verificationReportHasContent(worktreeCwd, storyId) {
14553
15144
  }
14554
15145
  function readAttestGateMode(repoCwd) {
14555
15146
  try {
14556
- const p = join31(repoCwd, ".roll", "policy.yaml");
14557
- if (!existsSync34(p))
15147
+ const p = join32(repoCwd, ".roll", "policy.yaml");
15148
+ if (!existsSync35(p))
14558
15149
  return "soft";
14559
- return parsePolicy(readFileSync26(p, "utf8")).loopSafety.attestGate === "hard" ? "hard" : "soft";
15150
+ return parsePolicy(readFileSync27(p, "utf8")).loopSafety.attestGate === "hard" ? "hard" : "soft";
14560
15151
  } catch {
14561
15152
  return "soft";
14562
15153
  }
@@ -14664,9 +15255,9 @@ async function executeCommand(cmd, ports, ctx) {
14664
15255
  // execute: spawn the agent (TCR commits happen inside the worktree). The
14665
15256
  // exit code + timeout feed back as agent_exited; usage is captured for cost.
14666
15257
  case "spawn_agent": {
14667
- const livePath = join32(dirname14(ports.paths.eventsPath), "live.log");
15258
+ const livePath = join33(dirname14(ports.paths.eventsPath), "live.log");
14668
15259
  try {
14669
- writeFileSync18(livePath, `\u2500\u2500 cycle ${ctx.cycleId ?? "?"} \xB7 ${ctx.storyId ?? "?"} \xB7 agent ${cmd.agent} \u2500\u2500
15260
+ writeFileSync19(livePath, `\u2500\u2500 cycle ${ctx.cycleId ?? "?"} \xB7 ${ctx.storyId ?? "?"} \xB7 agent ${cmd.agent} \u2500\u2500
14670
15261
  `);
14671
15262
  } catch {
14672
15263
  }
@@ -14684,9 +15275,9 @@ async function executeCommand(cmd, ports, ctx) {
14684
15275
  }
14685
15276
  });
14686
15277
  try {
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}
15278
+ const logDir = join33(dirname14(ports.paths.eventsPath), "cycle-logs");
15279
+ mkdirSync18(logDir, { recursive: true });
15280
+ writeFileSync19(join33(logDir, `${ctx.cycleId ?? "cycle"}.agent.log`), `# exit=${res.exitCode} timedOut=${res.timedOut}
14690
15281
  --- stdout ---
14691
15282
  ${res.stdout}
14692
15283
  --- stderr ---
@@ -14809,7 +15400,7 @@ ${res.stderr}
14809
15400
  // _worktree_cleanup (tolerant). Side effect; no feedback (terminal path).
14810
15401
  case "cleanup_worktree":
14811
15402
  try {
14812
- const dst = join32(ports.paths.worktreePath, ".roll");
15403
+ const dst = join33(ports.paths.worktreePath, ".roll");
14813
15404
  if (lstatSync2(dst, { throwIfNoEntry: false })?.isSymbolicLink() === true)
14814
15405
  unlinkSync(dst);
14815
15406
  } catch {
@@ -14881,9 +15472,9 @@ function buildRunRow(cmd, ctx, nowSec2) {
14881
15472
  }
14882
15473
  function readRunsRows(runsPath) {
14883
15474
  try {
14884
- if (!existsSync35(runsPath))
15475
+ if (!existsSync36(runsPath))
14885
15476
  return [];
14886
- return readFileSync27(runsPath, "utf8").split("\n").filter((l) => l.trim() !== "").map((l) => {
15477
+ return readFileSync28(runsPath, "utf8").split("\n").filter((l) => l.trim() !== "").map((l) => {
14887
15478
  try {
14888
15479
  return JSON.parse(l);
14889
15480
  } catch {
@@ -14906,15 +15497,15 @@ function sleep(ms) {
14906
15497
  }
14907
15498
  async function linkRollIntoWorktree(repoCwd, worktreePath) {
14908
15499
  try {
14909
- const src = join32(repoCwd, ".roll");
14910
- const dst = join32(worktreePath, ".roll");
14911
- if (!existsSync35(src))
15500
+ const src = join33(repoCwd, ".roll");
15501
+ const dst = join33(worktreePath, ".roll");
15502
+ if (!existsSync36(src))
14912
15503
  return;
14913
15504
  const dstStat = lstatSync2(dst, { throwIfNoEntry: false });
14914
15505
  if (dstStat) {
14915
15506
  if (dstStat.isSymbolicLink())
14916
15507
  return;
14917
- const incompleteFossil = existsSync35(join32(src, "backlog.md")) && !existsSync35(join32(dst, "backlog.md"));
15508
+ const incompleteFossil = existsSync36(join33(src, "backlog.md")) && !existsSync36(join33(dst, "backlog.md"));
14918
15509
  if (!incompleteFossil)
14919
15510
  return;
14920
15511
  rmSync8(dst, { recursive: true, force: true });
@@ -14923,10 +15514,10 @@ async function linkRollIntoWorktree(repoCwd, worktreePath) {
14923
15514
  const common = (await execFileAsync8("git", ["-C", repoCwd, "rev-parse", "--path-format=absolute", "--git-common-dir"])).stdout.trim();
14924
15515
  if (common === "")
14925
15516
  return;
14926
- const exclude = join32(common, "info", "exclude");
14927
- const cur = existsSync35(exclude) ? readFileSync27(exclude, "utf8") : "";
15517
+ const exclude = join33(common, "info", "exclude");
15518
+ const cur = existsSync36(exclude) ? readFileSync28(exclude, "utf8") : "";
14928
15519
  if (!/^\.roll$/m.test(cur)) {
14929
- mkdirSync17(dirname14(exclude), { recursive: true });
15520
+ mkdirSync18(dirname14(exclude), { recursive: true });
14930
15521
  appendFileSync6(exclude, `${cur === "" || cur.endsWith("\n") ? "" : "\n"}.roll
14931
15522
  `, "utf8");
14932
15523
  }
@@ -15015,10 +15606,10 @@ function nodePorts(opts) {
15015
15606
  },
15016
15607
  backlog: {
15017
15608
  read(projectCwd) {
15018
- const p = join32(projectCwd, ".roll", "backlog.md");
15019
- if (!existsSync35(p))
15609
+ const p = join33(projectCwd, ".roll", "backlog.md");
15610
+ if (!existsSync36(p))
15020
15611
  return [];
15021
- return parseBacklog(readFileSync27(p, "utf8"));
15612
+ return parseBacklog(readFileSync28(p, "utf8"));
15022
15613
  },
15023
15614
  // FIX-198: the production binding was MISSING entirely (the optional
15024
15615
  // chain made every In-Progress claim a silent no-op). ID-anchored mark
@@ -15026,8 +15617,8 @@ function nodePorts(opts) {
15026
15617
  // never kill the cycle, the reconcile pass is the safety net.
15027
15618
  markStatus(projectCwd, id, status2) {
15028
15619
  try {
15029
- const p = join32(projectCwd, ".roll", "backlog.md");
15030
- if (!existsSync35(p))
15620
+ const p = join33(projectCwd, ".roll", "backlog.md");
15621
+ if (!existsSync36(p))
15031
15622
  return;
15032
15623
  const store = new BacklogStore();
15033
15624
  const snap = store.readBacklog(p);
@@ -15047,7 +15638,7 @@ function nodePorts(opts) {
15047
15638
  };
15048
15639
  }
15049
15640
  function appendAlertLine(alertsPath, message) {
15050
- mkdirSync17(dirname14(alertsPath), { recursive: true });
15641
+ mkdirSync18(dirname14(alertsPath), { recursive: true });
15051
15642
  appendFileSync6(alertsPath, `${message}
15052
15643
  `, "utf8");
15053
15644
  }
@@ -15224,6 +15815,7 @@ function describeCommand(cmd) {
15224
15815
 
15225
15816
  // packages/cli/dist/commands/loop-run-once.js
15226
15817
  import { spawn as spawn3 } from "node:child_process";
15818
+ import { lookup } from "node:dns/promises";
15227
15819
  function announceReport(projectPath, slug, storyId, opener = (p) => {
15228
15820
  try {
15229
15821
  spawn3("open", [p], { stdio: "ignore", detached: true }).unref();
@@ -15232,13 +15824,13 @@ function announceReport(projectPath, slug, storyId, opener = (p) => {
15232
15824
  }) {
15233
15825
  if (storyId === "")
15234
15826
  return null;
15235
- const report = join33(projectPath, ".roll", "verification", storyId, "latest", "report.html");
15236
- if (!existsSync36(report))
15827
+ const report = join34(cardArchiveDir(projectPath, storyId), "latest", reportFileName(storyId));
15828
+ if (!existsSync37(report))
15237
15829
  return null;
15238
15830
  process.stdout.write(`evidence: ${report}
15239
15831
  \u9A8C\u6536\u62A5\u544A: ${report}
15240
15832
  `);
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}`));
15833
+ const muted = existsSync37(join34(projectPath, ".roll", "loop", `mute-${slug}`)) || existsSync37(join34(process.env["ROLL_SHARED_ROOT"] || join34(process.env["HOME"] ?? "", ".shared", "roll"), "loop", `mute-${slug}`));
15242
15834
  if (!muted)
15243
15835
  opener(report);
15244
15836
  return report;
@@ -15255,7 +15847,7 @@ function cycleSignalTeardown(paths, cycleId, branch, sig, deps = {}) {
15255
15847
  }
15256
15848
  let owned = false;
15257
15849
  try {
15258
- owned = existsSync36(paths.lockPath) && parseLock(readFileSync28(paths.lockPath, "utf8")).pid === pid;
15850
+ owned = existsSync37(paths.lockPath) && parseLock(readFileSync29(paths.lockPath, "utf8")).pid === pid;
15259
15851
  } catch {
15260
15852
  owned = false;
15261
15853
  }
@@ -15300,27 +15892,27 @@ function makeCycleId(now = /* @__PURE__ */ new Date(), pid = process.pid) {
15300
15892
  }
15301
15893
  function runtimeDir2(projectPath) {
15302
15894
  const env = (process.env["ROLL_PROJECT_RUNTIME_DIR"] ?? "").trim();
15303
- return env !== "" ? env : join33(projectPath, ".roll", "loop");
15895
+ return env !== "" ? env : join34(projectPath, ".roll", "loop");
15304
15896
  }
15305
15897
  var PAUSE_THRESHOLD = 3;
15306
15898
  function incrementConsecutiveFails(projectPath, slug, alertsPath, cycleId, storyId, terminal) {
15307
15899
  const rt = runtimeDir2(projectPath);
15308
- const counterFile = join33(rt, "consecutive-fails");
15900
+ const counterFile = join34(rt, "consecutive-fails");
15309
15901
  let count = 0;
15310
15902
  try {
15311
- if (existsSync36(counterFile)) {
15312
- count = parseInt(readFileSync28(counterFile, "utf8").trim(), 10) || 0;
15903
+ if (existsSync37(counterFile)) {
15904
+ count = parseInt(readFileSync29(counterFile, "utf8").trim(), 10) || 0;
15313
15905
  }
15314
15906
  } catch {
15315
15907
  }
15316
15908
  count += 1;
15317
15909
  try {
15318
- writeFileSync19(counterFile, String(count), "utf8");
15910
+ writeFileSync20(counterFile, String(count), "utf8");
15319
15911
  } catch {
15320
15912
  }
15321
15913
  if (count < PAUSE_THRESHOLD)
15322
15914
  return;
15323
- const pauseMarker = join33(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15915
+ const pauseMarker = join34(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15324
15916
  const alertMsg = `# ALERT \u2014 loop auto-paused after ${count} consecutive failures
15325
15917
 
15326
15918
  **Cycle**: ${cycleId}
@@ -15330,7 +15922,7 @@ function incrementConsecutiveFails(projectPath, slug, alertsPath, cycleId, story
15330
15922
  Resolve the root cause, then: \`roll loop resume\`
15331
15923
  `;
15332
15924
  try {
15333
- writeFileSync19(pauseMarker, alertMsg, "utf8");
15925
+ writeFileSync20(pauseMarker, alertMsg, "utf8");
15334
15926
  appendFileSync7(alertsPath, `${alertMsg}
15335
15927
  `, "utf8");
15336
15928
  } catch {
@@ -15342,7 +15934,7 @@ loop run-once: \u8FDE\u7EED ${count} \u6B21\u5931\u8D25\u540E\u81EA\u52A8\u6682\
15342
15934
  function resetConsecutiveFails(projectPath) {
15343
15935
  const rt = runtimeDir2(projectPath);
15344
15936
  try {
15345
- writeFileSync19(join33(rt, "consecutive-fails"), "0", "utf8");
15937
+ writeFileSync20(join34(rt, "consecutive-fails"), "0", "utf8");
15346
15938
  } catch {
15347
15939
  }
15348
15940
  }
@@ -15354,15 +15946,15 @@ function readSkillBody2(projectPath) {
15354
15946
  }
15355
15947
  function buildLoopRouteDeps(projectPath) {
15356
15948
  function readSlot(slot) {
15357
- const agentsYaml = join33(projectPath, ".roll", "agents.yaml");
15949
+ const agentsYaml = join34(projectPath, ".roll", "agents.yaml");
15358
15950
  try {
15359
- return readSlotFromText(readFileSync28(agentsYaml, "utf8"), slot);
15951
+ return readSlotFromText(readFileSync29(agentsYaml, "utf8"), slot);
15360
15952
  } catch {
15361
15953
  return void 0;
15362
15954
  }
15363
15955
  }
15364
15956
  function firstInstalled() {
15365
- const fromLocal = readField(join33(projectPath, ".roll", "local.yaml"), /^agent:/);
15957
+ const fromLocal = readField(join34(projectPath, ".roll", "local.yaml"), /^agent:/);
15366
15958
  if (fromLocal !== void 0)
15367
15959
  return fromLocal;
15368
15960
  return firstInstalledAgent(realAgentEnv());
@@ -15371,7 +15963,7 @@ function buildLoopRouteDeps(projectPath) {
15371
15963
  }
15372
15964
  function readField(path, re) {
15373
15965
  try {
15374
- const text = readFileSync28(path, "utf8");
15966
+ const text = readFileSync29(path, "utf8");
15375
15967
  for (const line of text.split("\n")) {
15376
15968
  const m7 = line.match(re);
15377
15969
  if (m7 !== null) {
@@ -15407,15 +15999,15 @@ async function loopRunOnceCommand(args) {
15407
15999
  return 0;
15408
16000
  }
15409
16001
  const rt = runtimeDir2(id.path);
15410
- const alertsPath = join33(rt, `ALERT-${id.slug}.md`);
15411
- mkdirSync18(dirname15(alertsPath), { recursive: true });
16002
+ const alertsPath = join34(rt, `ALERT-${id.slug}.md`);
16003
+ mkdirSync19(dirname15(alertsPath), { recursive: true });
15412
16004
  const paths = {
15413
- eventsPath: join33(rt, "events.ndjson"),
15414
- runsPath: join33(rt, "runs.jsonl"),
16005
+ eventsPath: join34(rt, "events.ndjson"),
16006
+ runsPath: join34(rt, "runs.jsonl"),
15415
16007
  alertsPath,
15416
- lockPath: join33(rt, "inner.lock"),
15417
- heartbeatPath: join33(rt, "heartbeat"),
15418
- worktreePath: join33(rt, "worktrees", `cycle-${cycleId}`)
16008
+ lockPath: join34(rt, "inner.lock"),
16009
+ heartbeatPath: join34(rt, "heartbeat"),
16010
+ worktreePath: join34(rt, "worktrees", `cycle-${cycleId}`)
15419
16011
  };
15420
16012
  const skillBody = readSkillBody2(id.path);
15421
16013
  if (skillBody === null) {
@@ -15463,24 +16055,43 @@ loop run-once: \u627E\u4E0D\u5230 roll-loop SKILL.md \u2014 \u62D2\u7EDD\u76F2\u
15463
16055
  }
15464
16056
  const isFail = result.terminal === "failed" || result.terminal === "blocked";
15465
16057
  if (isFail) {
16058
+ if (await isOffline()) {
16059
+ process.stderr.write("loop run-once: network unreachable \u2014 degraded to local-only delivery (commits stay on the branch; push/PR catch up when back online)\nloop run-once: \u7F51\u7EDC\u4E0D\u53EF\u8FBE\u2014\u2014\u5DF2\u964D\u7EA7\u4E3A\u672C\u5730\u4EA4\u4ED8\uFF08\u63D0\u4EA4\u4FDD\u7559\u5728\u5206\u652F\u4E0A\uFF0C\u8054\u7F51\u540E push/PR \u81EA\u7136\u8865\u4E0A\uFF09\n");
16060
+ return 0;
16061
+ }
15466
16062
  const storyId = (result.state?.ctx?.storyId ?? "").trim();
15467
16063
  incrementConsecutiveFails(id.path, id.slug, alertsPath, cycleId, storyId, result.terminal ?? "unknown");
15468
16064
  }
15469
16065
  return isFail ? 1 : 0;
15470
16066
  }
16067
+ async function isOffline(resolve3 = (h) => lookup(h)) {
16068
+ try {
16069
+ await Promise.race([
16070
+ resolve3("github.com"),
16071
+ new Promise((_, rej) => {
16072
+ const t2 = setTimeout(() => rej(new Error("dns timeout")), 1500);
16073
+ if (typeof t2 === "object")
16074
+ t2.unref();
16075
+ })
16076
+ ]);
16077
+ return false;
16078
+ } catch {
16079
+ return true;
16080
+ }
16081
+ }
15471
16082
 
15472
16083
  // packages/cli/dist/commands/loop-sched.js
15473
16084
  import { createHash as createHash4 } from "node:crypto";
15474
16085
  import { spawn as spawn4, spawnSync as spawnSync6 } from "node:child_process";
15475
- import { existsSync as existsSync37, mkdirSync as mkdirSync19, readFileSync as readFileSync29, rmSync as rmSync9, writeFileSync as writeFileSync20 } from "node:fs";
16086
+ import { existsSync as existsSync38, mkdirSync as mkdirSync20, readFileSync as readFileSync30, rmSync as rmSync9, writeFileSync as writeFileSync21 } from "node:fs";
15476
16087
  import { homedir as homedir15 } from "node:os";
15477
- import { dirname as dirname16, join as join34 } from "node:path";
16088
+ import { dirname as dirname16, join as join35 } from "node:path";
15478
16089
  function realDeps3() {
15479
16090
  return {
15480
16091
  identity: () => projectIdentity(),
15481
16092
  uid: () => process.getuid?.() ?? 501,
15482
- sharedRoot: () => process.env["ROLL_SHARED_ROOT"] || join34(homedir15(), ".shared", "roll"),
15483
- launchdDir: () => join34(homedir15(), "Library", "LaunchAgents"),
16093
+ sharedRoot: () => process.env["ROLL_SHARED_ROOT"] || join35(homedir15(), ".shared", "roll"),
16094
+ launchdDir: () => join35(homedir15(), "Library", "LaunchAgents"),
15484
16095
  launchd: { reinstall, uninstall, isLoaded },
15485
16096
  execRunner: (runner) => new Promise((resolve3) => {
15486
16097
  const child = spawn4("bash", [runner], {
@@ -15500,8 +16111,8 @@ function realDeps3() {
15500
16111
  // The `loop now` inline observation: tail live.log while the cycle holds
15501
16112
  // the inner lock; Ctrl-C stops the TAIL only (the cycle lives in tmux).
15502
16113
  observe: (rt) => new Promise((resolve3) => {
15503
- const lock = join34(rt, "inner.lock");
15504
- const tail = spawn4("tail", ["-n", "+1", "-F", join34(rt, "live.log")], { stdio: "inherit" });
16114
+ const lock = join35(rt, "inner.lock");
16115
+ const tail = spawn4("tail", ["-n", "+1", "-F", join35(rt, "live.log")], { stdio: "inherit" });
15505
16116
  let sawLock = false;
15506
16117
  const t0 = Date.now();
15507
16118
  const finish = () => {
@@ -15513,9 +16124,9 @@ function realDeps3() {
15513
16124
  resolve3();
15514
16125
  };
15515
16126
  const timer = setInterval(() => {
15516
- if (existsSync37(lock))
16127
+ if (existsSync38(lock))
15517
16128
  sawLock = true;
15518
- const done = sawLock ? !existsSync37(lock) : Date.now() - t0 > 3e4;
16129
+ const done = sawLock ? !existsSync38(lock) : Date.now() - t0 > 3e4;
15519
16130
  if (done) {
15520
16131
  clearInterval(timer);
15521
16132
  finish();
@@ -15684,11 +16295,11 @@ function pathValue() {
15684
16295
  ].join(":");
15685
16296
  }
15686
16297
  function writeExecutable(path, content) {
15687
- mkdirSync19(dirname16(path), { recursive: true });
15688
- writeFileSync20(path, content, { mode: 493 });
16298
+ mkdirSync20(dirname16(path), { recursive: true });
16299
+ writeFileSync21(path, content, { mode: 493 });
15689
16300
  }
15690
16301
  function pauseMarkerPath(projectPath, slug) {
15691
- return join34(projectPath, ".roll", "loop", `PAUSE-${slug}`);
16302
+ return join35(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15692
16303
  }
15693
16304
  var LOOP_SERVICES = ["loop", "dream", "pr"];
15694
16305
  async function mountService(deps, uid, label4, plist) {
@@ -15707,16 +16318,16 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15707
16318
  const shared = deps.sharedRoot();
15708
16319
  const ld = deps.launchdDir();
15709
16320
  const uid = deps.uid();
15710
- mkdirSync19(ld, { recursive: true });
16321
+ mkdirSync20(ld, { recursive: true });
15711
16322
  let period = 30;
15712
- const localYaml = join34(id.path, ".roll", "local.yaml");
15713
- if (existsSync37(localYaml)) {
16323
+ const localYaml = join35(id.path, ".roll", "local.yaml");
16324
+ if (existsSync38(localYaml)) {
15714
16325
  try {
15715
- period = parseLoopPeriodMinutes(readFileSync29(localYaml, "utf8"));
16326
+ period = parseLoopPeriodMinutes(readFileSync30(localYaml, "utf8"));
15716
16327
  } catch {
15717
16328
  }
15718
16329
  }
15719
- const loopRunner = join34(shared, "loop", `run-${id.slug}.sh`);
16330
+ const loopRunner = join35(shared, "loop", `run-${id.slug}.sh`);
15720
16331
  const rollBinOverride = (process.env["ROLL_RUNNER_ROLL_BIN"] ?? "").trim();
15721
16332
  writeExecutable(loopRunner, buildLoopRunnerScript({
15722
16333
  projectPath: id.path,
@@ -15727,7 +16338,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15727
16338
  }));
15728
16339
  const loopLabel = launchdLabel("loop", id.slug);
15729
16340
  const loopPlist = launchdPlistPath("loop", id.slug, ld);
15730
- writeFileSync20(loopPlist, plistContent({
16341
+ writeFileSync21(loopPlist, plistContent({
15731
16342
  label: loopLabel,
15732
16343
  runnerScript: loopRunner,
15733
16344
  projectPath: id.path,
@@ -15735,14 +16346,14 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15735
16346
  schedule: { kind: "interval", periodMinutes: period }
15736
16347
  }));
15737
16348
  const loopMount = await mountService(deps, uid, loopLabel, loopPlist);
15738
- const prRunner = join34(shared, "pr", `run-${id.slug}.sh`);
16349
+ const prRunner = join35(shared, "pr", `run-${id.slug}.sh`);
15739
16350
  writeExecutable(prRunner, buildPrRunnerScript({
15740
16351
  projectPath: id.path,
15741
16352
  ...rollBinOverride !== "" ? { rollBin: rollBinOverride } : {}
15742
16353
  }));
15743
16354
  const prLabel = launchdLabel("pr", id.slug);
15744
16355
  const prPlist = launchdPlistPath("pr", id.slug, ld);
15745
- writeFileSync20(prPlist, plistContent({
16356
+ writeFileSync21(prPlist, plistContent({
15746
16357
  label: prLabel,
15747
16358
  runnerScript: prRunner,
15748
16359
  projectPath: id.path,
@@ -15751,7 +16362,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15751
16362
  }));
15752
16363
  const prMount = await mountService(deps, uid, prLabel, prPlist);
15753
16364
  const dream = dreamScheduleFor(id.path);
15754
- const dreamRunner = join34(shared, "dream", `run-${id.slug}.sh`);
16365
+ const dreamRunner = join35(shared, "dream", `run-${id.slug}.sh`);
15755
16366
  writeExecutable(dreamRunner, buildDreamRunnerScript({
15756
16367
  projectPath: id.path,
15757
16368
  slug: id.slug,
@@ -15759,7 +16370,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15759
16370
  }));
15760
16371
  const dreamLabel = launchdLabel("dream", id.slug);
15761
16372
  const dreamPlist = launchdPlistPath("dream", id.slug, ld);
15762
- writeFileSync20(dreamPlist, plistContent({
16373
+ writeFileSync21(dreamPlist, plistContent({
15763
16374
  label: dreamLabel,
15764
16375
  runnerScript: dreamRunner,
15765
16376
  projectPath: id.path,
@@ -15811,10 +16422,10 @@ Loop \u5DF2\u505C\u7528(loop/dream/pr \u5747\u5DF2\u5378\u8F7D)
15811
16422
  async function loopPauseCommand(_args, deps = realDeps3()) {
15812
16423
  const id = await deps.identity();
15813
16424
  const marker = pauseMarkerPath(id.path, id.slug);
15814
- mkdirSync19(dirname16(marker), { recursive: true });
15815
- const already = existsSync37(marker);
16425
+ mkdirSync20(dirname16(marker), { recursive: true });
16426
+ const already = existsSync38(marker);
15816
16427
  if (!already)
15817
- writeFileSync20(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
16428
+ writeFileSync21(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
15818
16429
  `);
15819
16430
  process.stdout.write(already ? `Loop already paused
15820
16431
  Loop \u5DF2\u5904\u4E8E\u6682\u505C
@@ -15826,7 +16437,7 @@ Loop \u5DF2\u6682\u505C \u2014 \u540E\u7EED\u6392\u7A0B\u5468\u671F\u5C06\u8DF3\
15826
16437
  async function loopResumeCommand(_args, deps = realDeps3()) {
15827
16438
  const id = await deps.identity();
15828
16439
  const marker = pauseMarkerPath(id.path, id.slug);
15829
- const existed = existsSync37(marker);
16440
+ const existed = existsSync38(marker);
15830
16441
  rmSync9(marker, { force: true });
15831
16442
  process.stdout.write(existed ? `Loop resumed \u2014 scheduling active again
15832
16443
  Loop \u5DF2\u6062\u590D \u2014 \u6392\u7A0B\u91CD\u65B0\u751F\u6548
@@ -15844,16 +16455,16 @@ function isLegacyRunner(text) {
15844
16455
  }
15845
16456
  async function loopNowCommand(_args, deps = realDeps3()) {
15846
16457
  const id = await deps.identity();
15847
- const runner = join34(deps.sharedRoot(), "loop", `run-${id.slug}.sh`);
16458
+ const runner = join35(deps.sharedRoot(), "loop", `run-${id.slug}.sh`);
15848
16459
  let legacy = false;
15849
- if (existsSync37(runner)) {
16460
+ if (existsSync38(runner)) {
15850
16461
  try {
15851
- legacy = isLegacyRunner(readFileSync29(runner, "utf8"));
16462
+ legacy = isLegacyRunner(readFileSync30(runner, "utf8"));
15852
16463
  } catch {
15853
16464
  legacy = true;
15854
16465
  }
15855
16466
  }
15856
- if (!existsSync37(runner) || legacy) {
16467
+ if (!existsSync38(runner) || legacy) {
15857
16468
  process.stdout.write(legacy ? `Legacy v2 runner detected \u2014 regenerating templates (FIX-197)
15858
16469
  \u68C0\u6D4B\u5230 v2 \u65E7\u7248 runner \u2014 \u6B63\u5728\u518D\u751F\u6210\u6A21\u677F\uFF08FIX-197\uFF09
15859
16470
  ` : `No runner yet \u2014 generating templates
@@ -15881,7 +16492,7 @@ live transcript below \u2014 Ctrl-C stops watching, never the cycle
15881
16492
  }
15882
16493
  const rc = await exec(runner);
15883
16494
  if (rc === 0 && useTmux && deps.observe !== void 0) {
15884
- await deps.observe(join34(id.path, ".roll", "loop"));
16495
+ await deps.observe(join35(id.path, ".roll", "loop"));
15885
16496
  process.stdout.write(`
15886
16497
  cycle finished \u2014 logs: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
15887
16498
  \u5468\u671F\u7ED3\u675F \u2014 \u65E5\u5FD7: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
@@ -15892,7 +16503,7 @@ cycle finished \u2014 logs: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
15892
16503
 
15893
16504
  // packages/cli/dist/commands/migrate.js
15894
16505
  import { spawnSync as spawnSync7 } from "node:child_process";
15895
- import { existsSync as existsSync38, mkdirSync as mkdirSync20 } from "node:fs";
16506
+ import { existsSync as existsSync39, mkdirSync as mkdirSync21 } from "node:fs";
15896
16507
  import { dirname as dirname17 } from "node:path";
15897
16508
  function colors() {
15898
16509
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
@@ -16013,8 +16624,8 @@ function migrateExecute(activeMoves) {
16013
16624
  const src = srcOf(move);
16014
16625
  const tgt = tgtOf(move);
16015
16626
  const targetDir = dirname17(tgt);
16016
- if (!existsSync38(targetDir))
16017
- mkdirSync20(targetDir, { recursive: true });
16627
+ if (!existsSync39(targetDir))
16628
+ mkdirSync21(targetDir, { recursive: true });
16018
16629
  const r = git3(["mv", src, tgt]);
16019
16630
  if (r.status !== 0) {
16020
16631
  err11(`git mv failed: ${src} \u2192 ${tgt}`);
@@ -16023,7 +16634,7 @@ function migrateExecute(activeMoves) {
16023
16634
  }
16024
16635
  moved += 1;
16025
16636
  }
16026
- if (existsSync38("docs")) {
16637
+ if (existsSync39("docs")) {
16027
16638
  spawnSync7("find", ["docs", "-type", "d", "-empty", "-delete"], { stdio: "ignore" });
16028
16639
  }
16029
16640
  git3([
@@ -16064,10 +16675,10 @@ function migrateCommand(args) {
16064
16675
  const moves = buildMoves();
16065
16676
  let hasNew = false;
16066
16677
  let hasOld = false;
16067
- if (existsSync38(".roll"))
16678
+ if (existsSync39(".roll"))
16068
16679
  hasNew = true;
16069
16680
  for (const move of moves) {
16070
- if (existsSync38(srcOf(move))) {
16681
+ if (existsSync39(srcOf(move))) {
16071
16682
  hasOld = true;
16072
16683
  break;
16073
16684
  }
@@ -16079,7 +16690,7 @@ function migrateCommand(args) {
16079
16690
  for (const move of moves) {
16080
16691
  const src = srcOf(move);
16081
16692
  const tgt = tgtOf(move);
16082
- if (existsSync38(src) && existsSync38(tgt)) {
16693
+ if (existsSync39(src) && existsSync39(tgt)) {
16083
16694
  process.stderr.write(` - ${src} AND ${tgt} both exist
16084
16695
  `);
16085
16696
  }
@@ -16096,7 +16707,7 @@ function migrateCommand(args) {
16096
16707
  info3(m2("migrate.no_old_structure_detected_nothing_to"));
16097
16708
  return 0;
16098
16709
  }
16099
- const activeMoves = moves.filter((move) => existsSync38(srcOf(move)));
16710
+ const activeMoves = moves.filter((move) => existsSync39(srcOf(move)));
16100
16711
  if (activeMoves.length === 0) {
16101
16712
  warn4(m2("migrate.old_structure_markers_found_but_no"));
16102
16713
  return 0;
@@ -16113,11 +16724,11 @@ function migrateCommand(args) {
16113
16724
  }
16114
16725
 
16115
16726
  // 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";
16727
+ import { existsSync as existsSync40, mkdirSync as mkdirSync22, readFileSync as readFileSync31, readlinkSync as readlinkSync3, statSync as statSync18, writeFileSync as writeFileSync22 } from "node:fs";
16728
+ import { join as join36 } from "node:path";
16118
16729
  function specFrontmatter(specFile, storyId) {
16119
16730
  try {
16120
- const lines = readFileSync30(specFile, "utf8").split("\n").slice(0, 12);
16731
+ const lines = readFileSync31(specFile, "utf8").split("\n").slice(0, 12);
16121
16732
  const pick = (key) => {
16122
16733
  const row2 = lines.find((l) => l.startsWith(`${key}: `));
16123
16734
  return row2?.slice(key.length + 2).trim();
@@ -16137,24 +16748,24 @@ function specFrontmatter(specFile, storyId) {
16137
16748
  function migrateFeaturesCommand(args) {
16138
16749
  const refresh = args.includes("--refresh-html");
16139
16750
  const cwd = process.cwd();
16140
- if (!existsSync39(join35(cwd, ".roll", "index.json"))) {
16751
+ if (!existsSync40(join36(cwd, ".roll", "index.json"))) {
16141
16752
  process.stderr.write("migrate-features: .roll/index.json not found\n");
16142
16753
  return 1;
16143
16754
  }
16144
16755
  const stories = readIndex(cwd);
16145
- const root = join35(cwd, ".roll", "features");
16756
+ const root = join36(cwd, ".roll", "features");
16146
16757
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
16147
16758
  let specCreated = 0;
16148
16759
  let htmlWritten = 0;
16149
16760
  let skipped = 0;
16150
16761
  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)) {
16762
+ const storyDir = join36(root, epic, storyId);
16763
+ const specFile = join36(storyDir, "spec.md");
16764
+ if (!existsSync40(storyDir)) {
16765
+ const mdFile = join36(root, epic, `${storyId}.md`);
16766
+ if (existsSync40(mdFile)) {
16156
16767
  try {
16157
- mkdirSync21(storyDir, { recursive: true });
16768
+ mkdirSync22(storyDir, { recursive: true });
16158
16769
  } catch {
16159
16770
  skipped++;
16160
16771
  continue;
@@ -16164,8 +16775,8 @@ function migrateFeaturesCommand(args) {
16164
16775
  continue;
16165
16776
  }
16166
16777
  }
16167
- if (!existsSync39(specFile)) {
16168
- writeFileSync21(specFile, renderSpecMd({
16778
+ if (!existsSync40(specFile)) {
16779
+ writeFileSync22(specFile, renderSpecMd({
16169
16780
  id: storyId,
16170
16781
  epic,
16171
16782
  created: today,
@@ -16173,25 +16784,25 @@ function migrateFeaturesCommand(args) {
16173
16784
  }), "utf8");
16174
16785
  specCreated++;
16175
16786
  }
16176
- const htmlFile = join35(storyDir, "index.html");
16177
- if (!existsSync39(htmlFile) || refresh) {
16787
+ const htmlFile = join36(storyDir, "index.html");
16788
+ if (!existsSync40(htmlFile) || refresh) {
16178
16789
  const fm = specFrontmatter(specFile, storyId);
16179
16790
  const card = { id: storyId, epic, created: fm.created ?? today, ...fm.title !== void 0 ? { title: fm.title } : {} };
16180
16791
  let html = renderStoryPage(card);
16181
- const latest = join35(storyDir, "latest");
16182
- const report = join35(latest, reportFileName(storyId));
16183
- if (existsSync39(report)) {
16792
+ const latest = join36(storyDir, "latest");
16793
+ const report = join36(latest, reportFileName(storyId));
16794
+ if (existsSync40(report)) {
16184
16795
  let runRel = "latest";
16185
16796
  try {
16186
16797
  runRel = readlinkSync3(latest);
16187
16798
  } catch {
16188
16799
  }
16189
- const deliveredOn = statSync17(report).mtime.toISOString().slice(0, 10);
16800
+ const deliveredOn = statSync18(report).mtime.toISOString().slice(0, 10);
16190
16801
  html = markPhaseDone(html, "delivery", `<p><a href="${runRel}/${reportFileName(storyId)}">${bi("Attestation report", "\u9A8C\u6536\u62A5\u544A")}</a></p>
16191
16802
  <p class="muted">${bi("Delivered", "\u4EA4\u4ED8\u4E8E")} ${deliveredOn}</p>
16192
16803
  `);
16193
16804
  }
16194
- writeFileSync21(htmlFile, html, "utf8");
16805
+ writeFileSync22(htmlFile, html, "utf8");
16195
16806
  htmlWritten++;
16196
16807
  }
16197
16808
  }
@@ -16202,9 +16813,9 @@ function migrateFeaturesCommand(args) {
16202
16813
 
16203
16814
  // packages/cli/dist/commands/offboard.js
16204
16815
  import { spawnSync as spawnSync8 } from "node:child_process";
16205
- import { existsSync as existsSync40, readFileSync as readFileSync31, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync22 } from "node:fs";
16816
+ import { existsSync as existsSync41, readFileSync as readFileSync32, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync23 } from "node:fs";
16206
16817
  import { homedir as homedir16 } from "node:os";
16207
- import { join as join36, resolve as resolve2 } from "node:path";
16818
+ import { join as join37, resolve as resolve2 } from "node:path";
16208
16819
  function pal3() {
16209
16820
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
16210
16821
  return noColor2 ? { RED: "", GREEN: "", YELLOW: "", CYAN: "", BOLD: "", NC: "" } : {
@@ -16242,7 +16853,7 @@ function m3(key, ...args) {
16242
16853
  return t(v2Catalog, msgLang9(), key, ...args);
16243
16854
  }
16244
16855
  function changesetPath(projectDir) {
16245
- return join36(projectDir, ".roll", "onboard-changeset.yaml");
16856
+ return join37(projectDir, ".roll", "onboard-changeset.yaml");
16246
16857
  }
16247
16858
  function loopInCycle() {
16248
16859
  return (process.env["ROLL_LOOP_AGENT"] ?? "") !== "" || (process.env["ROLL_CYCLE_LOG_RAW"] ?? "") !== "";
@@ -16252,7 +16863,7 @@ function runLaunchctl(args) {
16252
16863
  if (args[0] !== void 0 && readOnly.has(args[0])) {
16253
16864
  return spawnSync8("launchctl", args, { stdio: "inherit" }).status ?? 1;
16254
16865
  }
16255
- const canonical = join36(homedir16(), "Library", "LaunchAgents");
16866
+ const canonical = join37(homedir16(), "Library", "LaunchAgents");
16256
16867
  const launchdDir = process.env["_LAUNCHD_DIR"] ?? canonical;
16257
16868
  if (launchdDir !== canonical)
16258
16869
  return 0;
@@ -16316,7 +16927,7 @@ function offboardCommand(args) {
16316
16927
  projectDir = process.cwd();
16317
16928
  }
16318
16929
  const changeset = changesetPath(projectDir);
16319
- if (!existsSync40(changeset)) {
16930
+ if (!existsSync41(changeset)) {
16320
16931
  err12(m3("offboard.no_changeset_en"));
16321
16932
  err12(m3("offboard.no_changeset_zh"));
16322
16933
  process.stderr.write("\n");
@@ -16330,7 +16941,7 @@ function offboardCommand(args) {
16330
16941
  `);
16331
16942
  return 1;
16332
16943
  }
16333
- const parsed = parseChangeset(readFileSync31(changeset, "utf8"));
16944
+ const parsed = parseChangeset(readFileSync32(changeset, "utf8"));
16334
16945
  if (!parsed.ok) {
16335
16946
  err12(m3("offboard.failed_to_parse_changeset"));
16336
16947
  return 1;
@@ -16406,8 +17017,8 @@ function offboardCommand(args) {
16406
17017
  process.stdout.write(m3("offboard.applying_offboard") + "\n");
16407
17018
  for (const item of files) {
16408
17019
  try {
16409
- rmSync10(join36(projectDir, item), { force: true });
16410
- if (!existsSync40(join36(projectDir, item)))
17020
+ rmSync10(join37(projectDir, item), { force: true });
17021
+ if (!existsSync41(join37(projectDir, item)))
16411
17022
  process.stdout.write(` removed file ${item}
16412
17023
  `);
16413
17024
  } catch {
@@ -16415,26 +17026,26 @@ function offboardCommand(args) {
16415
17026
  }
16416
17027
  for (const item of dirs) {
16417
17028
  try {
16418
- rmSync10(join36(projectDir, item), { recursive: true, force: true });
17029
+ rmSync10(join37(projectDir, item), { recursive: true, force: true });
16419
17030
  process.stdout.write(` removed dir ${item}
16420
17031
  `);
16421
17032
  } catch {
16422
17033
  }
16423
17034
  }
16424
17035
  for (const item of giEntries) {
16425
- const gi = join36(projectDir, ".gitignore");
16426
- if (existsSync40(gi)) {
16427
- const lines = readFileSync31(gi, "utf8").split("\n");
17036
+ const gi = join37(projectDir, ".gitignore");
17037
+ if (existsSync41(gi)) {
17038
+ const lines = readFileSync32(gi, "utf8").split("\n");
16428
17039
  if (lines.includes(item)) {
16429
17040
  const kept = lines.filter((l) => l !== item);
16430
- writeFileSync22(gi, kept.join("\n"));
17041
+ writeFileSync23(gi, kept.join("\n"));
16431
17042
  process.stdout.write(` .gitignore - ${item}
16432
17043
  `);
16433
17044
  }
16434
17045
  }
16435
17046
  }
16436
17047
  for (const item of plists) {
16437
- const plistPath = join36(homedir16(), "Library", "LaunchAgents", item);
17048
+ const plistPath = join37(homedir16(), "Library", "LaunchAgents", item);
16438
17049
  const r = runLaunchctl(["unload", "-w", plistPath]);
16439
17050
  if (r === 0)
16440
17051
  process.stdout.write(` unloaded ${item}
@@ -16447,16 +17058,16 @@ function offboardCommand(args) {
16447
17058
  }
16448
17059
 
16449
17060
  // packages/cli/dist/commands/prices.js
16450
- import { existsSync as existsSync41, readdirSync as readdirSync16, readFileSync as readFileSync32 } from "node:fs";
16451
- import { join as join37 } from "node:path";
17061
+ import { existsSync as existsSync42, readdirSync as readdirSync16, readFileSync as readFileSync33 } from "node:fs";
17062
+ import { join as join38 } from "node:path";
16452
17063
  function loadSnapshots() {
16453
- const dir = join37(repoRoot(), "lib", "prices");
16454
- if (!existsSync41(dir)) {
17064
+ const dir = join38(repoRoot(), "lib", "prices");
17065
+ if (!existsSync42(dir)) {
16455
17066
  throw new Error(`no price snapshots found in ${dir}; run \`roll prices refresh\``);
16456
17067
  }
16457
17068
  const files = readdirSync16(dir).filter((n) => n.startsWith("snapshot-") && n.endsWith(".json")).sort();
16458
17069
  return files.map((name) => {
16459
- const data = JSON.parse(readFileSync32(join37(dir, name), "utf8"));
17070
+ const data = JSON.parse(readFileSync33(join38(dir, name), "utf8"));
16460
17071
  for (const key of ["version", "effective_at", "source_url", "prices"]) {
16461
17072
  if (data[key] === void 0)
16462
17073
  throw new Error(`snapshot ${name} missing required key ${key}`);
@@ -16549,8 +17160,8 @@ function pricesCommand(args) {
16549
17160
  }
16550
17161
 
16551
17162
  // packages/cli/dist/commands/release.js
16552
- import { existsSync as existsSync42, readFileSync as readFileSync33 } from "node:fs";
16553
- import { join as join38 } from "node:path";
17163
+ import { existsSync as existsSync43, readFileSync as readFileSync34 } from "node:fs";
17164
+ import { join as join39 } from "node:path";
16554
17165
  function label3(lang4, key, ...args) {
16555
17166
  if (v3Catalog[key] !== void 0)
16556
17167
  return t(v3Catalog, lang4, key, ...args);
@@ -16561,25 +17172,34 @@ function dateOf(d) {
16561
17172
  }
16562
17173
  function currentVersion(cwd) {
16563
17174
  try {
16564
- const pkg = JSON.parse(readFileSync33(join38(cwd, "package.json"), "utf8"));
17175
+ const pkg = JSON.parse(readFileSync34(join39(cwd, "package.json"), "utf8"));
16565
17176
  return typeof pkg.version === "string" ? pkg.version : "";
16566
17177
  } catch {
16567
17178
  return "";
16568
17179
  }
16569
17180
  }
16570
- function changelogReady(cwd) {
16571
- const path = join38(cwd, "CHANGELOG.md");
16572
- if (!existsSync42(path))
17181
+ function changelogReady(cwd, current) {
17182
+ const path = join39(cwd, "CHANGELOG.md");
17183
+ if (!existsSync43(path))
17184
+ return false;
17185
+ const text = readFileSync34(path, "utf8");
17186
+ const sectionAfter = (idx, headingLen) => {
17187
+ let section2 = text.slice(idx + headingLen);
17188
+ const nextHeading = section2.search(/\n## /);
17189
+ if (nextHeading !== -1)
17190
+ section2 = section2.slice(0, nextHeading);
17191
+ return section2;
17192
+ };
17193
+ const hasBullet = (s) => /^\s*-\s+\S/m.test(s);
17194
+ const unreleased = text.indexOf("## Unreleased");
17195
+ if (unreleased !== -1 && hasBullet(sectionAfter(unreleased, "## Unreleased".length)))
17196
+ return true;
17197
+ const m7 = /^## v(\d+\.\d+\.\d+)\b.*$/m.exec(text);
17198
+ if (m7 === null)
16573
17199
  return false;
16574
- const text = readFileSync33(path, "utf8");
16575
- const idx = text.indexOf("## Unreleased");
16576
- if (idx === -1)
17200
+ if (m7[1] === current)
16577
17201
  return false;
16578
- let section = text.slice(idx + "## Unreleased".length);
16579
- const nextHeading = section.search(/\n## /);
16580
- if (nextHeading !== -1)
16581
- section = section.slice(0, nextHeading);
16582
- return /^\s*-\s+\S/m.test(section);
17202
+ return hasBullet(sectionAfter(m7.index, m7[0].length));
16583
17203
  }
16584
17204
  function releaseCommand(args, now) {
16585
17205
  const noColor2 = args.includes("--no-color") || !process.stdout.isTTY || (process.env["NO_COLOR"] ?? "") !== "";
@@ -16601,7 +17221,7 @@ function releaseCommand(args, now) {
16601
17221
  `);
16602
17222
  return 1;
16603
17223
  }
16604
- const ready = changelogReady(cwd);
17224
+ const ready = changelogReady(cwd, cur);
16605
17225
  const plan = planRelease({
16606
17226
  currentVersion: cur,
16607
17227
  date: now ?? dateOf(/* @__PURE__ */ new Date()),
@@ -16638,8 +17258,8 @@ function releaseCommand(args, now) {
16638
17258
 
16639
17259
  // packages/cli/dist/commands/setup.js
16640
17260
  import { spawnSync as spawnSync9 } from "node:child_process";
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";
17261
+ import { existsSync as existsSync44, lstatSync as lstatSync3, mkdirSync as mkdirSync23, readFileSync as readFileSync35, readdirSync as readdirSync17, readlinkSync as readlinkSync4, statSync as statSync19 } from "node:fs";
17262
+ import { join as join40 } from "node:path";
16643
17263
  function err13(line) {
16644
17264
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
16645
17265
  const RED = noColor2 ? "" : "\x1B[0;31m";
@@ -16664,7 +17284,7 @@ function setupSnapshot(watch) {
16664
17284
  continue;
16665
17285
  let isDir4 = false;
16666
17286
  try {
16667
- isDir4 = statSync18(d).isDirectory();
17287
+ isDir4 = statSync19(d).isDirectory();
16668
17288
  } catch {
16669
17289
  isDir4 = false;
16670
17290
  }
@@ -16682,7 +17302,7 @@ function walk(dir, lines) {
16682
17302
  return;
16683
17303
  }
16684
17304
  for (const name of names) {
16685
- const p = join39(dir, name);
17305
+ const p = join40(dir, name);
16686
17306
  let isLink = false;
16687
17307
  try {
16688
17308
  isLink = lstatSync3(p).isSymbolicLink();
@@ -16698,7 +17318,7 @@ function walk(dir, lines) {
16698
17318
  }
16699
17319
  let st;
16700
17320
  try {
16701
- st = statSync18(p);
17321
+ st = statSync19(p);
16702
17322
  } catch {
16703
17323
  continue;
16704
17324
  }
@@ -16712,7 +17332,7 @@ function walk(dir, lines) {
16712
17332
  }
16713
17333
  function fileFingerprint(p) {
16714
17334
  try {
16715
- const buf = readFileSync34(p);
17335
+ const buf = readFileSync35(p);
16716
17336
  let sum = 0;
16717
17337
  for (let i = 0; i < buf.length; i++)
16718
17338
  sum = sum * 31 + (buf[i] ?? 0) >>> 0;
@@ -16742,18 +17362,18 @@ function ensureHooksPath(repoPath) {
16742
17362
  }
16743
17363
  }
16744
17364
  function peerEnsureStateDir() {
16745
- const base = join39(rollHome(), ".peer-state");
16746
- mkdirSync22(base, { recursive: true });
16747
- mkdirSync22(join39(base, "logs"), { recursive: true });
17365
+ const base = join40(rollHome(), ".peer-state");
17366
+ mkdirSync23(base, { recursive: true });
17367
+ mkdirSync23(join40(base, "logs"), { recursive: true });
16748
17368
  }
16749
17369
  function tmuxPresent() {
16750
17370
  return onPath("tmux");
16751
17371
  }
16752
17372
  function submoduleGuard() {
16753
17373
  const pkg = rollPkgDir();
16754
- const skills = join39(pkg, "skills");
16755
- const hasGit = existsSync43(join39(pkg, ".git"));
16756
- const hasMods = existsSync43(join39(pkg, ".gitmodules"));
17374
+ const skills = join40(pkg, "skills");
17375
+ const hasGit = existsSync44(join40(pkg, ".git"));
17376
+ const hasMods = existsSync44(join40(pkg, ".gitmodules"));
16757
17377
  let empty = true;
16758
17378
  try {
16759
17379
  empty = readdirSync17(skills).length === 0;
@@ -16846,7 +17466,7 @@ function setupCommand(args) {
16846
17466
  return 1;
16847
17467
  }
16848
17468
  }
16849
- if (!existsSync43(rollPkgConventions()))
17469
+ if (!existsSync44(rollPkgConventions()))
16850
17470
  return null;
16851
17471
  submoduleGuard();
16852
17472
  const home = rollHome();
@@ -16865,7 +17485,7 @@ function setupCommand(args) {
16865
17485
  ".qwen"
16866
17486
  ];
16867
17487
  const homeDir = process.env["HOME"] ?? "";
16868
- const aiDirs = aiDirsList.map((d) => join39(homeDir, d)).join(":");
17488
+ const aiDirs = aiDirsList.map((d) => join40(homeDir, d)).join(":");
16869
17489
  const steps = [];
16870
17490
  const s1 = runSetupStep(home, () => {
16871
17491
  installLocal(force);
@@ -16879,7 +17499,7 @@ function setupCommand(args) {
16879
17499
  syncSkills(force);
16880
17500
  });
16881
17501
  steps.push({ num: 3, label: "Install skills to ~/.claude", status: stateToMarker(s3, force) });
16882
- const s4 = runSetupStep(join39(home, ".peer-state"), () => {
17502
+ const s4 = runSetupStep(join40(home, ".peer-state"), () => {
16883
17503
  peerEnsureStateDir();
16884
17504
  });
16885
17505
  steps.push({ num: 4, label: "Initialize peer-review state directory", status: stateToMarker(s4, force) });
@@ -16900,12 +17520,12 @@ function setupCommand(args) {
16900
17520
 
16901
17521
  // packages/cli/dist/commands/slides/index.js
16902
17522
  import { spawnSync as spawnSync10 } from "node:child_process";
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";
17523
+ import { mkdirSync as mkdirSync24, readFileSync as readFileSync38, readdirSync as readdirSync19, rmSync as rmSync11, statSync as statSync22, writeFileSync as writeFileSync24 } from "node:fs";
17524
+ import { join as join42 } from "node:path";
16905
17525
 
16906
17526
  // packages/cli/dist/commands/slides/render.js
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";
17527
+ import { readFileSync as readFileSync36, existsSync as existsSync45, readdirSync as readdirSync18, statSync as statSync20 } from "node:fs";
17528
+ import { join as join41, dirname as dirname18, basename as basename9 } from "node:path";
16909
17529
  function coerceScalar(v) {
16910
17530
  if (v.length >= 2 && v[0] === v[v.length - 1] && (v[0] === "'" || v[0] === '"')) {
16911
17531
  return v.slice(1, -1);
@@ -17221,7 +17841,7 @@ function pyTruthy(v) {
17221
17841
  return v.length > 0;
17222
17842
  return Object.keys(v).length > 0;
17223
17843
  }
17224
- function lookup(ctxStack, key) {
17844
+ function lookup2(ctxStack, key) {
17225
17845
  for (let i = ctxStack.length - 1; i >= 0; i--) {
17226
17846
  const ctx = ctxStack[i];
17227
17847
  if (isPlainObject(ctx) && Object.prototype.hasOwnProperty.call(ctx, key)) {
@@ -17280,19 +17900,19 @@ function mustache(template, context) {
17280
17900
  const tagKey = m7[3];
17281
17901
  const matchEnd = m7.index + m7[0].length;
17282
17902
  if (rawKey) {
17283
- buf.push(pyStr(lookup(ctxStack, rawKey)));
17903
+ buf.push(pyStr(lookup2(ctxStack, rawKey)));
17284
17904
  i = matchEnd;
17285
17905
  continue;
17286
17906
  }
17287
17907
  if (sigil === "") {
17288
- buf.push(escapeHtml2(pyStr(lookup(ctxStack, tagKey))));
17908
+ buf.push(escapeHtml2(pyStr(lookup2(ctxStack, tagKey))));
17289
17909
  i = matchEnd;
17290
17910
  continue;
17291
17911
  }
17292
17912
  if (sigil === "#" || sigil === "^") {
17293
17913
  const closeIdx = findClose(chunk, matchEnd, tagKey);
17294
17914
  const inner = chunk.slice(matchEnd, closeIdx);
17295
- const val = lookup(ctxStack, tagKey);
17915
+ const val = lookup2(ctxStack, tagKey);
17296
17916
  if (sigil === "#") {
17297
17917
  if (Array.isArray(val)) {
17298
17918
  for (const item of val) {
@@ -17390,7 +18010,7 @@ var DEFAULT_LAYOUT = "plain";
17390
18010
  var LayoutResolver = class {
17391
18011
  componentsDir;
17392
18012
  constructor(componentsDir) {
17393
- this.componentsDir = componentsDir ?? join40(libDirFor(import.meta.url), "slides", "components");
18013
+ this.componentsDir = componentsDir ?? join41(libDirFor(import.meta.url), "slides", "components");
17394
18014
  }
17395
18015
  available() {
17396
18016
  if (!isDir2(this.componentsDir))
@@ -17404,7 +18024,7 @@ var LayoutResolver = class {
17404
18024
  if (!/^[a-z0-9-]+$/.test(layout)) {
17405
18025
  throw new ValueError(`Unknown layout: ${layout}; available: ${this.available().join(", ")}`);
17406
18026
  }
17407
- const path = join40(this.componentsDir, `${layout}.html`);
18027
+ const path = join41(this.componentsDir, `${layout}.html`);
17408
18028
  if (!isFile(path)) {
17409
18029
  throw new ValueError(`Unknown layout: ${layout}; available: ${this.available().join(", ")}`);
17410
18030
  }
@@ -17414,7 +18034,7 @@ var LayoutResolver = class {
17414
18034
  function renderSlideInner(slide, resolver) {
17415
18035
  const layout = typeof slide["layout"] === "string" && slide["layout"] || DEFAULT_LAYOUT;
17416
18036
  const partialPath = resolver.resolve(String(layout));
17417
- let partial = readFileSync35(partialPath, "utf8");
18037
+ let partial = readFileSync36(partialPath, "utf8");
17418
18038
  partial = partial.replace(/^\s*<!--[\s\S]*?-->(?:\s*\n)?/, "");
17419
18039
  const rendered = mustache(partial, slide);
17420
18040
  return stripNewlines(rendered);
@@ -17462,28 +18082,28 @@ function pyRepr(s) {
17462
18082
  }
17463
18083
  function isDir2(p) {
17464
18084
  try {
17465
- return statSync19(p).isDirectory();
18085
+ return statSync20(p).isDirectory();
17466
18086
  } catch {
17467
18087
  return false;
17468
18088
  }
17469
18089
  }
17470
18090
  function isFile(p) {
17471
18091
  try {
17472
- return statSync19(p).isFile();
18092
+ return statSync20(p).isFile();
17473
18093
  } catch {
17474
18094
  return false;
17475
18095
  }
17476
18096
  }
17477
18097
  function libDirFor(_metaUrl) {
17478
18098
  void _metaUrl;
17479
- void existsSync44;
18099
+ void existsSync45;
17480
18100
  void dirname18;
17481
- return join40(repoLibFallback(), "lib");
18101
+ return join41(repoLibFallback(), "lib");
17482
18102
  }
17483
18103
  function repoLibFallback() {
17484
18104
  let dir = process.cwd();
17485
18105
  for (let i = 0; i < 12; i++) {
17486
- if (existsSync44(join40(dir, "bin", "roll")))
18106
+ if (existsSync45(join41(dir, "bin", "roll")))
17487
18107
  return dir;
17488
18108
  const parent = dirname18(dir);
17489
18109
  if (parent === dir)
@@ -17494,7 +18114,7 @@ function repoLibFallback() {
17494
18114
  }
17495
18115
 
17496
18116
  // packages/cli/dist/commands/slides/validate.js
17497
- import { readFileSync as readFileSync36, statSync as statSync20 } from "node:fs";
18117
+ import { readFileSync as readFileSync37, statSync as statSync21 } from "node:fs";
17498
18118
  var REQUIRED_FRONTMATTER = [
17499
18119
  "template",
17500
18120
  "slug",
@@ -17698,7 +18318,7 @@ function validateDeckFile(path, errSink, componentsDir) {
17698
18318
  let fm;
17699
18319
  let slides;
17700
18320
  try {
17701
- src = readFileSync36(path, "utf8");
18321
+ src = readFileSync37(path, "utf8");
17702
18322
  [fm] = parseFrontmatter(src);
17703
18323
  const [, body] = parseFrontmatter(src);
17704
18324
  slides = parseSlides(body);
@@ -17732,7 +18352,7 @@ function validateDeckFile(path, errSink, componentsDir) {
17732
18352
  }
17733
18353
  function isFile2(p) {
17734
18354
  try {
17735
- return statSync20(p).isFile();
18355
+ return statSync21(p).isFile();
17736
18356
  } catch {
17737
18357
  return false;
17738
18358
  }
@@ -17811,13 +18431,13 @@ function slidesHelp(out2) {
17811
18431
  out2(SLIDES_HELP);
17812
18432
  }
17813
18433
  function slidesLib() {
17814
- return join41(rollPkgDir(), "lib");
18434
+ return join42(rollPkgDir(), "lib");
17815
18435
  }
17816
18436
  function slidesTemplatePath(name) {
17817
- const projTpl = join41(".roll", "slides", "templates", `${name}.html`);
18437
+ const projTpl = join42(".roll", "slides", "templates", `${name}.html`);
17818
18438
  if (isFile3(projTpl))
17819
18439
  return projTpl;
17820
- const tpl = join41(rollPkgDir(), "lib", "slides", "templates", `${name}.html`);
18440
+ const tpl = join42(rollPkgDir(), "lib", "slides", "templates", `${name}.html`);
17821
18441
  if (isFile3(tpl))
17822
18442
  return tpl;
17823
18443
  return null;
@@ -17825,7 +18445,7 @@ function slidesTemplatePath(name) {
17825
18445
  function slidesTemplateForDeck(deckPath) {
17826
18446
  let tpl = "";
17827
18447
  try {
17828
- const src = readFileSync37(deckPath, "utf8");
18448
+ const src = readFileSync38(deckPath, "utf8");
17829
18449
  let d = 0;
17830
18450
  for (const line of src.split("\n")) {
17831
18451
  if (/^---[ \t]*$/.test(line)) {
@@ -17851,21 +18471,21 @@ function slidesTemplateForDeck(deckPath) {
17851
18471
  return tpl;
17852
18472
  }
17853
18473
  function slidesEnsureGitignore() {
17854
- const gi = join41(".roll", ".gitignore");
17855
- mkdirSync23(".roll", { recursive: true });
18474
+ const gi = join42(".roll", ".gitignore");
18475
+ mkdirSync24(".roll", { recursive: true });
17856
18476
  if (isFile3(gi)) {
17857
- const content = readFileSync37(gi, "utf8");
18477
+ const content = readFileSync38(gi, "utf8");
17858
18478
  if (content.split("\n").some((l) => /^slides\/\*\.html$/.test(l)))
17859
18479
  return;
17860
18480
  if (content.length > 0 && !content.endsWith("\n")) {
17861
- writeFileSync23(gi, content + "\n");
18481
+ writeFileSync24(gi, content + "\n");
17862
18482
  }
17863
18483
  }
17864
18484
  appendFile(gi, "slides/*.html\n");
17865
18485
  }
17866
18486
  function appendFile(path, data) {
17867
- const prev = isFile3(path) ? readFileSync37(path, "utf8") : "";
17868
- writeFileSync23(path, prev + data);
18487
+ const prev = isFile3(path) ? readFileSync38(path, "utf8") : "";
18488
+ writeFileSync24(path, prev + data);
17869
18489
  }
17870
18490
  function slidesOpenCmd() {
17871
18491
  const sys = spawnSync10("uname", ["-s"], { encoding: "utf8" });
@@ -17924,7 +18544,7 @@ function cmdBuild(args) {
17924
18544
  process.stderr.write(m5("slides_build.usage_roll_slides_build_slug_no") + "\n");
17925
18545
  return 1;
17926
18546
  }
17927
- const deck = join41(".roll", "slides", slug, "deck.md");
18547
+ const deck = join42(".roll", "slides", slug, "deck.md");
17928
18548
  if (!isFile3(deck)) {
17929
18549
  err15(`Deck not found: ${deck}`);
17930
18550
  process.stderr.write(m5("slides_build.en_deck", deck) + "\n");
@@ -17934,14 +18554,14 @@ function cmdBuild(args) {
17934
18554
  return 1;
17935
18555
  }
17936
18556
  const libDir = slidesLib();
17937
- const validator = join41(libDir, "slides-validate.py");
17938
- const renderer = join41(libDir, "slides-render.py");
18557
+ const validator = join42(libDir, "slides-validate.py");
18558
+ const renderer = join42(libDir, "slides-render.py");
17939
18559
  if (!isFile3(validator) || !isFile3(renderer)) {
17940
18560
  err15(m5("slides_build.slides_toolchain_missing_re_run_roll"));
17941
18561
  return 1;
17942
18562
  }
17943
- const errFile = join41(".roll", "slides", slug, ".last-build.err");
17944
- const componentsDir = join41(libDir, "slides", "components");
18563
+ const errFile = join42(".roll", "slides", slug, ".last-build.err");
18564
+ const componentsDir = join42(libDir, "slides", "components");
17945
18565
  const valLines = [];
17946
18566
  const valExit = validateDeckFile(deck, (l) => valLines.push(l), componentsDir);
17947
18567
  const valOut = valLines.join("\n");
@@ -17950,8 +18570,8 @@ function cmdBuild(args) {
17950
18570
  `);
17951
18571
  } else if (valExit !== 0) {
17952
18572
  const ts = utcTs();
17953
- mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
17954
- writeFileSync23(errFile, `[${ts}] stage=validate
18573
+ mkdirSync24(join42(".roll", "slides", slug), { recursive: true });
18574
+ writeFileSync24(errFile, `[${ts}] stage=validate
17955
18575
  ${valOut}
17956
18576
  `);
17957
18577
  if (valOut !== "")
@@ -17968,15 +18588,15 @@ ${valOut}
17968
18588
  const tplPath = slidesTemplatePath(tplName);
17969
18589
  if (tplPath === null) {
17970
18590
  const ts = utcTs();
17971
- mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
17972
- writeFileSync23(errFile, `[${ts}] stage=template
18591
+ mkdirSync24(join42(".roll", "slides", slug), { recursive: true });
18592
+ writeFileSync24(errFile, `[${ts}] stage=template
17973
18593
  template not found: ${tplName}
17974
18594
  `);
17975
18595
  const { RED, NC } = pal4();
17976
18596
  process.stderr.write(`${RED}[FAIL]${NC} ${m5("slides_build.template_not_found", tplName)}
17977
18597
  `);
17978
18598
  process.stderr.write(" " + m5("slides_build.available_templates") + "\n");
17979
- const builtinDir = join41(rollPkgDir(), "lib", "slides", "templates");
18599
+ const builtinDir = join42(rollPkgDir(), "lib", "slides", "templates");
17980
18600
  if (isDir3(builtinDir)) {
17981
18601
  for (const t2 of listHtml(builtinDir)) {
17982
18602
  const n = basenameNoHtml(t2);
@@ -17984,7 +18604,7 @@ template not found: ${tplName}
17984
18604
  `);
17985
18605
  }
17986
18606
  }
17987
- const projDir = join41(".roll", "slides", "templates");
18607
+ const projDir = join42(".roll", "slides", "templates");
17988
18608
  if (isDir3(projDir)) {
17989
18609
  for (const t2 of listHtml(projDir)) {
17990
18610
  const n = basenameNoHtml(t2);
@@ -17995,18 +18615,18 @@ template not found: ${tplName}
17995
18615
  process.stderr.write(" " + m5("slides_build.templates_list_hint") + "\n");
17996
18616
  return 1;
17997
18617
  }
17998
- const out2 = join41(".roll", "slides", `${slug}.html`);
17999
- mkdirSync23(join41(".roll", "slides"), { recursive: true });
18618
+ const out2 = join42(".roll", "slides", `${slug}.html`);
18619
+ mkdirSync24(join42(".roll", "slides"), { recursive: true });
18000
18620
  let htmlOut;
18001
18621
  try {
18002
- const src = readFileSync37(deck, "utf8");
18003
- const template = readFileSync37(tplPath, "utf8");
18622
+ const src = readFileSync38(deck, "utf8");
18623
+ const template = readFileSync38(tplPath, "utf8");
18004
18624
  htmlOut = renderDeck(src, template, { componentsDir });
18005
18625
  } catch (e) {
18006
18626
  const renderOut = renderErrorBlock(e);
18007
18627
  const ts = utcTs();
18008
- mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
18009
- writeFileSync23(errFile, `[${ts}] stage=render
18628
+ mkdirSync24(join42(".roll", "slides", slug), { recursive: true });
18629
+ writeFileSync24(errFile, `[${ts}] stage=render
18010
18630
  ${renderOut}
18011
18631
  `);
18012
18632
  const { RED, NC } = pal4();
@@ -18020,7 +18640,7 @@ ${renderOut}
18020
18640
  }
18021
18641
  return 1;
18022
18642
  }
18023
- writeFileSync23(out2, htmlOut);
18643
+ writeFileSync24(out2, htmlOut);
18024
18644
  try {
18025
18645
  rmSync11(errFile, { force: true });
18026
18646
  } catch {
@@ -18045,7 +18665,7 @@ function tailLines(s, n) {
18045
18665
  }
18046
18666
  function frontmatterField2(deckPath, field) {
18047
18667
  try {
18048
- const src = readFileSync37(deckPath, "utf8");
18668
+ const src = readFileSync38(deckPath, "utf8");
18049
18669
  let d = 0;
18050
18670
  const pat = new RegExp("^" + field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[ ]*:[ ]*");
18051
18671
  for (const line of src.split("\n")) {
@@ -18088,7 +18708,7 @@ function cmdList(args) {
18088
18708
  err15(m5("slides_list.unexpected_argument_1"));
18089
18709
  return 1;
18090
18710
  }
18091
- const slidesDir = join41(".roll", "slides");
18711
+ const slidesDir = join42(".roll", "slides");
18092
18712
  if (!isDir3(slidesDir)) {
18093
18713
  info5(m5("slides_list.no_decks_found_under_roll_slides"));
18094
18714
  process.stdout.write(` Hint: run 'roll slides new "<topic>"' to create one.
@@ -18098,8 +18718,8 @@ function cmdList(args) {
18098
18718
  }
18099
18719
  const slugs = [];
18100
18720
  for (const entry of readdirSync19(slidesDir)) {
18101
- const d = join41(slidesDir, entry);
18102
- if (isDir3(d) && isFile3(join41(d, "deck.md")))
18721
+ const d = join42(slidesDir, entry);
18722
+ if (isDir3(d) && isFile3(join42(d, "deck.md")))
18103
18723
  slugs.push(entry);
18104
18724
  }
18105
18725
  if (slugs.length === 0) {
@@ -18113,9 +18733,9 @@ function cmdList(args) {
18113
18733
  process.stdout.write(rowSix("slug", "template", "total_slides", "created", "built", "size") + "\n");
18114
18734
  process.stdout.write(rowSix("----", "--------", "------------", "-------", "------", "----") + "\n");
18115
18735
  for (const s of sorted) {
18116
- const deck = join41(slidesDir, s, "deck.md");
18117
- const html = join41(slidesDir, `${s}.html`);
18118
- const errFile = join41(slidesDir, s, ".last-build.err");
18736
+ const deck = join42(slidesDir, s, "deck.md");
18737
+ const html = join42(slidesDir, `${s}.html`);
18738
+ const errFile = join42(slidesDir, s, ".last-build.err");
18119
18739
  let template = frontmatterField2(deck, "template");
18120
18740
  if (template === "")
18121
18741
  template = "-";
@@ -18138,7 +18758,7 @@ function cmdList(args) {
18138
18758
  built = "\u2713 built";
18139
18759
  let bytes = 0;
18140
18760
  try {
18141
- bytes = statSync21(html).size;
18761
+ bytes = statSync22(html).size;
18142
18762
  } catch {
18143
18763
  bytes = 0;
18144
18764
  }
@@ -18154,7 +18774,7 @@ function cmdList(args) {
18154
18774
  }
18155
18775
  function isNewer(a, b) {
18156
18776
  try {
18157
- return statSync21(a).mtimeMs > statSync21(b).mtimeMs;
18777
+ return statSync22(a).mtimeMs > statSync22(b).mtimeMs;
18158
18778
  } catch {
18159
18779
  return false;
18160
18780
  }
@@ -18189,7 +18809,7 @@ function cmdPreview(args) {
18189
18809
  process.stderr.write(m5("slides_preview.usage_roll_slides_preview_slug_no") + "\n");
18190
18810
  return 1;
18191
18811
  }
18192
- const html = join41(".roll", "slides", `${slug}.html`);
18812
+ const html = join42(".roll", "slides", `${slug}.html`);
18193
18813
  if (!isFile3(html)) {
18194
18814
  err15(`Rendered HTML not found: ${html}`);
18195
18815
  process.stderr.write(m5("slides_preview.en_html", html) + "\n");
@@ -18230,9 +18850,9 @@ function cmdLogs(args) {
18230
18850
  process.stderr.write(m5("slides_logs.usage_roll_slides_logs_slug") + "\n");
18231
18851
  return 1;
18232
18852
  }
18233
- const deckDir = join41(".roll", "slides", slug);
18234
- const errFile = join41(deckDir, ".last-build.err");
18235
- if (!isDir3(deckDir) || !isFile3(join41(deckDir, "deck.md"))) {
18853
+ const deckDir = join42(".roll", "slides", slug);
18854
+ const errFile = join42(deckDir, ".last-build.err");
18855
+ if (!isDir3(deckDir) || !isFile3(join42(deckDir, "deck.md"))) {
18236
18856
  err15(m5("slides_logs.deck_not_found", slug));
18237
18857
  return 1;
18238
18858
  }
@@ -18240,7 +18860,7 @@ function cmdLogs(args) {
18240
18860
  info5(m5("slides_logs.no_failure_records_for", slug));
18241
18861
  return 0;
18242
18862
  }
18243
- process.stdout.write(readFileSync37(errFile, "utf8"));
18863
+ process.stdout.write(readFileSync38(errFile, "utf8"));
18244
18864
  return 0;
18245
18865
  }
18246
18866
  function cmdDelete(args) {
@@ -18273,9 +18893,9 @@ function cmdDelete(args) {
18273
18893
  process.stderr.write(m5("slides_delete.usage_roll_slides_delete_slug_force") + "\n");
18274
18894
  return 1;
18275
18895
  }
18276
- const deckDir = join41(".roll", "slides", slug);
18277
- const html = join41(".roll", "slides", `${slug}.html`);
18278
- if (!isDir3(deckDir) || !isFile3(join41(deckDir, "deck.md"))) {
18896
+ const deckDir = join42(".roll", "slides", slug);
18897
+ const html = join42(".roll", "slides", `${slug}.html`);
18898
+ if (!isDir3(deckDir) || !isFile3(join42(deckDir, "deck.md"))) {
18279
18899
  err15(m5("slides_delete.deck_not_found", slug));
18280
18900
  return 1;
18281
18901
  }
@@ -18314,7 +18934,7 @@ function cmdTemplates(args) {
18314
18934
  let found = false;
18315
18935
  process.stdout.write(rowThree("name", "source", "path") + "\n");
18316
18936
  process.stdout.write(rowThree("----", "------", "----") + "\n");
18317
- const builtinDir = join41(rollPkgDir(), "lib", "slides", "templates");
18937
+ const builtinDir = join42(rollPkgDir(), "lib", "slides", "templates");
18318
18938
  if (isDir3(builtinDir)) {
18319
18939
  for (const tpl of listHtml(builtinDir)) {
18320
18940
  const name = basenameNoHtml(tpl);
@@ -18322,11 +18942,11 @@ function cmdTemplates(args) {
18322
18942
  found = true;
18323
18943
  }
18324
18944
  }
18325
- const projDir = join41(".roll", "slides", "templates");
18945
+ const projDir = join42(".roll", "slides", "templates");
18326
18946
  if (isDir3(projDir)) {
18327
18947
  for (const tpl of listHtml(projDir)) {
18328
18948
  const name = basenameNoHtml(tpl);
18329
- const source = isFile3(join41(builtinDir, `${name}.html`)) ? "project (override)" : "project";
18949
+ const source = isFile3(join42(builtinDir, `${name}.html`)) ? "project (override)" : "project";
18330
18950
  process.stdout.write(rowThree(name, source, tpl) + "\n");
18331
18951
  found = true;
18332
18952
  }
@@ -18374,20 +18994,20 @@ function slidesCommand(args) {
18374
18994
  }
18375
18995
  function isFile3(p) {
18376
18996
  try {
18377
- return statSync21(p).isFile();
18997
+ return statSync22(p).isFile();
18378
18998
  } catch {
18379
18999
  return false;
18380
19000
  }
18381
19001
  }
18382
19002
  function isDir3(p) {
18383
19003
  try {
18384
- return statSync21(p).isDirectory();
19004
+ return statSync22(p).isDirectory();
18385
19005
  } catch {
18386
19006
  return false;
18387
19007
  }
18388
19008
  }
18389
19009
  function listHtml(dir) {
18390
- return readdirSync19(dir).filter((f) => f.endsWith(".html")).sort().map((f) => join41(dir, f));
19010
+ return readdirSync19(dir).filter((f) => f.endsWith(".html")).sort().map((f) => join42(dir, f));
18391
19011
  }
18392
19012
  function basenameNoHtml(path) {
18393
19013
  const b = path.slice(path.lastIndexOf("/") + 1);
@@ -18405,21 +19025,21 @@ function rowThree(a, b, c2) {
18405
19025
  }
18406
19026
 
18407
19027
  // packages/cli/dist/commands/status.js
18408
- import { execFileSync as execFileSync8 } from "node:child_process";
19028
+ import { execFileSync as execFileSync9 } from "node:child_process";
18409
19029
  import { createHash as createHash5 } from "node:crypto";
18410
- import { existsSync as existsSync45, lstatSync as lstatSync4, readdirSync as readdirSync20, readFileSync as readFileSync38, realpathSync as realpathSync6, statSync as statSync22 } from "node:fs";
19030
+ import { existsSync as existsSync46, lstatSync as lstatSync4, readdirSync as readdirSync20, readFileSync as readFileSync39, realpathSync as realpathSync6, statSync as statSync23 } from "node:fs";
18411
19031
  import { homedir as homedir17 } from "node:os";
18412
- import { basename as basename10, dirname as dirname19, join as join42 } from "node:path";
19032
+ import { basename as basename10, dirname as dirname19, join as join43 } from "node:path";
18413
19033
  function rollHome3() {
18414
- return process.env["ROLL_HOME"] ?? join42(homedir17(), ".roll");
19034
+ return process.env["ROLL_HOME"] ?? join43(homedir17(), ".roll");
18415
19035
  }
18416
- var globalDir = () => join42(rollHome3(), "conventions", "global");
18417
- var templatesDir = () => join42(rollHome3(), "conventions", "templates");
18418
- var configPath = () => join42(rollHome3(), "config.yaml");
19036
+ var globalDir = () => join43(rollHome3(), "conventions", "global");
19037
+ var templatesDir = () => join43(rollHome3(), "conventions", "templates");
19038
+ var configPath = () => join43(rollHome3(), "config.yaml");
18419
19039
  function projectSlugPy() {
18420
19040
  let path = realpathSync6(process.cwd());
18421
19041
  try {
18422
- const common = execFileSync8("git", ["-C", path, "rev-parse", "--git-common-dir"], {
19042
+ const common = execFileSync9("git", ["-C", path, "rev-parse", "--git-common-dir"], {
18423
19043
  encoding: "utf8",
18424
19044
  stdio: ["ignore", "pipe", "ignore"]
18425
19045
  }).trim();
@@ -18434,14 +19054,14 @@ function projectSlugPy() {
18434
19054
  var CONVENTION_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursor-rules", "project_rules.md"];
18435
19055
  var TEMPLATES = ["fullstack", "frontend-only", "backend-service", "cli"];
18436
19056
  function globalConventions() {
18437
- return CONVENTION_FILES.map((f) => [f, existsSync45(join42(globalDir(), f))]);
19057
+ return CONVENTION_FILES.map((f) => [f, existsSync46(join43(globalDir(), f))]);
18438
19058
  }
18439
19059
  function parseAiEntries() {
18440
19060
  const cfg = configPath();
18441
- if (!existsSync45(cfg))
19061
+ if (!existsSync46(cfg))
18442
19062
  return [];
18443
19063
  const entries = [];
18444
- for (const line of readFileSync38(cfg, "utf8").split("\n")) {
19064
+ for (const line of readFileSync39(cfg, "utf8").split("\n")) {
18445
19065
  const m7 = /^ai_[a-z]+:\s*(.+)/.exec(line);
18446
19066
  if (m7 === null)
18447
19067
  continue;
@@ -18461,21 +19081,21 @@ function parseAiEntries() {
18461
19081
  return entries;
18462
19082
  }
18463
19083
  function aiSyncStatus(e) {
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))
19084
+ const cfgFile = join43(e.ai_dir, e.cfg_file);
19085
+ const rollMd = join43(e.ai_dir, "roll.md");
19086
+ const src = join43(globalDir(), e.src_file);
19087
+ if (!existsSync46(cfgFile))
18468
19088
  return "missing";
18469
- if (!existsSync45(rollMd))
19089
+ if (!existsSync46(rollMd))
18470
19090
  return "out-of-sync";
18471
19091
  try {
18472
- if (existsSync45(src) && !readFileSync38(rollMd).equals(readFileSync38(src)))
19092
+ if (existsSync46(src) && !readFileSync39(rollMd).equals(readFileSync39(src)))
18473
19093
  return "out-of-sync";
18474
19094
  } catch {
18475
19095
  return "out-of-sync";
18476
19096
  }
18477
19097
  try {
18478
- if (!readFileSync38(cfgFile, "utf8").includes("@roll.md"))
19098
+ if (!readFileSync39(cfgFile, "utf8").includes("@roll.md"))
18479
19099
  return "out-of-sync";
18480
19100
  } catch {
18481
19101
  return "out-of-sync";
@@ -18483,15 +19103,15 @@ function aiSyncStatus(e) {
18483
19103
  return "sync";
18484
19104
  }
18485
19105
  function aiSkillCount(e) {
18486
- const skillsDir2 = join42(e.ai_dir, "skills");
18487
- if (!existsSync45(skillsDir2))
19106
+ const skillsDir2 = join43(e.ai_dir, "skills");
19107
+ if (!existsSync46(skillsDir2))
18488
19108
  return 0;
18489
19109
  try {
18490
19110
  let n = 0;
18491
19111
  for (const name of readdirSync20(skillsDir2)) {
18492
19112
  if (!name.startsWith("roll-"))
18493
19113
  continue;
18494
- const p = join42(skillsDir2, name);
19114
+ const p = join43(skillsDir2, name);
18495
19115
  const st = lstatSync4(p);
18496
19116
  if (st.isSymbolicLink() || st.isDirectory())
18497
19117
  n++;
@@ -18504,8 +19124,8 @@ function aiSkillCount(e) {
18504
19124
  function countFilesRecursive(dir) {
18505
19125
  let n = 0;
18506
19126
  for (const name of readdirSync20(dir)) {
18507
- const p = join42(dir, name);
18508
- const st = statSync22(p);
19127
+ const p = join43(dir, name);
19128
+ const st = statSync23(p);
18509
19129
  if (st.isDirectory())
18510
19130
  n += countFilesRecursive(p);
18511
19131
  else if (st.isFile())
@@ -18514,8 +19134,8 @@ function countFilesRecursive(dir) {
18514
19134
  return n;
18515
19135
  }
18516
19136
  function templateCount(tpl) {
18517
- const d = join42(templatesDir(), tpl);
18518
- if (!existsSync45(d))
19137
+ const d = join43(templatesDir(), tpl);
19138
+ if (!existsSync46(d))
18519
19139
  return 0;
18520
19140
  try {
18521
19141
  return countFilesRecursive(d);
@@ -18524,22 +19144,22 @@ function templateCount(tpl) {
18524
19144
  }
18525
19145
  }
18526
19146
  function skillsInstalled() {
18527
- const sd = join42(rollHome3(), "skills");
18528
- if (!existsSync45(sd))
19147
+ const sd = join43(rollHome3(), "skills");
19148
+ if (!existsSync46(sd))
18529
19149
  return 0;
18530
19150
  try {
18531
- return readdirSync20(sd).filter((n) => statSync22(join42(sd, n)).isDirectory()).length;
19151
+ return readdirSync20(sd).filter((n) => statSync23(join43(sd, n)).isDirectory()).length;
18532
19152
  } catch {
18533
19153
  return 0;
18534
19154
  }
18535
19155
  }
18536
19156
  function launchdState(service, slug) {
18537
19157
  const label4 = `com.roll.${service}.${slug}`;
18538
- const plist = join42(homedir17(), "Library", "LaunchAgents", `${label4}.plist`);
18539
- if (!existsSync45(plist))
19158
+ const plist = join43(homedir17(), "Library", "LaunchAgents", `${label4}.plist`);
19159
+ if (!existsSync46(plist))
18540
19160
  return "not-installed";
18541
19161
  try {
18542
- const out2 = execFileSync8("launchctl", ["list", label4], {
19162
+ const out2 = execFileSync9("launchctl", ["list", label4], {
18543
19163
  encoding: "utf8",
18544
19164
  stdio: ["ignore", "pipe", "ignore"]
18545
19165
  });
@@ -18674,13 +19294,13 @@ function liveData() {
18674
19294
  const aiClients = parseAiEntries().map((e) => ({
18675
19295
  name: e.name,
18676
19296
  cfg_file: e.cfg_file,
18677
- path: join42(e.ai_dir, e.cfg_file).replaceAll(home, "~"),
19297
+ path: join43(e.ai_dir, e.cfg_file).replaceAll(home, "~"),
18678
19298
  sync: aiSyncStatus(e),
18679
19299
  skills: aiSkillCount(e)
18680
19300
  }));
18681
19301
  const featDir = ".roll/features";
18682
19302
  let featCount = 0;
18683
- if (existsSync45(featDir)) {
19303
+ if (existsSync46(featDir)) {
18684
19304
  featCount = readdirSync20(featDir).filter((n) => n.endsWith(".md")).length;
18685
19305
  }
18686
19306
  return {
@@ -18688,8 +19308,8 @@ function liveData() {
18688
19308
  ai_clients: aiClients,
18689
19309
  templates: TEMPLATES.map((t2) => [t2, templateCount(t2)]),
18690
19310
  skills_installed: skillsInstalled(),
18691
- project_has_agents: existsSync45("AGENTS.md"),
18692
- project_has_backlog: existsSync45(".roll/backlog.md"),
19311
+ project_has_agents: existsSync46("AGENTS.md"),
19312
+ project_has_backlog: existsSync46(".roll/backlog.md"),
18693
19313
  project_features_count: featCount,
18694
19314
  loop_state: launchdState("loop", slug),
18695
19315
  dream_state: launchdState("dream", slug)
@@ -18713,8 +19333,8 @@ function statusCommand(args) {
18713
19333
 
18714
19334
  // packages/cli/dist/commands/test.js
18715
19335
  import { spawnSync as spawnSync11 } from "node:child_process";
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";
19336
+ import { existsSync as existsSync47, mkdirSync as mkdirSync25, readFileSync as readFileSync40, rmSync as rmSync12, writeFileSync as writeFileSync25 } from "node:fs";
19337
+ import { dirname as dirname20, join as join44 } from "node:path";
18718
19338
  function pal5() {
18719
19339
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
18720
19340
  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" };
@@ -18731,12 +19351,12 @@ function err16(line) {
18731
19351
  }
18732
19352
  var SUPPORTED_TYPES = ["none"];
18733
19353
  function isolationGetType() {
18734
- const file = join43(process.cwd(), ".roll", "local.yaml");
18735
- if (!existsSync46(file))
19354
+ const file = join44(process.cwd(), ".roll", "local.yaml");
19355
+ if (!existsSync47(file))
18736
19356
  return "none";
18737
19357
  let text;
18738
19358
  try {
18739
- text = readFileSync39(file, "utf8");
19359
+ text = readFileSync40(file, "utf8");
18740
19360
  } catch {
18741
19361
  return "none";
18742
19362
  }
@@ -18784,14 +19404,14 @@ function resetLockPath() {
18784
19404
  return ".roll/.iso-reset.lock";
18785
19405
  }
18786
19406
  function resetLockHeld() {
18787
- return existsSync46(resetLockPath());
19407
+ return existsSync47(resetLockPath());
18788
19408
  }
18789
19409
  function resetAcquireLock() {
18790
19410
  const lock = resetLockPath();
18791
- if (existsSync46(lock))
19411
+ if (existsSync47(lock))
18792
19412
  return false;
18793
- mkdirSync24(dirname20(lock), { recursive: true });
18794
- writeFileSync24(lock, `${process.pid}
19413
+ mkdirSync25(dirname20(lock), { recursive: true });
19414
+ writeFileSync25(lock, `${process.pid}
18795
19415
  `);
18796
19416
  return true;
18797
19417
  }
@@ -18808,7 +19428,7 @@ function runForward(cmd, argv) {
18808
19428
  }
18809
19429
  function isolationDispatch(method, args) {
18810
19430
  const type = isolationGetType();
18811
- if (type === "none" && !existsSync46(join43(process.cwd(), ".roll", "local.yaml"))) {
19431
+ if (type === "none" && !existsSync47(join44(process.cwd(), ".roll", "local.yaml"))) {
18812
19432
  infoErr("isolation: no test_isolation config, falling back to type=none (host)");
18813
19433
  }
18814
19434
  if (!SUPPORTED_TYPES.includes(type)) {
@@ -18902,9 +19522,9 @@ function testCommand(args) {
18902
19522
 
18903
19523
  // packages/cli/dist/commands/update.js
18904
19524
  import { spawnSync as spawnSync12 } from "node:child_process";
18905
- import { existsSync as existsSync47, mkdtempSync as mkdtempSync4, readFileSync as readFileSync40, rmSync as rmSync13 } from "node:fs";
19525
+ import { existsSync as existsSync48, mkdtempSync as mkdtempSync4, readFileSync as readFileSync41, rmSync as rmSync13 } from "node:fs";
18906
19526
  import { tmpdir as tmpdir4 } from "node:os";
18907
- import { join as join44 } from "node:path";
19527
+ import { join as join45 } from "node:path";
18908
19528
  function pal6() {
18909
19529
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
18910
19530
  return noColor2 ? { CYAN: "", GREEN: "", YELLOW: "", RED: "", BOLD: "", NC: "" } : {
@@ -18968,20 +19588,20 @@ function resolveRemoteVersion() {
18968
19588
  }
18969
19589
  function downloadAndInstallCurl(tag) {
18970
19590
  const url = `https://github.com/seanyao/roll/archive/refs/tags/${tag}.tar.gz`;
18971
- const tmpDir = mkdtempSync4(join44(tmpdir4(), "roll-update-"));
19591
+ const tmpDir = mkdtempSync4(join45(tmpdir4(), "roll-update-"));
18972
19592
  try {
18973
19593
  info6(`[roll] Downloading roll ${tag} ...`);
18974
- const dl = runForward2("curl", ["-fsSL", url, "-o", join44(tmpDir, "roll.tar.gz")]);
19594
+ const dl = runForward2("curl", ["-fsSL", url, "-o", join45(tmpDir, "roll.tar.gz")]);
18975
19595
  if (dl !== 0) {
18976
19596
  err17(m6("update.curl_download_failed"));
18977
19597
  return { ok: false };
18978
19598
  }
18979
19599
  info6("[roll] Extracting ...");
18980
- const extractDir = join44(tmpDir, "extract");
19600
+ const extractDir = join45(tmpDir, "extract");
18981
19601
  spawnSync12("mkdir", ["-p", extractDir], { stdio: "ignore" });
18982
19602
  const ex = runForward2("tar", [
18983
19603
  "-xzf",
18984
- join44(tmpDir, "roll.tar.gz"),
19604
+ join45(tmpDir, "roll.tar.gz"),
18985
19605
  "--strip-components=1",
18986
19606
  "-C",
18987
19607
  extractDir
@@ -18998,7 +19618,7 @@ function downloadAndInstallCurl(tag) {
18998
19618
  function checkInstalledVersionOrRetry() {
18999
19619
  const expected = (spawnSync12("npm", ["view", "@seanyao/roll", "version"], { encoding: "utf8" }).stdout ?? "").trim();
19000
19620
  const pkgRoot = (spawnSync12("npm", ["root", "-g"], { encoding: "utf8" }).stdout ?? "").trim();
19001
- const installedTree = join44(pkgRoot, "@seanyao", "roll");
19621
+ const installedTree = join45(pkgRoot, "@seanyao", "roll");
19002
19622
  const installed = treeVersion(installedTree);
19003
19623
  if (expected === "" || installed === "")
19004
19624
  return;
@@ -19012,18 +19632,18 @@ function checkInstalledVersionOrRetry() {
19012
19632
  }
19013
19633
  }
19014
19634
  function invalidateUpdateCache() {
19015
- rmSync13(join44(rollHome(), ".update-check"), { force: true });
19635
+ rmSync13(join45(rollHome(), ".update-check"), { force: true });
19016
19636
  }
19017
19637
  function showChangelog() {
19018
- const changelog = join44(rollPkgDir(), "CHANGELOG.md");
19019
- if (!existsSync47(changelog))
19638
+ const changelog = join45(rollPkgDir(), "CHANGELOG.md");
19639
+ if (!existsSync48(changelog))
19020
19640
  return;
19021
19641
  const { BOLD: BOLD2, CYAN, NC } = pal6();
19022
19642
  process.stdout.write(`${BOLD2}${m6("changelog.heading")}:${NC}
19023
19643
  `);
19024
19644
  let count = 0;
19025
19645
  let inSection = false;
19026
- for (const line of readFileSync40(changelog, "utf8").split("\n")) {
19646
+ for (const line of readFileSync41(changelog, "utf8").split("\n")) {
19027
19647
  if (/^## /.test(line)) {
19028
19648
  count += 1;
19029
19649
  if (count > 3)
@@ -19043,9 +19663,9 @@ function updateCommand(args) {
19043
19663
  void args;
19044
19664
  info6(m6("update.current_version", rollVersion()));
19045
19665
  let installMethod = "npm";
19046
- const methodFile = join44(rollPkgDir(), ".install-method");
19047
- if (existsSync47(methodFile)) {
19048
- installMethod = readFileSync40(methodFile, "utf8").trim() || "npm";
19666
+ const methodFile = join45(rollPkgDir(), ".install-method");
19667
+ if (existsSync48(methodFile)) {
19668
+ installMethod = readFileSync41(methodFile, "utf8").trim() || "npm";
19049
19669
  }
19050
19670
  if (installMethod === "curl") {
19051
19671
  info6(m6("update.upgrading_via_curl"));
@@ -19092,6 +19712,12 @@ function registerAll() {
19092
19712
  registerPorted("doctor", doctorCommand);
19093
19713
  registerPorted("attest", attestCommand);
19094
19714
  registerPorted("index", indexCommand);
19715
+ registerPorted("story", (args) => {
19716
+ if (args[0] === "new")
19717
+ return storyNewCommand(args.slice(1));
19718
+ process.stdout.write("Usage: roll story new <ID> --title <text> [--epic <epic>] [--note <text>]\n");
19719
+ return args[0] === void 0 || args[0] === "--help" || args[0] === "-h" ? 0 : 1;
19720
+ });
19095
19721
  registerPorted("gc", gcCommand);
19096
19722
  registerPorted("archive", (args) => {
19097
19723
  if (args[0] === "migrate")