@seanyao/roll 3.607.1 → 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
@@ -3028,6 +3028,21 @@ function agentBinNames(agent) {
3028
3028
  return null;
3029
3029
  }
3030
3030
  }
3031
+ function realAgentEnv() {
3032
+ return {
3033
+ home: homedir(),
3034
+ commandOnPath,
3035
+ dirExists: (p) => existsSync2(p),
3036
+ fileExecutable: (p) => {
3037
+ try {
3038
+ accessSync(p, constants.X_OK);
3039
+ return statSync(p).isFile();
3040
+ } catch {
3041
+ return false;
3042
+ }
3043
+ }
3044
+ };
3045
+ }
3031
3046
  function commandOnPath(bin) {
3032
3047
  for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
3033
3048
  if (dir === "")
@@ -3130,6 +3145,115 @@ var nodeFileStore = {
3130
3145
  }
3131
3146
  };
3132
3147
 
3148
+ // packages/core/dist/agent/registry.js
3149
+ function agentBinNames2(agent) {
3150
+ switch (agent) {
3151
+ case "claude":
3152
+ return ["claude"];
3153
+ case "codex":
3154
+ case "openai":
3155
+ return ["codex"];
3156
+ case "agy":
3157
+ case "gemini":
3158
+ return ["agy", "gemini"];
3159
+ case "kimi":
3160
+ return ["kimi-code", "kimi-cli", "kimi"];
3161
+ case "deepseek":
3162
+ return ["deepseek"];
3163
+ case "qwen":
3164
+ return ["qwen"];
3165
+ case "pi":
3166
+ return ["pi"];
3167
+ default:
3168
+ return null;
3169
+ }
3170
+ }
3171
+ function joinPath(...parts) {
3172
+ return parts.join("/");
3173
+ }
3174
+ function agentInstalledByName2(env, agent, dir) {
3175
+ const home = env.home;
3176
+ switch (agent) {
3177
+ case "trae":
3178
+ return env.dirExists(joinPath(home, "Library", "Application Support", "Trae")) || env.dirExists(joinPath(home, ".config", "Trae"));
3179
+ case "opencode":
3180
+ return env.fileExecutable(joinPath(home, ".opencode", "bin", "opencode"));
3181
+ case "cursor":
3182
+ return env.commandOnPath("cursor") || env.dirExists(joinPath(home, ".cursor"));
3183
+ case "openclaw":
3184
+ return env.dirExists(joinPath(home, ".openclaw", "workspace"));
3185
+ }
3186
+ const bins = agentBinNames2(agent);
3187
+ if (bins !== null)
3188
+ return bins.some((b) => env.commandOnPath(b));
3189
+ return dir !== void 0 && dir !== "" && env.dirExists(dir);
3190
+ }
3191
+ var FIRST_INSTALLED_ORDER = [
3192
+ "claude",
3193
+ "codex",
3194
+ "kimi",
3195
+ "deepseek",
3196
+ "qwen",
3197
+ "agy",
3198
+ "pi",
3199
+ "cursor",
3200
+ "opencode",
3201
+ "trae",
3202
+ "openclaw"
3203
+ ];
3204
+ function firstInstalledAgent(env) {
3205
+ return FIRST_INSTALLED_ORDER.find((a) => agentInstalledByName2(env, a));
3206
+ }
3207
+ function lineAgentValue(line) {
3208
+ const s = line.replace(/\t/g, " ");
3209
+ if (s.startsWith("agent:"))
3210
+ return s.slice("agent:".length);
3211
+ const m7 = /[ ,{]agent:/.exec(s);
3212
+ if (m7 === null)
3213
+ return void 0;
3214
+ return s.slice(m7.index + m7[0].length);
3215
+ }
3216
+ function cleanAgentValue(raw) {
3217
+ let v = raw;
3218
+ const hash = v.indexOf("#");
3219
+ if (hash >= 0)
3220
+ v = v.slice(0, hash);
3221
+ v = v.replace(/[{}",']/g, "").replace(/,/g, "");
3222
+ return v.trim();
3223
+ }
3224
+ function readSlotFromText(text, slot) {
3225
+ let inBlock = false;
3226
+ let agent = "";
3227
+ let found = false;
3228
+ const slotHeader = `${slot}:`;
3229
+ for (const raw of text.split("\n")) {
3230
+ const line = raw.replace(/\r$/, "");
3231
+ if (line === slotHeader || line.startsWith(`${slot}: `) || line.startsWith(`${slot}:{`)) {
3232
+ inBlock = true;
3233
+ const v = lineAgentValue(line);
3234
+ if (v !== void 0) {
3235
+ agent = v;
3236
+ found = true;
3237
+ }
3238
+ } else if (line.length > 0 && line[0] !== " ") {
3239
+ if (inBlock)
3240
+ break;
3241
+ } else if (inBlock && !found) {
3242
+ const v = lineAgentValue(line);
3243
+ if (v !== void 0) {
3244
+ agent = v;
3245
+ found = true;
3246
+ }
3247
+ }
3248
+ if (found)
3249
+ break;
3250
+ }
3251
+ if (!inBlock)
3252
+ return void 0;
3253
+ const cleaned = cleanAgentValue(agent);
3254
+ return cleaned === "" ? void 0 : cleaned;
3255
+ }
3256
+
3133
3257
  // packages/core/dist/agent/router.js
3134
3258
  var EASY_MAX_MIN = 8;
3135
3259
  var HARD_MIN_MIN = 20;
@@ -5988,11 +6112,11 @@ import { spawnSync as spawnSync2 } from "node:child_process";
5988
6112
  import { dirname as dirname5, join as join7 } from "node:path";
5989
6113
 
5990
6114
  // packages/cli/dist/lib/archive-migrate.js
5991
- 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";
5992
6116
  import { join as join6 } from "node:path";
5993
6117
 
5994
6118
  // packages/cli/dist/lib/archive.js
5995
- 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";
5996
6120
  import { basename as basename3, dirname as dirname4, join as join5 } from "node:path";
5997
6121
  var UNCATEGORIZED = "uncategorized";
5998
6122
  function findFeatureFile(projectPath, storyId) {
@@ -6006,7 +6130,8 @@ function findFeatureFile(projectPath, storyId) {
6006
6130
  if (e.isDirectory())
6007
6131
  walk2(p);
6008
6132
  else if (e.isFile() && e.name.endsWith(".md")) {
6009
- if (e.name === `${storyId}.md`)
6133
+ const idOwned = e.name === `${storyId}.md` || e.name === "spec.md" && basename3(dir) === storyId;
6134
+ if (idOwned)
6010
6135
  hits.unshift(p);
6011
6136
  else {
6012
6137
  try {
@@ -6028,14 +6153,22 @@ function findFeatureFile(projectPath, storyId) {
6028
6153
  function reportFileName(storyId) {
6029
6154
  return `${storyId}-report.html`;
6030
6155
  }
6156
+ function isEpicName(name) {
6157
+ return name !== "" && name !== "." && name !== "features";
6158
+ }
6159
+ function epicFromFeaturePath(featureFile) {
6160
+ const storyDir = dirname4(featureFile);
6161
+ const epic = basename3(dirname4(storyDir));
6162
+ if (isEpicName(epic))
6163
+ return epic;
6164
+ const legacy = basename3(storyDir);
6165
+ return isEpicName(legacy) ? legacy : null;
6166
+ }
6031
6167
  function liveEpicOf(projectPath, storyId) {
6032
6168
  const file = findFeatureFile(projectPath, storyId);
6033
6169
  if (file === null)
6034
6170
  return null;
6035
- const epic = basename3(dirname4(file));
6036
- if (epic === "" || epic === "." || epic === "features")
6037
- return null;
6038
- return epic;
6171
+ return epicFromFeaturePath(file);
6039
6172
  }
6040
6173
  function readIndex(projectPath) {
6041
6174
  const p = join5(projectPath, ".roll", "index.json");
@@ -6078,9 +6211,6 @@ function cardArchiveDir(projectPath, storyId) {
6078
6211
  const epic = epicForStory(projectPath, storyId) ?? UNCATEGORIZED;
6079
6212
  return join5(projectPath, ".roll", "features", epic, storyId);
6080
6213
  }
6081
- function legacyArchiveDir(projectPath, storyId) {
6082
- return join5(projectPath, ".roll", "verification", storyId);
6083
- }
6084
6214
  function buildStoryIndex(ids, epicOf) {
6085
6215
  const out2 = {};
6086
6216
  for (const id of ids) {
@@ -6096,6 +6226,69 @@ function serializeIndex(stories) {
6096
6226
  sorted[k] = stories[k];
6097
6227
  return JSON.stringify({ stories: sorted }, null, 2) + "\n";
6098
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
+ }
6099
6292
 
6100
6293
  // packages/cli/dist/lib/archive-migrate.js
6101
6294
  var RUN_ID_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/;
@@ -6111,7 +6304,7 @@ function runsInDir(absDir) {
6111
6304
  if (!e.isDirectory() || !RUN_ID_RE.test(e.name))
6112
6305
  continue;
6113
6306
  try {
6114
- 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) });
6115
6308
  } catch {
6116
6309
  }
6117
6310
  }
@@ -6818,7 +7011,7 @@ async function runPublishPlan(plan, opts = {}) {
6818
7011
  }
6819
7012
 
6820
7013
  // packages/infra/dist/process.js
6821
- 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";
6822
7015
  import { dirname as dirname8 } from "node:path";
6823
7016
  var OUTER_LOCK_STALE_SEC = 900;
6824
7017
  var INNER_LOCK_STALE_SEC = 14400;
@@ -6994,7 +7187,7 @@ import { promisify as promisify4 } from "node:util";
6994
7187
  var execFileAsync4 = promisify4(execFile4);
6995
7188
 
6996
7189
  // packages/infra/dist/evidence.js
6997
- 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";
6998
7191
  import { join as join9 } from "node:path";
6999
7192
  import { execFile as execFile5 } from "node:child_process";
7000
7193
  import { promisify as promisify5 } from "node:util";
@@ -7051,13 +7244,13 @@ async function collectEvidence(opts) {
7051
7244
  const proof = join9(opts.projectPath, ".roll", "last-test-pass");
7052
7245
  if (existsSync12(proof)) {
7053
7246
  try {
7054
- 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));
7055
7248
  testPass = { present: true, age_seconds: age };
7056
7249
  } catch {
7057
7250
  testPass = { present: true, age_seconds: -1 };
7058
7251
  }
7059
7252
  }
7060
- const listDir = (sub, ext) => {
7253
+ const listDir2 = (sub, ext) => {
7061
7254
  const dir = join9(opts.runDir, sub);
7062
7255
  if (!existsSync12(dir))
7063
7256
  return [];
@@ -7074,8 +7267,8 @@ async function collectEvidence(opts) {
7074
7267
  ci,
7075
7268
  deploy,
7076
7269
  test_pass: testPass,
7077
- screenshots: listDir("screenshots", /\.png$/i),
7078
- texts: listDir("evidence", /\.(txt|log)$/i)
7270
+ screenshots: listDir2("screenshots", /\.png$/i),
7271
+ texts: listDir2("evidence", /\.(txt|log)$/i)
7079
7272
  };
7080
7273
  }
7081
7274
  function writeEvidenceJson(manifest, runDir) {
@@ -7086,7 +7279,7 @@ function writeEvidenceJson(manifest, runDir) {
7086
7279
  }
7087
7280
 
7088
7281
  // packages/infra/dist/screenshot.js
7089
- import { existsSync as existsSync13, statSync as statSync7 } from "node:fs";
7282
+ import { existsSync as existsSync13, statSync as statSync8 } from "node:fs";
7090
7283
  import { execFile as execFile6 } from "node:child_process";
7091
7284
  import { promisify as promisify6 } from "node:util";
7092
7285
 
@@ -7148,7 +7341,7 @@ var defaultRun2 = async (cmd, argv) => {
7148
7341
  };
7149
7342
  function fileNonEmpty(p) {
7150
7343
  try {
7151
- return existsSync13(p) && statSync7(p).size > 0;
7344
+ return existsSync13(p) && statSync8(p).size > 0;
7152
7345
  } catch {
7153
7346
  return false;
7154
7347
  }
@@ -7164,11 +7357,11 @@ function parseRegion(region) {
7164
7357
  return { x, y, w, h };
7165
7358
  }
7166
7359
  function terminalOpenScript(line, r) {
7167
- const esc2 = line.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7360
+ const esc5 = line.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7168
7361
  return [
7169
7362
  'tell application "Terminal"',
7170
7363
  " activate",
7171
- ` do script "${esc2}"`,
7364
+ ` do script "${esc5}"`,
7172
7365
  " delay 1.5",
7173
7366
  ` set bounds of front window to {${r.x}, ${r.y}, ${r.x + r.w}, ${r.y + r.h}}`,
7174
7367
  "end tell"
@@ -7243,7 +7436,7 @@ function screenshotEvidenceRef(result, href) {
7243
7436
 
7244
7437
  // packages/cli/dist/commands/attest.js
7245
7438
  import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync6, rmSync as rmSync5, symlinkSync as symlinkSync2, writeFileSync as writeFileSync7 } from "node:fs";
7246
- import { basename as basename4, dirname as dirname9, join as join10, relative } from "node:path";
7439
+ import { basename as basename4, join as join10, relative } from "node:path";
7247
7440
 
7248
7441
  // packages/cli/dist/lib/story-page.js
7249
7442
  var STORY_ID_RE = /^(FIX|US|IDEA|REFACTOR)-/;
@@ -7537,11 +7730,11 @@ function buildCardContext(projectPath, featureFile, storyId, env) {
7537
7730
  summary = extractSummary(readFileSync9(featureFile, "utf8"));
7538
7731
  } catch {
7539
7732
  }
7540
- const epic = basename4(dirname9(featureFile));
7733
+ const epic = epicFromFeaturePath(featureFile);
7541
7734
  const cycleId = env.LOOP_CYCLE_ID;
7542
7735
  const ctx = {
7543
7736
  ...oneLiner !== void 0 && oneLiner !== "" ? { oneLiner } : {},
7544
- ...epic !== "" && epic !== "." && epic !== "features" ? { epic } : {},
7737
+ ...epic !== null ? { epic } : {},
7545
7738
  ...summary !== void 0 ? { summary } : {},
7546
7739
  ...row2.status !== void 0 ? { backlogStatus: row2.status } : {},
7547
7740
  ...cycleId !== void 0 && cycleId !== "" ? { delivery: { cycleId } } : {}
@@ -7709,6 +7902,10 @@ async function attestCommand(args, deps = {}) {
7709
7902
  \u9A8C\u6536\u62A5\u544A\u5DF2\u751F\u6210
7710
7903
  ${relative(projectPath, reportPath)}
7711
7904
  `);
7905
+ try {
7906
+ generateIndex(projectPath);
7907
+ } catch {
7908
+ }
7712
7909
  if (!smoke.ok) {
7713
7910
  for (const p of smoke.problems)
7714
7911
  warn2(`render smoke: ${p}`);
@@ -8441,9 +8638,9 @@ function writeUnreleased(draft, cl) {
8441
8638
  import { spawnSync as spawnSync3 } from "node:child_process";
8442
8639
 
8443
8640
  // packages/cli/dist/commands/setup-shared.js
8444
- 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";
8445
8642
  import { homedir as homedir5 } from "node:os";
8446
- import { basename as basename5, dirname as dirname10, join as join12 } from "node:path";
8643
+ import { basename as basename5, dirname as dirname9, join as join12 } from "node:path";
8447
8644
  function rollHome() {
8448
8645
  return process.env["ROLL_HOME"] ?? join12(homedir5(), ".roll");
8449
8646
  }
@@ -8484,14 +8681,14 @@ function aiField(entry, idx) {
8484
8681
  }
8485
8682
  function canonicalDir(path) {
8486
8683
  try {
8487
- if (!statSync8(path).isDirectory())
8684
+ if (!statSync9(path).isDirectory())
8488
8685
  return null;
8489
8686
  return realpathSync2(path);
8490
8687
  } catch {
8491
8688
  return null;
8492
8689
  }
8493
8690
  }
8494
- function agentBinNames2(agent) {
8691
+ function agentBinNames3(agent) {
8495
8692
  switch (agent) {
8496
8693
  case "claude":
8497
8694
  return ["claude"];
@@ -8520,7 +8717,7 @@ function onPath(bin) {
8520
8717
  continue;
8521
8718
  const p = join12(dir, bin);
8522
8719
  try {
8523
- const st = statSync8(p);
8720
+ const st = statSync9(p);
8524
8721
  if (st.isFile() && (st.mode & 73) !== 0)
8525
8722
  return true;
8526
8723
  } catch {
@@ -8528,7 +8725,7 @@ function onPath(bin) {
8528
8725
  }
8529
8726
  return false;
8530
8727
  }
8531
- function agentInstalledByName2(agent, dir = "") {
8728
+ function agentInstalledByName3(agent, dir = "") {
8532
8729
  const home = homedir5();
8533
8730
  switch (agent) {
8534
8731
  case "trae":
@@ -8536,7 +8733,7 @@ function agentInstalledByName2(agent, dir = "") {
8536
8733
  case "opencode": {
8537
8734
  const p = join12(home, ".opencode", "bin", "opencode");
8538
8735
  try {
8539
- return existsSync18(p) && (statSync8(p).mode & 73) !== 0;
8736
+ return existsSync18(p) && (statSync9(p).mode & 73) !== 0;
8540
8737
  } catch {
8541
8738
  return false;
8542
8739
  }
@@ -8548,7 +8745,7 @@ function agentInstalledByName2(agent, dir = "") {
8548
8745
  default:
8549
8746
  break;
8550
8747
  }
8551
- const bins = agentBinNames2(agent);
8748
+ const bins = agentBinNames3(agent);
8552
8749
  if (bins !== null) {
8553
8750
  return bins.some((b) => onPath(b));
8554
8751
  }
@@ -8557,13 +8754,13 @@ function agentInstalledByName2(agent, dir = "") {
8557
8754
  function isAiInstalled(aiDir) {
8558
8755
  let bn = basename5(aiDir).replace(/^\./, "");
8559
8756
  if (bn === "agent" || bn === "workspace") {
8560
- bn = basename5(dirname10(aiDir)).replace(/^\./, "");
8757
+ bn = basename5(dirname9(aiDir)).replace(/^\./, "");
8561
8758
  }
8562
8759
  if (bn === "gemini")
8563
8760
  bn = "agy";
8564
8761
  if (bn === "kimi-code")
8565
8762
  bn = "kimi";
8566
- return agentInstalledByName2(bn, aiDir);
8763
+ return agentInstalledByName3(bn, aiDir);
8567
8764
  }
8568
8765
  function listDirEntries(dir) {
8569
8766
  try {
@@ -8581,7 +8778,7 @@ function pruneDir(installedDir, sourceDir) {
8581
8778
  const installedF = join12(installedDir, name);
8582
8779
  let isFile4 = false;
8583
8780
  try {
8584
- isFile4 = statSync8(installedF).isFile();
8781
+ isFile4 = statSync9(installedF).isFile();
8585
8782
  } catch {
8586
8783
  isFile4 = false;
8587
8784
  }
@@ -8594,7 +8791,7 @@ function pruneDir(installedDir, sourceDir) {
8594
8791
  function safeCopy(src, dst, force) {
8595
8792
  if (!existsSync18(src))
8596
8793
  return;
8597
- mkdirSync10(dirname10(dst), { recursive: true });
8794
+ mkdirSync10(dirname9(dst), { recursive: true });
8598
8795
  if (existsSync18(dst) && !force) {
8599
8796
  if (sameFile(src, dst))
8600
8797
  return;
@@ -8619,7 +8816,7 @@ function listFiles(dir, includeDot) {
8619
8816
  continue;
8620
8817
  const p = join12(dir, name);
8621
8818
  try {
8622
- if (statSync8(p).isFile())
8819
+ if (statSync9(p).isFile())
8623
8820
  out2.push(name);
8624
8821
  } catch {
8625
8822
  }
@@ -8646,7 +8843,7 @@ function pullConventions(force) {
8646
8843
  for (const tplName of listDirEntries(tplSrcRoot)) {
8647
8844
  const tplDir = join12(tplSrcRoot, tplName);
8648
8845
  try {
8649
- if (!statSync8(tplDir).isDirectory())
8846
+ if (!statSync9(tplDir).isDirectory())
8650
8847
  continue;
8651
8848
  } catch {
8652
8849
  continue;
@@ -8670,7 +8867,7 @@ function pullSkills() {
8670
8867
  for (const skillName of listDirEntries(pkgSkills)) {
8671
8868
  const skillDir = join12(pkgSkills, skillName);
8672
8869
  try {
8673
- if (!statSync8(skillDir).isDirectory())
8870
+ if (!statSync9(skillDir).isDirectory())
8674
8871
  continue;
8675
8872
  } catch {
8676
8873
  continue;
@@ -8680,7 +8877,7 @@ function pullSkills() {
8680
8877
  for (const name of listDirEntries(skillDir)) {
8681
8878
  const f = join12(skillDir, name);
8682
8879
  try {
8683
- if (statSync8(f).isFile())
8880
+ if (statSync9(f).isFile())
8684
8881
  copyFileSync(f, join12(destDir, name));
8685
8882
  } catch {
8686
8883
  }
@@ -8690,7 +8887,7 @@ function pullSkills() {
8690
8887
  for (const installedName of listDirEntries(homeSkills)) {
8691
8888
  const installedDir = join12(homeSkills, installedName);
8692
8889
  try {
8693
- if (!statSync8(installedDir).isDirectory())
8890
+ if (!statSync9(installedDir).isDirectory())
8694
8891
  continue;
8695
8892
  } catch {
8696
8893
  continue;
@@ -8771,7 +8968,7 @@ loop_dream_hour: 3
8771
8968
  # loop_dream_minute: 10 # omit to auto-derive
8772
8969
  primary_agent: claude
8773
8970
  `;
8774
- function firstInstalledAgent() {
8971
+ function firstInstalledAgent2() {
8775
8972
  for (const agent of [
8776
8973
  "claude",
8777
8974
  "codex",
@@ -8785,7 +8982,7 @@ function firstInstalledAgent() {
8785
8982
  "trae",
8786
8983
  "openclaw"
8787
8984
  ]) {
8788
- if (agentInstalledByName2(agent))
8985
+ if (agentInstalledByName3(agent))
8789
8986
  return agent;
8790
8987
  }
8791
8988
  return null;
@@ -8810,7 +9007,7 @@ function installLocal(force) {
8810
9007
  }
8811
9008
  if (!existsSync18(cfg)) {
8812
9009
  writeFileSync9(cfg, DEFAULT_CONFIG);
8813
- const detected = firstInstalledAgent();
9010
+ const detected = firstInstalledAgent2();
8814
9011
  if (detected !== null && detected !== "claude")
8815
9012
  replacePrimaryAgent(detected);
8816
9013
  }
@@ -8820,7 +9017,7 @@ function installLocal(force) {
8820
9017
  function syncConventionForTool(src, mainDst, force) {
8821
9018
  if (!existsSync18(src))
8822
9019
  return;
8823
- const dstDir = dirname10(mainDst);
9020
+ const dstDir = dirname9(mainDst);
8824
9021
  if (dstDir !== join12(homedir5(), ".claude") && !isAiInstalled(dstDir) && !existsSync18(dstDir)) {
8825
9022
  return;
8826
9023
  }
@@ -8917,7 +9114,7 @@ function linkSkills() {
8917
9114
  for (const skillName of listDirEntries(homeSkills)) {
8918
9115
  const skillDir = `${join12(homeSkills, skillName)}/`;
8919
9116
  try {
8920
- if (!statSync8(join12(homeSkills, skillName)).isDirectory())
9117
+ if (!statSync9(join12(homeSkills, skillName)).isDirectory())
8921
9118
  continue;
8922
9119
  } catch {
8923
9120
  continue;
@@ -9335,9 +9532,9 @@ function configCommand(args) {
9335
9532
  }
9336
9533
 
9337
9534
  // packages/cli/dist/commands/consistency.js
9338
- 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";
9339
9536
  import { join as join14 } from "node:path";
9340
- var DIMENSIONS2 = ["code", "docs", "i18n", "tests", "site"];
9537
+ var DIMENSIONS2 = ["code", "cards", "docs", "i18n", "tests", "site"];
9341
9538
  function err6(line) {
9342
9539
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
9343
9540
  const RED = noColor2 ? "" : "\x1B[0;31m";
@@ -9398,6 +9595,64 @@ function checkFeaturesCatalog(projectDir) {
9398
9595
  }
9399
9596
  return { status: gaps.length === 0 ? "pass" : "fail", gaps };
9400
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
+ }
9401
9656
  var SITE_INTERNAL_FEATURES = /* @__PURE__ */ new Set([
9402
9657
  "cycle-meta-sync",
9403
9658
  "loop-log-locality",
@@ -9496,7 +9751,7 @@ function listFiles2(dir) {
9496
9751
  try {
9497
9752
  return readdirSync8(dir).filter((n) => {
9498
9753
  try {
9499
- return statSync9(join14(dir, n)).isFile();
9754
+ return statSync10(join14(dir, n)).isFile();
9500
9755
  } catch {
9501
9756
  return false;
9502
9757
  }
@@ -9573,7 +9828,7 @@ function rglobBats(dir) {
9573
9828
  const full = join14(d, e);
9574
9829
  let st;
9575
9830
  try {
9576
- st = statSync9(full);
9831
+ st = statSync10(full);
9577
9832
  } catch {
9578
9833
  continue;
9579
9834
  }
@@ -9650,6 +9905,8 @@ function runAll(projectDir) {
9650
9905
  let result;
9651
9906
  if (dim === "code")
9652
9907
  result = checkFeaturesCatalog(projectDir);
9908
+ else if (dim === "cards")
9909
+ result = checkCards(projectDir);
9653
9910
  else if (dim === "i18n")
9654
9911
  result = checkI18n(projectDir);
9655
9912
  else if (dim === "tests")
@@ -9764,7 +10021,7 @@ function consistencyCommand(args) {
9764
10021
  // packages/cli/dist/commands/dashboard.js
9765
10022
  import { execFileSync as execFileSync3 } from "node:child_process";
9766
10023
  import { createHash as createHash3 } from "node:crypto";
9767
- 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";
9768
10025
  import { homedir as homedir7, platform } from "node:os";
9769
10026
  import { basename as basename6, join as join15 } from "node:path";
9770
10027
  var TZ_OFFSET_MS2 = 8 * 3600 * 1e3;
@@ -9857,7 +10114,7 @@ function sharedRoot() {
9857
10114
  }
9858
10115
  function isDir(p) {
9859
10116
  try {
9860
- return statSync10(p).isDirectory();
10117
+ return statSync11(p).isDirectory();
9861
10118
  } catch {
9862
10119
  return false;
9863
10120
  }
@@ -10432,8 +10689,8 @@ function loadClaudeSessionUsage(label4, slug) {
10432
10689
  if (entries.length === 0)
10433
10690
  return null;
10434
10691
  entries.sort((a, b) => {
10435
- const sa = statSync10(join15(projDir, a)).size;
10436
- const sb = statSync10(join15(projDir, b)).size;
10692
+ const sa = statSync11(join15(projDir, a)).size;
10693
+ const sb = statSync11(join15(projDir, b)).size;
10437
10694
  return sb - sa;
10438
10695
  });
10439
10696
  const path = join15(projDir, entries[0] ?? "");
@@ -10585,17 +10842,34 @@ function rollupForDay(dayCycles) {
10585
10842
  }
10586
10843
  return r;
10587
10844
  }
10588
- function selfScoreSummaryLine(notesDir = ".roll/notes", windowN = 14) {
10589
- if (!existsSync20(notesDir))
10590
- return "";
10591
- let files;
10845
+ function selfScoreSummaryLine(notesDir = ".roll/notes", windowN = 14, featuresDir = ".roll/features") {
10846
+ const entries = [];
10592
10847
  try {
10593
- 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) });
10594
10851
  } catch {
10595
- return "";
10596
10852
  }
10597
- files.sort();
10598
- files = files.slice(-windowN);
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
+ }
10869
+ } catch {
10870
+ }
10871
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
10872
+ const files = entries.slice(-windowN);
10599
10873
  if (files.length === 0)
10600
10874
  return "";
10601
10875
  let total = 0;
@@ -10607,7 +10881,7 @@ function selfScoreSummaryLine(notesDir = ".roll/notes", windowN = 14) {
10607
10881
  let verdict = null;
10608
10882
  let content;
10609
10883
  try {
10610
- content = readFileSync15(join15(notesDir, f), "utf8");
10884
+ content = readFileSync15(f.path, "utf8");
10611
10885
  } catch {
10612
10886
  content = "";
10613
10887
  }
@@ -10959,8 +11233,17 @@ function nextCronHint(zh) {
10959
11233
  const nxtSh = toShanghai(new Date(nxtEpoch));
10960
11234
  return `${pad22(nxtSh.getUTCHours())}:${pad22(nxtSh.getUTCMinutes())} \xB7 in ${mins}m ${pad22(secs)}s`;
10961
11235
  }
11236
+ function renderNow() {
11237
+ const v = process.env["ROLL_RENDER_NOW"] ?? "";
11238
+ if (v !== "") {
11239
+ const ms = /^\d+$/.test(v) ? Number(v) : Date.parse(v);
11240
+ if (!Number.isNaN(ms))
11241
+ return new Date(ms);
11242
+ }
11243
+ return /* @__PURE__ */ new Date();
11244
+ }
10962
11245
  function fixtureData() {
10963
- const now = /* @__PURE__ */ new Date();
11246
+ const now = renderNow();
10964
11247
  const events = [];
10965
11248
  const cron = [];
10966
11249
  let cycleId = 0;
@@ -11163,7 +11446,7 @@ function render(out2, events, cron, state, backlog, args) {
11163
11446
  out2.push(" " + c("dim", agentLine));
11164
11447
  let skillLine = "";
11165
11448
  try {
11166
- skillLine = selfScoreSummaryLine();
11449
+ skillLine = selfScoreSummaryLine(process.env["ROLL_NOTES_DIR"], 14, process.env["ROLL_FEATURES_DIR"]);
11167
11450
  } catch {
11168
11451
  skillLine = "";
11169
11452
  }
@@ -11554,7 +11837,7 @@ roll-loop-status.py: error: unrecognized arguments: ${args.unknown.join(" ")}
11554
11837
  }
11555
11838
  renderState.useColor = !args.noColor && (process.env["NO_COLOR"] ?? "") === "" && (process.stdout.isTTY === true || (process.env["FORCE_COLOR"] ?? "") !== "");
11556
11839
  const lang4 = args.en ? "en" : args.zh ? "zh" : "both";
11557
- const now = /* @__PURE__ */ new Date();
11840
+ const now = renderNow();
11558
11841
  let events;
11559
11842
  let cron;
11560
11843
  let state;
@@ -11901,7 +12184,7 @@ function loopRunsCommand(argv) {
11901
12184
 
11902
12185
  // packages/cli/dist/commands/loop-signals.js
11903
12186
  import { appendFileSync as appendFileSync3, existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "node:fs";
11904
- import { dirname as dirname11, join as join17 } from "node:path";
12187
+ import { dirname as dirname10, join as join17 } from "node:path";
11905
12188
  function lang2() {
11906
12189
  return resolveLang({ rollLang: process.env["ROLL_LANG"], lcAll: process.env["LC_ALL"], lang: process.env["LANG"] });
11907
12190
  }
@@ -11956,7 +12239,7 @@ function loopSignalsCommand(argv) {
11956
12239
  const seenFile = join17(rtDir, `signals-seen-${slug}`);
11957
12240
  const candFile = join17(projectPath, ".roll", "signals", "candidates.md");
11958
12241
  mkdirSync11(rtDir, { recursive: true });
11959
- mkdirSync11(dirname11(candFile), { recursive: true });
12242
+ mkdirSync11(dirname10(candFile), { recursive: true });
11960
12243
  if (!existsSync22(seenFile))
11961
12244
  writeFileSync10(seenFile, "");
11962
12245
  let lastId = 0;
@@ -12027,13 +12310,13 @@ function loopAttachRetired() {
12027
12310
 
12028
12311
  // packages/cli/dist/commands/doctor.js
12029
12312
  import { execFileSync as execFileSync5 } from "node:child_process";
12030
- 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";
12031
12314
  import { homedir as homedir9, tmpdir as tmpdir2 } from "node:os";
12032
12315
  import { delimiter as delimiter2, join as join19 } from "node:path";
12033
12316
 
12034
12317
  // packages/cli/dist/commands/skills.js
12035
12318
  import { execFileSync as execFileSync4 } from "node:child_process";
12036
- 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";
12037
12320
  import { tmpdir } from "node:os";
12038
12321
  import { join as join18 } from "node:path";
12039
12322
  function pkgDir() {
@@ -12108,7 +12391,7 @@ function generateCatalog() {
12108
12391
  for (const name of entries) {
12109
12392
  const skillDir = join18(dir, name);
12110
12393
  try {
12111
- if (!statSync11(skillDir).isDirectory())
12394
+ if (!statSync12(skillDir).isDirectory())
12112
12395
  continue;
12113
12396
  } catch {
12114
12397
  continue;
@@ -12214,7 +12497,7 @@ var out = { lines: [] };
12214
12497
  function emit(line) {
12215
12498
  out.lines.push(line);
12216
12499
  }
12217
- function agentBinNames3(agent) {
12500
+ function agentBinNames4(agent) {
12218
12501
  switch (agent) {
12219
12502
  case "claude":
12220
12503
  return ["claude"];
@@ -12241,7 +12524,7 @@ function commandOnPath2(bin) {
12241
12524
  if (dir === "")
12242
12525
  continue;
12243
12526
  try {
12244
- const st = statSync12(join19(dir, bin));
12527
+ const st = statSync13(join19(dir, bin));
12245
12528
  if (!st.isFile())
12246
12529
  continue;
12247
12530
  accessSync2(join19(dir, bin), constants2.X_OK);
@@ -12251,7 +12534,7 @@ function commandOnPath2(bin) {
12251
12534
  }
12252
12535
  return false;
12253
12536
  }
12254
- function agentInstalledByName3(agent, dir) {
12537
+ function agentInstalledByName4(agent, dir) {
12255
12538
  const home = homedir9();
12256
12539
  switch (agent) {
12257
12540
  case "trae":
@@ -12260,7 +12543,7 @@ function agentInstalledByName3(agent, dir) {
12260
12543
  const p = join19(home, ".opencode", "bin", "opencode");
12261
12544
  try {
12262
12545
  accessSync2(p, constants2.X_OK);
12263
- return statSync12(p).isFile();
12546
+ return statSync13(p).isFile();
12264
12547
  } catch {
12265
12548
  return false;
12266
12549
  }
@@ -12270,7 +12553,7 @@ function agentInstalledByName3(agent, dir) {
12270
12553
  case "openclaw":
12271
12554
  return existsSync24(join19(home, ".openclaw", "workspace"));
12272
12555
  default: {
12273
- const bins = agentBinNames3(agent);
12556
+ const bins = agentBinNames4(agent);
12274
12557
  if (bins !== null)
12275
12558
  return bins.some(commandOnPath2);
12276
12559
  return dir !== "" && existsSync24(dir) && safeIsDir(dir);
@@ -12279,7 +12562,7 @@ function agentInstalledByName3(agent, dir) {
12279
12562
  }
12280
12563
  function safeIsDir(p) {
12281
12564
  try {
12282
- return statSync12(p).isDirectory();
12565
+ return statSync13(p).isDirectory();
12283
12566
  } catch {
12284
12567
  return false;
12285
12568
  }
@@ -12319,7 +12602,7 @@ function agentSection(p) {
12319
12602
  dir = dir.replace(/^ /, "");
12320
12603
  if (dir.startsWith("~"))
12321
12604
  dir = home + dir.slice(1);
12322
- const installed = agentInstalledByName3(name, dir) ? t(v2Catalog, msgLang6(), "doctor.agent_installed") : t(v2Catalog, msgLang6(), "doctor.agent_missing");
12605
+ const installed = agentInstalledByName4(name, dir) ? t(v2Catalog, msgLang6(), "doctor.agent_installed") : t(v2Catalog, msgLang6(), "doctor.agent_missing");
12323
12606
  const dirExists = safeIsDir(dir) ? t(v2Catalog, msgLang6(), "doctor.agent_dir_exists") : t(v2Catalog, msgLang6(), "doctor.agent_dir_missing");
12324
12607
  const tag = name === primary ? ` (${t(v2Catalog, msgLang6(), "doctor.agent_primary_label")})` : "";
12325
12608
  emit(` ${padEnd(name, 10)} ${padEnd(installed, 14)} ${dirExists}${tag}`);
@@ -12515,7 +12798,7 @@ function killLiveAgents(signal2 = "SIGKILL") {
12515
12798
  let n = 0;
12516
12799
  for (const c2 of liveAgents) {
12517
12800
  try {
12518
- if (c2.kill(signal2))
12801
+ if (killHard(c2, signal2))
12519
12802
  n += 1;
12520
12803
  } catch {
12521
12804
  }
@@ -12523,18 +12806,20 @@ function killLiveAgents(signal2 = "SIGKILL") {
12523
12806
  liveAgents.clear();
12524
12807
  return n;
12525
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
+ }
12526
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: ";
12527
12820
  var AGENT_ARGV_TODO = {
12528
- kimi: "kimi-code|kimi-cli|kimi -p <prompt> (FIX-126/133; no stream-json splice)",
12529
- pi: "pi -p <prompt> (text mode; no usage on stdout \u2014 cost via session file)",
12530
- deepseek: "deepseek <prompt> (positional)",
12531
- codex: "codex exec <prompt>",
12532
12821
  openai: "codex exec <prompt>",
12533
- opencode: "opencode run <prompt>",
12534
- gemini: "agy -p --dangerously-skip-permissions <prompt> (FIX-153)",
12535
- agy: "agy -p --dangerously-skip-permissions <prompt> (FIX-153)",
12536
- antigravity: "agy -p --dangerously-skip-permissions <prompt> (FIX-153)",
12537
- qwen: "qwen <prompt> (positional)"
12822
+ opencode: "opencode run <prompt>"
12538
12823
  };
12539
12824
  function storyPinDirective(storyId) {
12540
12825
  return `[\u672C\u5468\u671F\u6307\u5B9A\u6545\u4E8B] \u8C03\u5EA6\u5668\u5DF2\u9501\u5B9A ${storyId} \u5E76\u5728 backlog \u6807\u8BB0 \u{1F528} In Progress\u2014\u2014\u53EA\u6267\u884C\u8FD9\u4E00\u4E2A\u6545\u4E8B,\u4E25\u7981\u91CD\u65B0\u6311\u9009\u6216\u987A\u624B\u505A\u522B\u7684;\u82E5\u5B83\u786E\u5B9E\u4E0D\u53EF\u6267\u884C,\u5199 ALERT \u8BF4\u660E\u539F\u56E0\u540E\u5E72\u51C0\u9000\u51FA\u3002
@@ -12545,7 +12830,7 @@ function buildClaudeArgv(input) {
12545
12830
  const bin = input.bin ?? "claude";
12546
12831
  const pin = input.storyId !== void 0 && input.storyId !== "" ? storyPinDirective(input.storyId) : "";
12547
12832
  const prompt = `${AUTORUN_DIRECTIVE}${pin}${input.skillBody}`;
12548
- const args = [
12833
+ const args = input.interactive ? ["-p", prompt, "--dangerously-skip-permissions", "--add-dir", input.worktree] : [
12549
12834
  "-p",
12550
12835
  prompt,
12551
12836
  "--verbose",
@@ -12557,24 +12842,54 @@ function buildClaudeArgv(input) {
12557
12842
  ];
12558
12843
  return { bin, args };
12559
12844
  }
12560
- var realAgentSpawn = (agent, opts) => {
12561
- if (agent !== "claude") {
12562
- const hint = AGENT_ARGV_TODO[agent] ?? "unknown agent";
12563
- throw new Error(`runner: agent '${agent}' argv not yet ported (only 'claude' is). v2 shape: ${hint}`);
12845
+ function buildSpawnCommand(agent, opts) {
12846
+ const prompt = `${AUTORUN_DIRECTIVE}${opts.storyId !== void 0 && opts.storyId !== "" ? storyPinDirective(opts.storyId) : ""}${opts.skillBody}`;
12847
+ if (agent === "claude") {
12848
+ return buildClaudeArgv({
12849
+ worktree: opts.cwd,
12850
+ skillBody: opts.skillBody,
12851
+ ...opts.storyId !== void 0 ? { storyId: opts.storyId } : {},
12852
+ bin: opts.bin,
12853
+ interactive: opts.interactive
12854
+ });
12564
12855
  }
12565
- const { bin, args } = buildClaudeArgv({
12566
- worktree: opts.cwd,
12567
- skillBody: opts.skillBody,
12568
- ...opts.storyId !== void 0 ? { storyId: opts.storyId } : {},
12569
- bin: opts.bin
12570
- });
12856
+ if (agent === "pi") {
12857
+ return { bin: opts.bin ?? "pi", args: ["-p", prompt] };
12858
+ }
12859
+ if (agent === "kimi") {
12860
+ return { bin: opts.bin ?? "kimi", args: ["-p", prompt] };
12861
+ }
12862
+ if (agent === "codex") {
12863
+ return { bin: opts.bin ?? "codex", args: ["exec", prompt] };
12864
+ }
12865
+ if (agent === "deepseek") {
12866
+ return { bin: opts.bin ?? "deepseek", args: [prompt] };
12867
+ }
12868
+ if (agent === "qwen") {
12869
+ return { bin: opts.bin ?? "qwen", args: [prompt] };
12870
+ }
12871
+ if (agent === "agy" || agent === "gemini" || agent === "antigravity") {
12872
+ return { bin: opts.bin ?? "agy", args: ["-p", "--dangerously-skip-permissions", prompt] };
12873
+ }
12874
+ const hint = AGENT_ARGV_TODO[agent] ?? "unknown agent";
12875
+ throw new Error(`runner: agent '${agent}' argv not yet ported. v2 shape: ${hint}`);
12876
+ }
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) {
12571
12883
  process.stderr.write(`[runner] spawn ${bin} argv=${JSON.stringify(args.map((a) => a.length > 80 ? `${a.slice(0, 77)}...` : a))}
12572
12884
  `);
12573
- return new Promise((resolve3, reject) => {
12885
+ return new Promise((resolve3) => {
12574
12886
  const child = spawn(bin, args, {
12575
12887
  cwd: opts.cwd,
12576
12888
  env: opts.env ?? process.env,
12577
- 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
12578
12893
  });
12579
12894
  let stdout = "";
12580
12895
  let stderr = "";
@@ -12583,7 +12898,7 @@ var realAgentSpawn = (agent, opts) => {
12583
12898
  if (opts.timeoutMs !== void 0 && opts.timeoutMs > 0) {
12584
12899
  timer = setTimeout(() => {
12585
12900
  timedOut = true;
12586
- child.kill("SIGKILL");
12901
+ killHard(child, "SIGKILL");
12587
12902
  }, opts.timeoutMs);
12588
12903
  }
12589
12904
  child.stdout?.on("data", (d) => {
@@ -12616,16 +12931,22 @@ var realAgentSpawn = (agent, opts) => {
12616
12931
  settle({ stdout, stderr, exitCode: code ?? 1, timedOut });
12617
12932
  });
12618
12933
  });
12934
+ }
12935
+ var realAgentSpawn = (agent, opts) => {
12936
+ const { bin, args, pty } = withPtyWrap(buildSpawnCommand(agent, opts), agent);
12937
+ return spawnAndWait(bin, args, opts, pty);
12619
12938
  };
12620
12939
 
12621
12940
  // packages/cli/dist/runner/skill-body.js
12622
12941
  import { existsSync as existsSync25, readFileSync as readFileSync20 } from "node:fs";
12942
+ import { homedir as homedir10 } from "node:os";
12623
12943
  import { join as join20 } from "node:path";
12624
12944
  function readSkillBody(projectPath, opts) {
12625
12945
  const candidates = [
12626
12946
  opts.envOverride ?? "",
12627
12947
  join20(projectPath, ".roll", "skills", opts.skillName, "SKILL.md"),
12628
- join20(projectPath, "skills", opts.skillName, "SKILL.md")
12948
+ join20(projectPath, "skills", opts.skillName, "SKILL.md"),
12949
+ join20(homedir10(), ".roll", "skills", opts.skillName, "SKILL.md")
12629
12950
  ].filter((p) => p !== "");
12630
12951
  for (const p of candidates) {
12631
12952
  if (!existsSync25(p))
@@ -12701,7 +13022,7 @@ dream run-once: \u627E\u4E0D\u5230 roll-.dream SKILL.md \u2014 \u62D2\u7EDD\u76F
12701
13022
  // packages/cli/dist/commands/feedback.js
12702
13023
  import { execFileSync as execFileSync6, spawnSync as spawnSync4 } from "node:child_process";
12703
13024
  import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
12704
- import { homedir as homedir10 } from "node:os";
13025
+ import { homedir as homedir11 } from "node:os";
12705
13026
  import { basename as basename8, join as join23 } from "node:path";
12706
13027
 
12707
13028
  // packages/cli/dist/commands/version.js
@@ -12745,7 +13066,7 @@ function err8(line) {
12745
13066
  `);
12746
13067
  }
12747
13068
  function projectAgent2() {
12748
- const readField = (file, key) => {
13069
+ const readField2 = (file, key) => {
12749
13070
  if (!existsSync26(file))
12750
13071
  return void 0;
12751
13072
  let text;
@@ -12764,14 +13085,14 @@ function projectAgent2() {
12764
13085
  };
12765
13086
  const local = ".roll/local.yaml";
12766
13087
  if (existsSync26(local) && /^agent:/m.test(safeRead(local))) {
12767
- return readField(local, /^agent:/) ?? "claude";
13088
+ return readField2(local, /^agent:/) ?? "claude";
12768
13089
  }
12769
13090
  if (existsSync26(".roll.yaml") && /^agent:/m.test(safeRead(".roll.yaml"))) {
12770
- return readField(".roll.yaml", /^agent:/) ?? "claude";
13091
+ return readField2(".roll.yaml", /^agent:/) ?? "claude";
12771
13092
  }
12772
13093
  const cfg = rollConfig2();
12773
13094
  if (existsSync26(cfg) && /primary_agent:/.test(safeRead(cfg))) {
12774
- return readField(cfg, /primary_agent:/) ?? "claude";
13095
+ return readField2(cfg, /primary_agent:/) ?? "claude";
12775
13096
  }
12776
13097
  return "claude";
12777
13098
  }
@@ -12783,7 +13104,7 @@ function safeRead(p) {
12783
13104
  }
12784
13105
  }
12785
13106
  function rollConfig2() {
12786
- const rollHome4 = process.env["ROLL_HOME"] ?? join23(homedir10(), ".roll");
13107
+ const rollHome4 = process.env["ROLL_HOME"] ?? join23(homedir11(), ".roll");
12787
13108
  return join23(rollHome4, "config.yaml");
12788
13109
  }
12789
13110
  function feedbackYamlField(file, field) {
@@ -12830,7 +13151,7 @@ function feedbackDefaultRepo() {
12830
13151
  let v = feedbackYamlField(".roll/local.yaml", "feedback_repo");
12831
13152
  if (v)
12832
13153
  return v;
12833
- v = feedbackYamlField(join23(homedir10(), ".roll", "config.yaml"), "feedback_repo");
13154
+ v = feedbackYamlField(join23(homedir11(), ".roll", "config.yaml"), "feedback_repo");
12834
13155
  if (v)
12835
13156
  return v;
12836
13157
  return feedbackOriginRepo();
@@ -12990,7 +13311,7 @@ function feedbackCommand(args) {
12990
13311
  }
12991
13312
 
12992
13313
  // packages/cli/dist/commands/gc.js
12993
- 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";
12994
13315
  import { join as join24 } from "node:path";
12995
13316
  var RUN_ID_RE2 = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/;
12996
13317
  function runsInDir2(dir) {
@@ -13005,7 +13326,7 @@ function runsInDir2(dir) {
13005
13326
  if (!e.isDirectory() || !RUN_ID_RE2.test(e.name))
13006
13327
  continue;
13007
13328
  try {
13008
- 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) });
13009
13330
  } catch {
13010
13331
  }
13011
13332
  }
@@ -13152,14 +13473,14 @@ ${label2(lang4, "ideav3.usage")}
13152
13473
  throw e;
13153
13474
  }
13154
13475
  const kindLabel = label2(lang4, plan.kind === "bug" ? "ideav3.kind_bug" : "ideav3.kind_idea");
13155
- const section = IDEA_SECTIONS[plan.kind].replace(/^#+\s*/, "");
13476
+ const section2 = IDEA_SECTIONS[plan.kind].replace(/^#+\s*/, "");
13156
13477
  process.stdout.write(`
13157
13478
  ${c("green", "\u{1F4DD} " + label2(lang4, "ideav3.recorded", plan.id))}
13158
13479
 
13159
13480
  `);
13160
13481
  process.stdout.write(` ${c("dim", label2(lang4, "ideav3.type") + ":")} ${kindLabel}
13161
13482
  `);
13162
- process.stdout.write(` ${c("dim", label2(lang4, "ideav3.section") + ":")} ${section}
13483
+ process.stdout.write(` ${c("dim", label2(lang4, "ideav3.section") + ":")} ${section2}
13163
13484
  `);
13164
13485
  process.stdout.write(` ${c("dim", label2(lang4, "ideav3.text") + ":")} ${text}
13165
13486
 
@@ -13182,109 +13503,546 @@ ${c("green", "\u{1F4DD} " + label2(lang4, "ideav3.recorded", plan.id))}
13182
13503
  }
13183
13504
 
13184
13505
  // packages/cli/dist/commands/index-gen.js
13185
- 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";
13186
13507
  import { join as join26 } from "node:path";
13187
- function indexCommand(args) {
13188
- if (args.includes("--help") || args.includes("-h")) {
13189
- process.stdout.write("Usage: roll index\n Regenerate .roll/index.json + .roll/features/index.html\n");
13190
- return 0;
13191
- }
13192
- const cwd = process.cwd();
13193
- const stories = generateIndex(cwd);
13194
- const n = Object.keys(stories).length;
13195
- process.stdout.write(`index.json regenerated
13196
- \u7D22\u5F15\u5DF2\u91CD\u5EFA
13197
- ${n} stories mapped to epics (.roll/index.json)
13198
- `);
13199
- const featuresDir = join26(cwd, ".roll", "features");
13200
- if (existsSync29(featuresDir)) {
13201
- const epics = /* @__PURE__ */ new Map();
13202
- for (const [sid, epic] of Object.entries(stories)) {
13203
- if (!epics.has(epic))
13204
- epics.set(epic, /* @__PURE__ */ new Set());
13205
- epics.get(epic).add(sid);
13206
- }
13207
- try {
13208
- for (const d of readdirSync14(featuresDir)) {
13209
- const epicDir = join26(featuresDir, d);
13210
- if (!statSync14(epicDir).isDirectory())
13211
- continue;
13212
- if (!epics.has(d))
13213
- epics.set(d, /* @__PURE__ */ new Set());
13214
- for (const s of readdirSync14(epicDir)) {
13215
- const storyDir = join26(epicDir, s);
13216
- if (!statSync14(storyDir).isDirectory())
13217
- continue;
13218
- if (STORY_ID_RE.test(s))
13219
- epics.get(d).add(s);
13220
- }
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);
13221
13625
  }
13222
- } catch {
13626
+ document.documentElement.setAttribute("data-filtered", needle || ship ? "1" : "0");
13223
13627
  }
13224
- const rows = [];
13225
- for (const epic of [...epics.keys()].sort()) {
13226
- const unique = [...epics.get(epic)].sort();
13227
- let delivered = 0;
13228
- for (const s of unique) {
13229
- const latest = join26(featuresDir, epic, s, "latest");
13230
- try {
13231
- if (statSync14(latest).isDirectory())
13232
- delivered++;
13233
- } catch {
13234
- }
13235
- }
13236
- const links = unique.slice(0, 20).map((s) => `<a href="${epic}/${s}/index.html">${s}</a>`).join(", ");
13237
- const more = unique.length > 20 ? ` ${bi(`\u2026 +${unique.length - 20} more`, `\u2026 \u53E6 ${unique.length - 20} \u4E2A`)}` : "";
13238
- rows.push(`<tr>
13239
- <td><strong><a href="${epic}/">${epic}</a></strong></td>
13240
- <td>${bi(`${unique.length} stories (${delivered} delivered)`, `${unique.length} \u4E2A\u6545\u4E8B\uFF08\u5DF2\u4EA4\u4ED8 ${delivered}\uFF09`)}</td>
13241
- <td style="font-size:0.85em">${links}${more}</td>
13242
- </tr>`);
13243
- }
13244
- 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>
13245
13672
  <html lang="zh-CN">
13246
13673
  <head>
13247
13674
  <meta charset="UTF-8">
13248
13675
  <meta name="viewport" content="width=device-width, initial-scale=1">
13249
- <title>Roll Features Index \xB7 \u529F\u80FD\u6863\u6848</title>
13676
+ <title>Roll \xB7 Delivery Dossier</title>
13250
13677
  <style>
13251
- ${CHROME_CSS}body { max-width:1000px; }
13252
- table { width:100%; border-collapse:collapse; position:relative; z-index:2; }
13253
- th, td { padding:8px 12px; text-align:left; border-bottom:1px solid var(--line); vertical-align:top; }
13254
- th { font-family:var(--serif); letter-spacing:.04em; color:var(--muted); }
13255
- tr:hover td { background:var(--bg-raise); }
13256
- td code, td strong a { font-family:var(--mono); }
13678
+ ${CHROME_CSS}${DOSSIER_CSS}body { max-width:1000px; }
13257
13679
  </style>
13258
13680
  ${CHROME_SCRIPT}
13681
+ ${DOSSIER_FILTER_SCRIPT}
13259
13682
  </head>
13260
13683
  <body>
13261
13684
  ${CHROME_CONTROLS}
13685
+ <div class="masthead">
13262
13686
  <p class="kicker">Roll \xB7 ${bi("Delivery Dossier", "\u4EA4\u4ED8\u6863\u6848")}</p>
13263
13687
  <h1>${bi("Features Index", "\u529F\u80FD\u6863\u6848")}</h1>
13264
- <p class="meta">${bi(`${epics.size} epics \xB7 ${n} stories`, `${epics.size} \u4E2A\u53F2\u8BD7 \xB7 ${n} \u4E2A\u6545\u4E8B`)}</p>
13265
- <table><thead><tr><th>Epic</th><th>${bi("Stories", "\u6545\u4E8B")}</th><th>${bi("Recent", "\u6700\u8FD1")}</th></tr></thead>
13266
- <tbody>
13267
- ${rows.join("\n")}
13268
- </tbody></table>
13269
- <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>
13270
13692
  </body>
13271
13693
  </html>
13272
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>
13759
+ </body>
13760
+ </html>
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 = [];
13273
13892
  try {
13274
- writeFileSync14(join26(featuresDir, "index.html"), html, "utf8");
13275
- 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)
13276
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;
13277
13956
  } catch {
13278
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;
13279
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 {
14033
+ }
14034
+ process.stdout.write(`card minted
14035
+ \u5361\u5DF2\u5EFA\u6863
14036
+ .roll/features/${epic}/${id}/spec.md
14037
+ `);
13280
14038
  return 0;
13281
14039
  }
13282
14040
 
13283
14041
  // packages/cli/dist/commands/init.js
13284
14042
  import { spawnSync as spawnSync5 } from "node:child_process";
13285
- 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";
13286
- import { homedir as homedir11 } from "node:os";
13287
- import { dirname as dirname12, join as join27 } from "node:path";
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";
14044
+ import { homedir as homedir12 } from "node:os";
14045
+ import { dirname as dirname11, join as join28 } from "node:path";
13288
14046
  function err9(line) {
13289
14047
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
13290
14048
  const RED = noColor2 ? "" : "\x1B[0;31m";
@@ -13293,43 +14051,43 @@ function err9(line) {
13293
14051
  `);
13294
14052
  }
13295
14053
  function rollHome2() {
13296
- return process.env["ROLL_HOME"] ?? join27(homedir11(), ".roll");
14054
+ return process.env["ROLL_HOME"] ?? join28(homedir12(), ".roll");
13297
14055
  }
13298
14056
  function rollGlobal2() {
13299
- return join27(rollHome2(), "conventions", "global");
14057
+ return join28(rollHome2(), "conventions", "global");
13300
14058
  }
13301
14059
  function rollTemplates2() {
13302
- return join27(rollHome2(), "conventions", "templates");
14060
+ return join28(rollHome2(), "conventions", "templates");
13303
14061
  }
13304
14062
  function scanProjectType(dir) {
13305
14063
  let hasFrontend = false;
13306
14064
  let hasBackend = false;
13307
14065
  let hasCli = false;
13308
- const pkg = join27(dir, "package.json");
14066
+ const pkg = join28(dir, "package.json");
13309
14067
  const readPkg = () => {
13310
14068
  try {
13311
- return readFileSync23(pkg, "utf8");
14069
+ return readFileSync24(pkg, "utf8");
13312
14070
  } catch {
13313
14071
  return "";
13314
14072
  }
13315
14073
  };
13316
- if (existsSync30(pkg)) {
14074
+ if (existsSync31(pkg)) {
13317
14075
  if (/"react"|"vue"|"next"|"nuxt"|"vite"|"svelte"/i.test(readPkg()))
13318
14076
  hasFrontend = true;
13319
14077
  }
13320
- if (["src", "app", "pages", "components"].some((d) => existsSync30(join27(dir, d))))
14078
+ if (["src", "app", "pages", "components"].some((d) => existsSync31(join28(dir, d))))
13321
14079
  hasFrontend = true;
13322
- if (["server", "api", "backend"].some((d) => existsSync30(join27(dir, d))))
14080
+ if (["server", "api", "backend"].some((d) => existsSync31(join28(dir, d))))
13323
14081
  hasBackend = true;
13324
- 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))))
13325
14083
  hasBackend = true;
13326
- if (existsSync30(pkg)) {
14084
+ if (existsSync31(pkg)) {
13327
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()))
13328
14086
  hasBackend = true;
13329
14087
  }
13330
- if (existsSync30(join27(dir, "prisma", "schema.prisma")))
14088
+ if (existsSync31(join28(dir, "prisma", "schema.prisma")))
13331
14089
  hasBackend = true;
13332
- if (existsSync30(join27(dir, "bin")) || existsSync30(join27(dir, "cmd")))
14090
+ if (existsSync31(join28(dir, "bin")) || existsSync31(join28(dir, "cmd")))
13333
14091
  hasCli = true;
13334
14092
  if (hasFrontend && hasBackend)
13335
14093
  return "fullstack";
@@ -13349,8 +14107,8 @@ function countNonEmptyFiles(dir) {
13349
14107
  }
13350
14108
  function isLegacyProject(projectDir) {
13351
14109
  for (const dir of ["src", "app", "lib", "pkg", "cmd"]) {
13352
- const p = join27(projectDir, dir);
13353
- if (existsSync30(p) && statSync15(p).isDirectory()) {
14110
+ const p = join28(projectDir, dir);
14111
+ if (existsSync31(p) && statSync16(p).isDirectory()) {
13354
14112
  if (countNonEmptyFiles(p) >= 10)
13355
14113
  return true;
13356
14114
  }
@@ -13380,12 +14138,12 @@ function isLegacyProject(projectDir) {
13380
14138
  "deno.jsonc"
13381
14139
  ];
13382
14140
  for (const man of manifests)
13383
- if (existsSync30(join27(projectDir, man)))
14141
+ if (existsSync31(join28(projectDir, man)))
13384
14142
  return true;
13385
14143
  const tf = spawnSync5("bash", ["-c", `compgen -G '${projectDir.replace(/'/g, "'\\''")}/*.tf' >/dev/null 2>&1`]);
13386
14144
  if (tf.status === 0)
13387
14145
  return true;
13388
- if (existsSync30(join27(projectDir, ".git"))) {
14146
+ if (existsSync31(join28(projectDir, ".git"))) {
13389
14147
  const g = spawnSync5("git", ["rev-parse", "--verify", "HEAD"], { cwd: projectDir, stdio: "ignore" });
13390
14148
  if (g.status === 0)
13391
14149
  return true;
@@ -13393,18 +14151,18 @@ function isLegacyProject(projectDir) {
13393
14151
  return false;
13394
14152
  }
13395
14153
  function mergeGlobalToProject(projectDir, summary) {
13396
- const src = join27(rollGlobal2(), "AGENTS.md");
13397
- const dst = join27(projectDir, "AGENTS.md");
13398
- if (!existsSync30(src)) {
14154
+ const src = join28(rollGlobal2(), "AGENTS.md");
14155
+ const dst = join28(projectDir, "AGENTS.md");
14156
+ if (!existsSync31(src)) {
13399
14157
  return;
13400
14158
  }
13401
14159
  const projectType = scanProjectType(projectDir);
13402
14160
  const skipFrontend = ["cli", "backend-service", "unknown"].includes(projectType);
13403
14161
  const FRONTEND_HEAD = "## 7. Frontend Default Stack";
13404
- const srcText = readFileSync23(src, "utf8");
14162
+ const srcText = readFileSync24(src, "utf8");
13405
14163
  const srcLines = srcText.split("\n");
13406
14164
  const lines = srcText.endsWith("\n") ? srcLines.slice(0, -1) : srcLines;
13407
- if (!existsSync30(dst)) {
14165
+ if (!existsSync31(dst)) {
13408
14166
  let out2 = "";
13409
14167
  let fcH = "";
13410
14168
  let fcB = "";
@@ -13433,11 +14191,11 @@ ${fcB}`;
13433
14191
  }
13434
14192
  }
13435
14193
  flush();
13436
- writeFileSync15(dst, out2);
14194
+ writeFileSync16(dst, out2);
13437
14195
  summary.push("created|AGENTS.md");
13438
14196
  return;
13439
14197
  }
13440
- const dstText = readFileSync23(dst, "utf8");
14198
+ const dstText = readFileSync24(dst, "utf8");
13441
14199
  let added = 0;
13442
14200
  let curH = "";
13443
14201
  let curB = "";
@@ -13467,7 +14225,7 @@ ${curB}`;
13467
14225
  }
13468
14226
  tryAppend();
13469
14227
  if (appendBuffer !== "")
13470
- writeFileSync15(dst, dstText + appendBuffer);
14228
+ writeFileSync16(dst, dstText + appendBuffer);
13471
14229
  if (added > 0)
13472
14230
  summary.push("merged|AGENTS.md");
13473
14231
  else
@@ -13475,20 +14233,20 @@ ${curB}`;
13475
14233
  }
13476
14234
  function mergeClaudeToProject(projectDir, summary) {
13477
14235
  const projectType = scanProjectType(projectDir);
13478
- const tplFile = join27(rollTemplates2(), projectType, "CLAUDE.md");
13479
- if (!existsSync30(tplFile))
14236
+ const tplFile = join28(rollTemplates2(), projectType, "CLAUDE.md");
14237
+ if (!existsSync31(tplFile))
13480
14238
  return;
13481
- const claudeDir = join27(projectDir, ".claude");
13482
- const outFile = join27(claudeDir, "CLAUDE.md");
13483
- mkdirSync14(claudeDir, { recursive: true });
13484
- 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)) {
13485
14243
  copyFileSync2(tplFile, outFile);
13486
14244
  summary.push("created|.claude/CLAUDE.md");
13487
14245
  return;
13488
14246
  }
13489
- const tplText = readFileSync23(tplFile, "utf8");
14247
+ const tplText = readFileSync24(tplFile, "utf8");
13490
14248
  const lines = tplText.endsWith("\n") ? tplText.split("\n").slice(0, -1) : tplText.split("\n");
13491
- const outText = readFileSync23(outFile, "utf8");
14249
+ const outText = readFileSync24(outFile, "utf8");
13492
14250
  let added = 0;
13493
14251
  let curH = "";
13494
14252
  let curB = "";
@@ -13513,7 +14271,7 @@ ${curB}`;
13513
14271
  }
13514
14272
  tryAppend();
13515
14273
  if (appendBuffer !== "")
13516
- writeFileSync15(outFile, outText + appendBuffer);
14274
+ writeFileSync16(outFile, outText + appendBuffer);
13517
14275
  if (added > 0)
13518
14276
  summary.push("merged|.claude/CLAUDE.md");
13519
14277
  else
@@ -13530,20 +14288,20 @@ var BACKLOG_TEMPLATE = `# Project Backlog
13530
14288
  |----|---------|--------|
13531
14289
  `;
13532
14290
  function writeBacklog(path, summary) {
13533
- if (existsSync30(path)) {
14291
+ if (existsSync31(path)) {
13534
14292
  summary.push("unchanged|.roll/backlog.md");
13535
14293
  return;
13536
14294
  }
13537
- mkdirSync14(dirname12(path), { recursive: true });
13538
- writeFileSync15(path, BACKLOG_TEMPLATE);
14295
+ mkdirSync15(dirname11(path), { recursive: true });
14296
+ writeFileSync16(path, BACKLOG_TEMPLATE);
13539
14297
  summary.push("created|.roll/backlog.md");
13540
14298
  }
13541
14299
  function ensureFeaturesDir(path, summary) {
13542
- if (existsSync30(path) && statSync15(path).isDirectory()) {
14300
+ if (existsSync31(path) && statSync16(path).isDirectory()) {
13543
14301
  summary.push("unchanged|.roll/features/");
13544
14302
  return;
13545
14303
  }
13546
- mkdirSync14(path, { recursive: true });
14304
+ mkdirSync15(path, { recursive: true });
13547
14305
  summary.push("created|.roll/features/");
13548
14306
  }
13549
14307
  var FEATURES_TEMPLATE = `# Features
@@ -13557,38 +14315,38 @@ var FEATURES_TEMPLATE = `# Features
13557
14315
  <!-- Add feature entries here as epics are completed -->
13558
14316
  `;
13559
14317
  function writeFeaturesMd(path, summary) {
13560
- if (existsSync30(path)) {
14318
+ if (existsSync31(path)) {
13561
14319
  summary.push("unchanged|.roll/features.md");
13562
14320
  return;
13563
14321
  }
13564
- mkdirSync14(dirname12(path), { recursive: true });
13565
- writeFileSync15(path, FEATURES_TEMPLATE);
14322
+ mkdirSync15(dirname11(path), { recursive: true });
14323
+ writeFileSync16(path, FEATURES_TEMPLATE);
13566
14324
  summary.push("created|.roll/features.md");
13567
14325
  }
13568
14326
  function initSeedAgentRoutes(templateName, projectDir, summary) {
13569
- const dest = join27(projectDir, ".roll", "agent-routes.yaml");
13570
- if (existsSync30(dest)) {
14327
+ const dest = join28(projectDir, ".roll", "agent-routes.yaml");
14328
+ if (existsSync31(dest)) {
13571
14329
  summary.push("unchanged|.roll/agent-routes.yaml");
13572
14330
  return 0;
13573
14331
  }
13574
- const src = join27(rollTemplates2(), "agent-routes", `${templateName}.yaml`);
13575
- if (!existsSync30(src)) {
14332
+ const src = join28(rollTemplates2(), "agent-routes", `${templateName}.yaml`);
14333
+ if (!existsSync31(src)) {
13576
14334
  return 1;
13577
14335
  }
13578
- mkdirSync14(dirname12(dest), { recursive: true });
14336
+ mkdirSync15(dirname11(dest), { recursive: true });
13579
14337
  copyFileSync2(src, dest);
13580
14338
  summary.push("created|.roll/agent-routes.yaml");
13581
14339
  return 0;
13582
14340
  }
13583
14341
  function writeVersionStamp(projectDir, summary) {
13584
- const stampPath = join27(projectDir, ".roll", ".version");
13585
- if (existsSync30(stampPath)) {
14342
+ const stampPath = join28(projectDir, ".roll", ".version");
14343
+ if (existsSync31(stampPath)) {
13586
14344
  summary.push("unchanged|.roll/.version");
13587
14345
  return;
13588
14346
  }
13589
- mkdirSync14(join27(projectDir, ".roll"), { recursive: true });
14347
+ mkdirSync15(join28(projectDir, ".roll"), { recursive: true });
13590
14348
  const installedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
13591
- 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).
13592
14350
  # Used by \`_check_structure\` to recognise a previously-onboarded Roll project
13593
14351
  # without depending on directory-name heuristics.
13594
14352
  roll_version: "${rollVersion() || "unknown"}"
@@ -13726,7 +14484,7 @@ function initCommand(args) {
13726
14484
  return null;
13727
14485
  if (args[0] !== void 0 && args[0].startsWith("-"))
13728
14486
  return null;
13729
- if (!existsSync30(rollTemplates2()))
14487
+ if (!existsSync31(rollTemplates2()))
13730
14488
  return null;
13731
14489
  let projectDir;
13732
14490
  try {
@@ -13736,16 +14494,16 @@ function initCommand(args) {
13736
14494
  }
13737
14495
  let hasAgents = false;
13738
14496
  const summary = [];
13739
- if (existsSync30(join27(projectDir, "AGENTS.md"))) {
14497
+ if (existsSync31(join28(projectDir, "AGENTS.md"))) {
13740
14498
  hasAgents = true;
13741
14499
  } else if (isLegacyProject(projectDir)) {
13742
14500
  return null;
13743
14501
  }
13744
14502
  mergeGlobalToProject(projectDir, summary);
13745
14503
  mergeClaudeToProject(projectDir, summary);
13746
- writeBacklog(join27(projectDir, ".roll", "backlog.md"), summary);
13747
- ensureFeaturesDir(join27(projectDir, ".roll", "features"), summary);
13748
- 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);
13749
14507
  const routesTemplate = process.env["ROLL_AGENT_ROUTES_TEMPLATE"] ?? "default";
13750
14508
  initSeedAgentRoutes(routesTemplate, projectDir, summary);
13751
14509
  writeVersionStamp(projectDir, summary);
@@ -13758,19 +14516,19 @@ function initCommand(args) {
13758
14516
  }
13759
14517
 
13760
14518
  // packages/cli/dist/commands/lang.js
13761
- import { execFileSync as execFileSync7 } from "node:child_process";
13762
- import { existsSync as existsSync31, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync3, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync16 } from "node:fs";
13763
- import { homedir as homedir12, tmpdir as tmpdir3 } from "node:os";
13764
- import { dirname as dirname13, join as join28 } from "node:path";
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";
14521
+ import { homedir as homedir13, tmpdir as tmpdir3 } from "node:os";
14522
+ import { dirname as dirname12, join as join29 } from "node:path";
13765
14523
  function rollConfigPath4() {
13766
- const rollHome4 = process.env["ROLL_HOME"] ?? join28(homedir12(), ".roll");
13767
- return join28(rollHome4, "config.yaml");
14524
+ const rollHome4 = process.env["ROLL_HOME"] ?? join29(homedir13(), ".roll");
14525
+ return join29(rollHome4, "config.yaml");
13768
14526
  }
13769
14527
  function configLang2() {
13770
14528
  const cfg = rollConfigPath4();
13771
- if (!existsSync31(cfg))
14529
+ if (!existsSync32(cfg))
13772
14530
  return void 0;
13773
- for (const line of readFileSync24(cfg, "utf8").split("\n")) {
14531
+ for (const line of readFileSync25(cfg, "utf8").split("\n")) {
13774
14532
  const m7 = /^lang:\s*(.*)$/.exec(line);
13775
14533
  if (m7 !== null) {
13776
14534
  const v = (m7[1] ?? "").replace(/\s*#.*$/, "").trim();
@@ -13782,15 +14540,15 @@ function configLang2() {
13782
14540
  }
13783
14541
  function configHasLangLine() {
13784
14542
  const cfg = rollConfigPath4();
13785
- if (!existsSync31(cfg))
14543
+ if (!existsSync32(cfg))
13786
14544
  return false;
13787
- return readFileSync24(cfg, "utf8").split("\n").some((l) => /^lang:/.test(l));
14545
+ return readFileSync25(cfg, "utf8").split("\n").some((l) => /^lang:/.test(l));
13788
14546
  }
13789
14547
  function appleLang2() {
13790
14548
  if (process.platform !== "darwin")
13791
14549
  return void 0;
13792
14550
  try {
13793
- const out2 = execFileSync7("defaults", ["read", "-g", "AppleLanguages"], {
14551
+ const out2 = execFileSync8("defaults", ["read", "-g", "AppleLanguages"], {
13794
14552
  encoding: "utf8",
13795
14553
  stdio: ["ignore", "pipe", "ignore"]
13796
14554
  });
@@ -13818,7 +14576,7 @@ function resolveSource() {
13818
14576
  const env = process.env;
13819
14577
  if ((env["ROLL_LANG"] ?? "") !== "")
13820
14578
  return "ROLL_LANG env";
13821
- if (existsSync31(rollConfigPath4()) && configHasLangLine())
14579
+ if (existsSync32(rollConfigPath4()) && configHasLangLine())
13822
14580
  return `config (${rollConfigPath4()})`;
13823
14581
  if ((env["LC_ALL"] ?? "") !== "" || (env["LANG"] ?? "") !== "")
13824
14582
  return "LC_ALL/LANG";
@@ -13843,28 +14601,28 @@ function err10(line) {
13843
14601
  }
13844
14602
  function writeLang(value) {
13845
14603
  const cfg = rollConfigPath4();
13846
- mkdirSync15(dirname13(cfg), { recursive: true });
13847
- const existing = existsSync31(cfg) ? readFileSync24(cfg, "utf8") : "";
14604
+ mkdirSync16(dirname12(cfg), { recursive: true });
14605
+ const existing = existsSync32(cfg) ? readFileSync25(cfg, "utf8") : "";
13848
14606
  const kept = existing === "" ? [] : existing.split("\n").filter((l) => !/^lang:/.test(l));
13849
14607
  if (kept.length > 0 && kept[kept.length - 1] === "")
13850
14608
  kept.pop();
13851
14609
  const body = kept.length > 0 ? kept.join("\n") + "\n" : "";
13852
- const tmp = join28(mkdtempSync3(join28(tmpdir3(), "roll-lang-")), "config.yaml");
13853
- writeFileSync16(tmp, `${body}lang: ${value}
14610
+ const tmp = join29(mkdtempSync3(join29(tmpdir3(), "roll-lang-")), "config.yaml");
14611
+ writeFileSync17(tmp, `${body}lang: ${value}
13854
14612
  `);
13855
14613
  renameSync2(tmp, cfg);
13856
14614
  }
13857
14615
  function clearLang() {
13858
14616
  const cfg = rollConfigPath4();
13859
- if (!existsSync31(cfg))
14617
+ if (!existsSync32(cfg))
13860
14618
  return;
13861
- const existing = readFileSync24(cfg, "utf8");
14619
+ const existing = readFileSync25(cfg, "utf8");
13862
14620
  const kept = existing.split("\n").filter((l) => !/^lang:/.test(l));
13863
14621
  if (kept.length > 0 && kept[kept.length - 1] === "")
13864
14622
  kept.pop();
13865
14623
  const body = kept.length > 0 ? kept.join("\n") + "\n" : "";
13866
- const tmp = join28(mkdtempSync3(join28(tmpdir3(), "roll-lang-")), "config.yaml");
13867
- writeFileSync16(tmp, body);
14624
+ const tmp = join29(mkdtempSync3(join29(tmpdir3(), "roll-lang-")), "config.yaml");
14625
+ writeFileSync17(tmp, body);
13868
14626
  renameSync2(tmp, cfg);
13869
14627
  }
13870
14628
  function langCommand(args) {
@@ -13964,9 +14722,9 @@ async function loopFmtCommand(args) {
13964
14722
 
13965
14723
  // packages/cli/dist/commands/loop-pr-inbox.js
13966
14724
  import { spawn as spawn2 } from "node:child_process";
13967
- import { appendFileSync as appendFileSync5, existsSync as existsSync32, mkdirSync as mkdirSync16, readFileSync as readFileSync25, writeFileSync as writeFileSync17 } from "node:fs";
13968
- import { homedir as homedir13 } from "node:os";
13969
- import { dirname as dirname14, join as join29 } from "node:path";
14725
+ import { appendFileSync as appendFileSync5, existsSync as existsSync33, mkdirSync as mkdirSync17, readFileSync as readFileSync26, writeFileSync as writeFileSync18 } from "node:fs";
14726
+ import { homedir as homedir14 } from "node:os";
14727
+ import { dirname as dirname13, join as join30 } from "node:path";
13970
14728
  function reducePrView(raw) {
13971
14729
  const reviews = raw.reviews ?? [];
13972
14730
  const botReviews = reviews.filter((r) => r.authorAssociation === "BOT" || r.authorAssociation === "APP");
@@ -14049,7 +14807,7 @@ function runtimeDir() {
14049
14807
  const override = (process.env["ROLL_PROJECT_RUNTIME_DIR"] ?? "").trim();
14050
14808
  if (override !== "")
14051
14809
  return override;
14052
- return join29(process.cwd(), ".roll", "loop");
14810
+ return join30(process.cwd(), ".roll", "loop");
14053
14811
  }
14054
14812
  function projSlug() {
14055
14813
  const override = (process.env["ROLL_MAIN_SLUG"] ?? "").trim();
@@ -14058,26 +14816,26 @@ function projSlug() {
14058
14816
  return process.cwd().split("/").filter(Boolean).pop() ?? "default";
14059
14817
  }
14060
14818
  function alertPath() {
14061
- return join29(runtimeDir(), `ALERT-${projSlug()}.md`);
14819
+ return join30(runtimeDir(), `ALERT-${projSlug()}.md`);
14062
14820
  }
14063
14821
  function statePath() {
14064
- return join29(runtimeDir(), `state-${projSlug()}.yaml`);
14822
+ return join30(runtimeDir(), `state-${projSlug()}.yaml`);
14065
14823
  }
14066
14824
  function tickPath() {
14067
- return join29(runtimeDir(), "pr-tick.jsonl");
14825
+ return join30(runtimeDir(), "pr-tick.jsonl");
14068
14826
  }
14069
14827
  function engineBin() {
14070
- let dir = dirname14(new URL(import.meta.url).pathname);
14828
+ let dir = dirname13(new URL(import.meta.url).pathname);
14071
14829
  for (let i = 0; i < 10; i++) {
14072
- const candidate = join29(dir, "bin", "roll");
14073
- if (existsSync32(candidate))
14830
+ const candidate = join30(dir, "bin", "roll");
14831
+ if (existsSync33(candidate))
14074
14832
  return candidate;
14075
- const parent = dirname14(dir);
14833
+ const parent = dirname13(dir);
14076
14834
  if (parent === dir)
14077
14835
  break;
14078
14836
  dir = parent;
14079
14837
  }
14080
- return join29(homedir13(), ".local", "lib", "roll", "bin", "roll");
14838
+ return join30(homedir14(), ".local", "lib", "roll", "bin", "roll");
14081
14839
  }
14082
14840
  function pal2() {
14083
14841
  return (process.env["NO_COLOR"] ?? "") !== "" ? { yellow: "", nc: "" } : { yellow: "\x1B[0;33m", nc: "\x1B[0m" };
@@ -14087,21 +14845,21 @@ function nowSec() {
14087
14845
  }
14088
14846
  function writeTickFile(tick) {
14089
14847
  const file = tickPath();
14090
- mkdirSync16(dirname14(file), { recursive: true });
14848
+ mkdirSync17(dirname13(file), { recursive: true });
14091
14849
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
14092
14850
  appendFileSync5(file, `${JSON.stringify({ ts, ...tick })}
14093
14851
  `);
14094
14852
  try {
14095
- const lines = readFileSync25(file, "utf8").split("\n").filter((l) => l !== "");
14853
+ const lines = readFileSync26(file, "utf8").split("\n").filter((l) => l !== "");
14096
14854
  if (lines.length > 500)
14097
- writeFileSync17(file, `${lines.slice(-500).join("\n")}
14855
+ writeFileSync18(file, `${lines.slice(-500).join("\n")}
14098
14856
  `);
14099
14857
  } catch {
14100
14858
  }
14101
14859
  }
14102
14860
  function appendAlert(line) {
14103
14861
  const file = alertPath();
14104
- mkdirSync16(dirname14(file), { recursive: true });
14862
+ mkdirSync17(dirname13(file), { recursive: true });
14105
14863
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
14106
14864
  appendFileSync5(file, `[${ts}] ${line}
14107
14865
  `);
@@ -14110,16 +14868,16 @@ function rebaseCircuitAllowed(num2) {
14110
14868
  const state = statePath();
14111
14869
  let body = "";
14112
14870
  try {
14113
- body = readFileSync25(state, "utf8");
14871
+ body = readFileSync26(state, "utf8");
14114
14872
  } catch {
14115
14873
  }
14116
14874
  const verdict = rebaseCircuitVerdict(parseRebaseAttempts(body, num2), nowSec());
14117
14875
  writeRebaseAttempts(state, num2, verdict.freshTimestamps);
14118
14876
  if (!verdict.allowed) {
14119
14877
  const file = alertPath();
14120
- mkdirSync16(dirname14(file), { recursive: true });
14878
+ mkdirSync17(dirname13(file), { recursive: true });
14121
14879
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
14122
- writeFileSync17(file, [
14880
+ writeFileSync18(file, [
14123
14881
  `# ALERT \u2014 PR rebase circuit breaker tripped`,
14124
14882
  ``,
14125
14883
  `**Time**: ${stamp}`,
@@ -14173,13 +14931,13 @@ function upsertRebaseAttempts(stateBody, pr, value) {
14173
14931
  `;
14174
14932
  }
14175
14933
  function writeRebaseAttempts(state, pr, timestamps) {
14176
- mkdirSync16(dirname14(state), { recursive: true });
14934
+ mkdirSync17(dirname13(state), { recursive: true });
14177
14935
  let body = "";
14178
14936
  try {
14179
- body = readFileSync25(state, "utf8");
14937
+ body = readFileSync26(state, "utf8");
14180
14938
  } catch {
14181
14939
  }
14182
- writeFileSync17(state, upsertRebaseAttempts(body, pr, renderRebaseAttempts(timestamps)));
14940
+ writeFileSync18(state, upsertRebaseAttempts(body, pr, renderRebaseAttempts(timestamps)));
14183
14941
  }
14184
14942
  function bridgeBash(args) {
14185
14943
  return new Promise((resolve3) => {
@@ -14266,19 +15024,19 @@ async function loopPrInboxCommand(_args, deps = realDeps2()) {
14266
15024
  }
14267
15025
 
14268
15026
  // packages/cli/dist/commands/loop-run-once.js
14269
- import { appendFileSync as appendFileSync7, existsSync as existsSync36, mkdirSync as mkdirSync18, readFileSync as readFileSync28, writeFileSync as writeFileSync19 } from "node:fs";
14270
- import { dirname as dirname16, 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";
14271
15029
 
14272
15030
  // packages/cli/dist/runner/executor.js
14273
15031
  import { execFile as execFile8 } from "node:child_process";
14274
- 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";
14275
- import { dirname as dirname15, 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";
14276
15034
  import { promisify as promisify8 } from "node:util";
14277
15035
 
14278
15036
  // packages/cli/dist/runner/peer-gate.js
14279
15037
  import { execFile as execFile7 } from "node:child_process";
14280
- import { existsSync as existsSync33, readdirSync as readdirSync15 } from "node:fs";
14281
- 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";
14282
15040
  import { promisify as promisify7 } from "node:util";
14283
15041
  var execFileAsync7 = promisify7(execFile7);
14284
15042
  var HIGH_RISK = [/^\.github\/workflows\//, /^packages\/infra\/src\/(git|github|process)\.ts$/];
@@ -14308,8 +15066,8 @@ async function cycleChangedFiles(worktreeCwd) {
14308
15066
  }
14309
15067
  }
14310
15068
  function peerEvidencePresent(runtimeDir3, cycleId) {
14311
- const dir = join30(runtimeDir3, "peer");
14312
- if (!existsSync33(dir))
15069
+ const dir = join31(runtimeDir3, "peer");
15070
+ if (!existsSync34(dir))
14313
15071
  return false;
14314
15072
  try {
14315
15073
  return readdirSync15(dir).some((f) => f.startsWith(`cycle-${cycleId}.`));
@@ -14336,24 +15094,18 @@ async function runPeerGate(worktreeCwd, runtimeDir3, cycleId, sinks) {
14336
15094
  }
14337
15095
 
14338
15096
  // packages/cli/dist/runner/attest-gate.js
14339
- import { existsSync as existsSync34, readFileSync as readFileSync26, statSync as statSync16 } from "node:fs";
14340
- 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";
14341
15099
  function reportCandidates(worktreeCwd, storyId) {
14342
- return [
14343
- join31(cardArchiveDir(worktreeCwd, storyId), "latest", reportFileName(storyId)),
14344
- join31(legacyArchiveDir(worktreeCwd, storyId), "latest", "report.html")
14345
- ];
15100
+ return [join32(cardArchiveDir(worktreeCwd, storyId), "latest", reportFileName(storyId))];
14346
15101
  }
14347
15102
  function acMapCandidates(worktreeCwd, storyId) {
14348
- return [
14349
- join31(cardArchiveDir(worktreeCwd, storyId), "ac-map.json"),
14350
- join31(legacyArchiveDir(worktreeCwd, storyId), "ac-map.json")
14351
- ];
15103
+ return [join32(cardArchiveDir(worktreeCwd, storyId), "ac-map.json")];
14352
15104
  }
14353
15105
  function existingReport(worktreeCwd, storyId) {
14354
15106
  for (const p of reportCandidates(worktreeCwd, storyId)) {
14355
15107
  try {
14356
- if (statSync16(p).isFile())
15108
+ if (statSync17(p).isFile())
14357
15109
  return p;
14358
15110
  } catch {
14359
15111
  }
@@ -14367,7 +15119,7 @@ function verificationReportFresh(worktreeCwd, storyId, sinceSec) {
14367
15119
  if (p === null)
14368
15120
  return false;
14369
15121
  try {
14370
- const st = statSync16(p);
15122
+ const st = statSync17(p);
14371
15123
  if (sinceSec === void 0)
14372
15124
  return true;
14373
15125
  return st.mtimeMs / 1e3 >= sinceSec;
@@ -14382,9 +15134,9 @@ function verificationReportHasContent(worktreeCwd, storyId) {
14382
15134
  if (p === null)
14383
15135
  return false;
14384
15136
  try {
14385
- const html = readFileSync26(p, "utf8");
15137
+ const html = readFileSync27(p, "utf8");
14386
15138
  const hasAc = /<section class="ac[\s">]/.test(html);
14387
- const hasMap = acMapCandidates(worktreeCwd, storyId).some((m7) => existsSync34(m7));
15139
+ const hasMap = acMapCandidates(worktreeCwd, storyId).some((m7) => existsSync35(m7));
14388
15140
  return hasAc && hasMap;
14389
15141
  } catch {
14390
15142
  return false;
@@ -14392,10 +15144,10 @@ function verificationReportHasContent(worktreeCwd, storyId) {
14392
15144
  }
14393
15145
  function readAttestGateMode(repoCwd) {
14394
15146
  try {
14395
- const p = join31(repoCwd, ".roll", "policy.yaml");
14396
- if (!existsSync34(p))
15147
+ const p = join32(repoCwd, ".roll", "policy.yaml");
15148
+ if (!existsSync35(p))
14397
15149
  return "soft";
14398
- return parsePolicy(readFileSync26(p, "utf8")).loopSafety.attestGate === "hard" ? "hard" : "soft";
15150
+ return parsePolicy(readFileSync27(p, "utf8")).loopSafety.attestGate === "hard" ? "hard" : "soft";
14399
15151
  } catch {
14400
15152
  return "soft";
14401
15153
  }
@@ -14503,9 +15255,9 @@ async function executeCommand(cmd, ports, ctx) {
14503
15255
  // execute: spawn the agent (TCR commits happen inside the worktree). The
14504
15256
  // exit code + timeout feed back as agent_exited; usage is captured for cost.
14505
15257
  case "spawn_agent": {
14506
- const livePath = join32(dirname15(ports.paths.eventsPath), "live.log");
15258
+ const livePath = join33(dirname14(ports.paths.eventsPath), "live.log");
14507
15259
  try {
14508
- 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
14509
15261
  `);
14510
15262
  } catch {
14511
15263
  }
@@ -14523,9 +15275,9 @@ async function executeCommand(cmd, ports, ctx) {
14523
15275
  }
14524
15276
  });
14525
15277
  try {
14526
- const logDir = join32(dirname15(ports.paths.eventsPath), "cycle-logs");
14527
- mkdirSync17(logDir, { recursive: true });
14528
- 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}
14529
15281
  --- stdout ---
14530
15282
  ${res.stdout}
14531
15283
  --- stderr ---
@@ -14572,7 +15324,7 @@ ${res.stderr}
14572
15324
  tcrCount = await ports.git.tcrCount(ports.paths.worktreePath);
14573
15325
  } catch {
14574
15326
  }
14575
- await runPeerGate(ports.paths.worktreePath, dirname15(ports.paths.eventsPath), ctx.cycleId ?? "", {
15327
+ await runPeerGate(ports.paths.worktreePath, dirname14(ports.paths.eventsPath), ctx.cycleId ?? "", {
14576
15328
  alert: (m7) => ports.events.appendAlert(ports.paths.alertsPath, m7),
14577
15329
  event: (p) => ports.events.appendEvent(ports.paths.eventsPath, {
14578
15330
  type: "peer:gate",
@@ -14648,7 +15400,7 @@ ${res.stderr}
14648
15400
  // _worktree_cleanup (tolerant). Side effect; no feedback (terminal path).
14649
15401
  case "cleanup_worktree":
14650
15402
  try {
14651
- const dst = join32(ports.paths.worktreePath, ".roll");
15403
+ const dst = join33(ports.paths.worktreePath, ".roll");
14652
15404
  if (lstatSync2(dst, { throwIfNoEntry: false })?.isSymbolicLink() === true)
14653
15405
  unlinkSync(dst);
14654
15406
  } catch {
@@ -14720,9 +15472,9 @@ function buildRunRow(cmd, ctx, nowSec2) {
14720
15472
  }
14721
15473
  function readRunsRows(runsPath) {
14722
15474
  try {
14723
- if (!existsSync35(runsPath))
15475
+ if (!existsSync36(runsPath))
14724
15476
  return [];
14725
- 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) => {
14726
15478
  try {
14727
15479
  return JSON.parse(l);
14728
15480
  } catch {
@@ -14745,15 +15497,15 @@ function sleep(ms) {
14745
15497
  }
14746
15498
  async function linkRollIntoWorktree(repoCwd, worktreePath) {
14747
15499
  try {
14748
- const src = join32(repoCwd, ".roll");
14749
- const dst = join32(worktreePath, ".roll");
14750
- if (!existsSync35(src))
15500
+ const src = join33(repoCwd, ".roll");
15501
+ const dst = join33(worktreePath, ".roll");
15502
+ if (!existsSync36(src))
14751
15503
  return;
14752
15504
  const dstStat = lstatSync2(dst, { throwIfNoEntry: false });
14753
15505
  if (dstStat) {
14754
15506
  if (dstStat.isSymbolicLink())
14755
15507
  return;
14756
- 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"));
14757
15509
  if (!incompleteFossil)
14758
15510
  return;
14759
15511
  rmSync8(dst, { recursive: true, force: true });
@@ -14762,10 +15514,10 @@ async function linkRollIntoWorktree(repoCwd, worktreePath) {
14762
15514
  const common = (await execFileAsync8("git", ["-C", repoCwd, "rev-parse", "--path-format=absolute", "--git-common-dir"])).stdout.trim();
14763
15515
  if (common === "")
14764
15516
  return;
14765
- const exclude = join32(common, "info", "exclude");
14766
- const cur = existsSync35(exclude) ? readFileSync27(exclude, "utf8") : "";
15517
+ const exclude = join33(common, "info", "exclude");
15518
+ const cur = existsSync36(exclude) ? readFileSync28(exclude, "utf8") : "";
14767
15519
  if (!/^\.roll$/m.test(cur)) {
14768
- mkdirSync17(dirname15(exclude), { recursive: true });
15520
+ mkdirSync18(dirname14(exclude), { recursive: true });
14769
15521
  appendFileSync6(exclude, `${cur === "" || cur.endsWith("\n") ? "" : "\n"}.roll
14770
15522
  `, "utf8");
14771
15523
  }
@@ -14854,10 +15606,10 @@ function nodePorts(opts) {
14854
15606
  },
14855
15607
  backlog: {
14856
15608
  read(projectCwd) {
14857
- const p = join32(projectCwd, ".roll", "backlog.md");
14858
- if (!existsSync35(p))
15609
+ const p = join33(projectCwd, ".roll", "backlog.md");
15610
+ if (!existsSync36(p))
14859
15611
  return [];
14860
- return parseBacklog(readFileSync27(p, "utf8"));
15612
+ return parseBacklog(readFileSync28(p, "utf8"));
14861
15613
  },
14862
15614
  // FIX-198: the production binding was MISSING entirely (the optional
14863
15615
  // chain made every In-Progress claim a silent no-op). ID-anchored mark
@@ -14865,8 +15617,8 @@ function nodePorts(opts) {
14865
15617
  // never kill the cycle, the reconcile pass is the safety net.
14866
15618
  markStatus(projectCwd, id, status2) {
14867
15619
  try {
14868
- const p = join32(projectCwd, ".roll", "backlog.md");
14869
- if (!existsSync35(p))
15620
+ const p = join33(projectCwd, ".roll", "backlog.md");
15621
+ if (!existsSync36(p))
14870
15622
  return;
14871
15623
  const store = new BacklogStore();
14872
15624
  const snap = store.readBacklog(p);
@@ -14886,7 +15638,7 @@ function nodePorts(opts) {
14886
15638
  };
14887
15639
  }
14888
15640
  function appendAlertLine(alertsPath, message) {
14889
- mkdirSync17(dirname15(alertsPath), { recursive: true });
15641
+ mkdirSync18(dirname14(alertsPath), { recursive: true });
14890
15642
  appendFileSync6(alertsPath, `${message}
14891
15643
  `, "utf8");
14892
15644
  }
@@ -15063,6 +15815,7 @@ function describeCommand(cmd) {
15063
15815
 
15064
15816
  // packages/cli/dist/commands/loop-run-once.js
15065
15817
  import { spawn as spawn3 } from "node:child_process";
15818
+ import { lookup } from "node:dns/promises";
15066
15819
  function announceReport(projectPath, slug, storyId, opener = (p) => {
15067
15820
  try {
15068
15821
  spawn3("open", [p], { stdio: "ignore", detached: true }).unref();
@@ -15071,13 +15824,13 @@ function announceReport(projectPath, slug, storyId, opener = (p) => {
15071
15824
  }) {
15072
15825
  if (storyId === "")
15073
15826
  return null;
15074
- const report = join33(projectPath, ".roll", "verification", storyId, "latest", "report.html");
15075
- if (!existsSync36(report))
15827
+ const report = join34(cardArchiveDir(projectPath, storyId), "latest", reportFileName(storyId));
15828
+ if (!existsSync37(report))
15076
15829
  return null;
15077
15830
  process.stdout.write(`evidence: ${report}
15078
15831
  \u9A8C\u6536\u62A5\u544A: ${report}
15079
15832
  `);
15080
- 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}`));
15081
15834
  if (!muted)
15082
15835
  opener(report);
15083
15836
  return report;
@@ -15094,7 +15847,7 @@ function cycleSignalTeardown(paths, cycleId, branch, sig, deps = {}) {
15094
15847
  }
15095
15848
  let owned = false;
15096
15849
  try {
15097
- owned = existsSync36(paths.lockPath) && parseLock(readFileSync28(paths.lockPath, "utf8")).pid === pid;
15850
+ owned = existsSync37(paths.lockPath) && parseLock(readFileSync29(paths.lockPath, "utf8")).pid === pid;
15098
15851
  } catch {
15099
15852
  owned = false;
15100
15853
  }
@@ -15139,27 +15892,27 @@ function makeCycleId(now = /* @__PURE__ */ new Date(), pid = process.pid) {
15139
15892
  }
15140
15893
  function runtimeDir2(projectPath) {
15141
15894
  const env = (process.env["ROLL_PROJECT_RUNTIME_DIR"] ?? "").trim();
15142
- return env !== "" ? env : join33(projectPath, ".roll", "loop");
15895
+ return env !== "" ? env : join34(projectPath, ".roll", "loop");
15143
15896
  }
15144
15897
  var PAUSE_THRESHOLD = 3;
15145
15898
  function incrementConsecutiveFails(projectPath, slug, alertsPath, cycleId, storyId, terminal) {
15146
15899
  const rt = runtimeDir2(projectPath);
15147
- const counterFile = join33(rt, "consecutive-fails");
15900
+ const counterFile = join34(rt, "consecutive-fails");
15148
15901
  let count = 0;
15149
15902
  try {
15150
- if (existsSync36(counterFile)) {
15151
- count = parseInt(readFileSync28(counterFile, "utf8").trim(), 10) || 0;
15903
+ if (existsSync37(counterFile)) {
15904
+ count = parseInt(readFileSync29(counterFile, "utf8").trim(), 10) || 0;
15152
15905
  }
15153
15906
  } catch {
15154
15907
  }
15155
15908
  count += 1;
15156
15909
  try {
15157
- writeFileSync19(counterFile, String(count), "utf8");
15910
+ writeFileSync20(counterFile, String(count), "utf8");
15158
15911
  } catch {
15159
15912
  }
15160
15913
  if (count < PAUSE_THRESHOLD)
15161
15914
  return;
15162
- const pauseMarker = join33(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15915
+ const pauseMarker = join34(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15163
15916
  const alertMsg = `# ALERT \u2014 loop auto-paused after ${count} consecutive failures
15164
15917
 
15165
15918
  **Cycle**: ${cycleId}
@@ -15169,7 +15922,7 @@ function incrementConsecutiveFails(projectPath, slug, alertsPath, cycleId, story
15169
15922
  Resolve the root cause, then: \`roll loop resume\`
15170
15923
  `;
15171
15924
  try {
15172
- writeFileSync19(pauseMarker, alertMsg, "utf8");
15925
+ writeFileSync20(pauseMarker, alertMsg, "utf8");
15173
15926
  appendFileSync7(alertsPath, `${alertMsg}
15174
15927
  `, "utf8");
15175
15928
  } catch {
@@ -15181,7 +15934,7 @@ loop run-once: \u8FDE\u7EED ${count} \u6B21\u5931\u8D25\u540E\u81EA\u52A8\u6682\
15181
15934
  function resetConsecutiveFails(projectPath) {
15182
15935
  const rt = runtimeDir2(projectPath);
15183
15936
  try {
15184
- writeFileSync19(join33(rt, "consecutive-fails"), "0", "utf8");
15937
+ writeFileSync20(join34(rt, "consecutive-fails"), "0", "utf8");
15185
15938
  } catch {
15186
15939
  }
15187
15940
  }
@@ -15191,6 +15944,38 @@ function readSkillBody2(projectPath) {
15191
15944
  envOverride: process.env["ROLL_LOOP_SKILL"]
15192
15945
  });
15193
15946
  }
15947
+ function buildLoopRouteDeps(projectPath) {
15948
+ function readSlot(slot) {
15949
+ const agentsYaml = join34(projectPath, ".roll", "agents.yaml");
15950
+ try {
15951
+ return readSlotFromText(readFileSync29(agentsYaml, "utf8"), slot);
15952
+ } catch {
15953
+ return void 0;
15954
+ }
15955
+ }
15956
+ function firstInstalled() {
15957
+ const fromLocal = readField(join34(projectPath, ".roll", "local.yaml"), /^agent:/);
15958
+ if (fromLocal !== void 0)
15959
+ return fromLocal;
15960
+ return firstInstalledAgent(realAgentEnv());
15961
+ }
15962
+ return { readSlot, firstInstalled };
15963
+ }
15964
+ function readField(path, re) {
15965
+ try {
15966
+ const text = readFileSync29(path, "utf8");
15967
+ for (const line of text.split("\n")) {
15968
+ const m7 = line.match(re);
15969
+ if (m7 !== null) {
15970
+ const v = line.slice((m7.index ?? 0) + m7[0].length).trim();
15971
+ if (v !== "")
15972
+ return v.replace(/^["']|["']$/g, "");
15973
+ }
15974
+ }
15975
+ } catch {
15976
+ }
15977
+ return void 0;
15978
+ }
15194
15979
  async function loopRunOnceCommand(args) {
15195
15980
  const dryRun = args.includes("--dry-run");
15196
15981
  const id = await projectIdentity();
@@ -15214,15 +15999,15 @@ async function loopRunOnceCommand(args) {
15214
15999
  return 0;
15215
16000
  }
15216
16001
  const rt = runtimeDir2(id.path);
15217
- const alertsPath = join33(rt, `ALERT-${id.slug}.md`);
15218
- mkdirSync18(dirname16(alertsPath), { recursive: true });
16002
+ const alertsPath = join34(rt, `ALERT-${id.slug}.md`);
16003
+ mkdirSync19(dirname15(alertsPath), { recursive: true });
15219
16004
  const paths = {
15220
- eventsPath: join33(rt, "events.ndjson"),
15221
- runsPath: join33(rt, "runs.jsonl"),
16005
+ eventsPath: join34(rt, "events.ndjson"),
16006
+ runsPath: join34(rt, "runs.jsonl"),
15222
16007
  alertsPath,
15223
- lockPath: join33(rt, "inner.lock"),
15224
- heartbeatPath: join33(rt, "heartbeat"),
15225
- worktreePath: join33(rt, "worktrees", `cycle-${cycleId}`)
16008
+ lockPath: join34(rt, "inner.lock"),
16009
+ heartbeatPath: join34(rt, "heartbeat"),
16010
+ worktreePath: join34(rt, "worktrees", `cycle-${cycleId}`)
15226
16011
  };
15227
16012
  const skillBody = readSkillBody2(id.path);
15228
16013
  if (skillBody === null) {
@@ -15238,15 +16023,16 @@ loop run-once: \u627E\u4E0D\u5230 roll-loop SKILL.md \u2014 \u62D2\u7EDD\u76F2\u
15238
16023
  incrementConsecutiveFails(id.path, id.slug, alertsPath, cycleId, "", "skill_missing");
15239
16024
  return 1;
15240
16025
  }
15241
- const routeDeps = {
15242
- readSlot: () => void 0,
15243
- firstInstalled: () => process.env["ROLL_LOOP_AGENT"] ?? "claude"
15244
- };
16026
+ const routeDeps = buildLoopRouteDeps(id.path);
16027
+ const isInteractive = (process.env["ROLL_LOOP_FORCE"] ?? "").trim() !== "";
15245
16028
  const ports = nodePorts({
15246
16029
  repoCwd: id.path,
15247
16030
  paths,
15248
16031
  skillBody,
15249
- routeDeps
16032
+ routeDeps,
16033
+ ...isInteractive ? {
16034
+ agentSpawn: (agent, opts) => realAgentSpawn(agent, { ...opts, interactive: true })
16035
+ } : {}
15250
16036
  });
15251
16037
  const disposeSignals = installCycleSignalTeardown(paths, cycleId, branch);
15252
16038
  let result;
@@ -15269,24 +16055,43 @@ loop run-once: \u627E\u4E0D\u5230 roll-loop SKILL.md \u2014 \u62D2\u7EDD\u76F2\u
15269
16055
  }
15270
16056
  const isFail = result.terminal === "failed" || result.terminal === "blocked";
15271
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
+ }
15272
16062
  const storyId = (result.state?.ctx?.storyId ?? "").trim();
15273
16063
  incrementConsecutiveFails(id.path, id.slug, alertsPath, cycleId, storyId, result.terminal ?? "unknown");
15274
16064
  }
15275
16065
  return isFail ? 1 : 0;
15276
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
+ }
15277
16082
 
15278
16083
  // packages/cli/dist/commands/loop-sched.js
15279
16084
  import { createHash as createHash4 } from "node:crypto";
15280
16085
  import { spawn as spawn4, spawnSync as spawnSync6 } from "node:child_process";
15281
- import { existsSync as existsSync37, mkdirSync as mkdirSync19, readFileSync as readFileSync29, rmSync as rmSync9, writeFileSync as writeFileSync20 } from "node:fs";
15282
- import { homedir as homedir14 } from "node:os";
15283
- import { dirname as dirname17, join as join34 } from "node:path";
16086
+ import { existsSync as existsSync38, mkdirSync as mkdirSync20, readFileSync as readFileSync30, rmSync as rmSync9, writeFileSync as writeFileSync21 } from "node:fs";
16087
+ import { homedir as homedir15 } from "node:os";
16088
+ import { dirname as dirname16, join as join35 } from "node:path";
15284
16089
  function realDeps3() {
15285
16090
  return {
15286
16091
  identity: () => projectIdentity(),
15287
16092
  uid: () => process.getuid?.() ?? 501,
15288
- sharedRoot: () => process.env["ROLL_SHARED_ROOT"] || join34(homedir14(), ".shared", "roll"),
15289
- launchdDir: () => join34(homedir14(), "Library", "LaunchAgents"),
16093
+ sharedRoot: () => process.env["ROLL_SHARED_ROOT"] || join35(homedir15(), ".shared", "roll"),
16094
+ launchdDir: () => join35(homedir15(), "Library", "LaunchAgents"),
15290
16095
  launchd: { reinstall, uninstall, isLoaded },
15291
16096
  execRunner: (runner) => new Promise((resolve3) => {
15292
16097
  const child = spawn4("bash", [runner], {
@@ -15306,8 +16111,8 @@ function realDeps3() {
15306
16111
  // The `loop now` inline observation: tail live.log while the cycle holds
15307
16112
  // the inner lock; Ctrl-C stops the TAIL only (the cycle lives in tmux).
15308
16113
  observe: (rt) => new Promise((resolve3) => {
15309
- const lock = join34(rt, "inner.lock");
15310
- 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" });
15311
16116
  let sawLock = false;
15312
16117
  const t0 = Date.now();
15313
16118
  const finish = () => {
@@ -15319,9 +16124,9 @@ function realDeps3() {
15319
16124
  resolve3();
15320
16125
  };
15321
16126
  const timer = setInterval(() => {
15322
- if (existsSync37(lock))
16127
+ if (existsSync38(lock))
15323
16128
  sawLock = true;
15324
- const done = sawLock ? !existsSync37(lock) : Date.now() - t0 > 3e4;
16129
+ const done = sawLock ? !existsSync38(lock) : Date.now() - t0 > 3e4;
15325
16130
  if (done) {
15326
16131
  clearInterval(timer);
15327
16132
  finish();
@@ -15478,7 +16283,7 @@ function parseLoopPeriodMinutes(text) {
15478
16283
  return 30;
15479
16284
  }
15480
16285
  function pathValue() {
15481
- const home = homedir14();
16286
+ const home = homedir15();
15482
16287
  return [
15483
16288
  "/opt/homebrew/bin",
15484
16289
  `${home}/.local/bin`,
@@ -15490,11 +16295,11 @@ function pathValue() {
15490
16295
  ].join(":");
15491
16296
  }
15492
16297
  function writeExecutable(path, content) {
15493
- mkdirSync19(dirname17(path), { recursive: true });
15494
- writeFileSync20(path, content, { mode: 493 });
16298
+ mkdirSync20(dirname16(path), { recursive: true });
16299
+ writeFileSync21(path, content, { mode: 493 });
15495
16300
  }
15496
16301
  function pauseMarkerPath(projectPath, slug) {
15497
- return join34(projectPath, ".roll", "loop", `PAUSE-${slug}`);
16302
+ return join35(projectPath, ".roll", "loop", `PAUSE-${slug}`);
15498
16303
  }
15499
16304
  var LOOP_SERVICES = ["loop", "dream", "pr"];
15500
16305
  async function mountService(deps, uid, label4, plist) {
@@ -15513,16 +16318,16 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15513
16318
  const shared = deps.sharedRoot();
15514
16319
  const ld = deps.launchdDir();
15515
16320
  const uid = deps.uid();
15516
- mkdirSync19(ld, { recursive: true });
16321
+ mkdirSync20(ld, { recursive: true });
15517
16322
  let period = 30;
15518
- const localYaml = join34(id.path, ".roll", "local.yaml");
15519
- if (existsSync37(localYaml)) {
16323
+ const localYaml = join35(id.path, ".roll", "local.yaml");
16324
+ if (existsSync38(localYaml)) {
15520
16325
  try {
15521
- period = parseLoopPeriodMinutes(readFileSync29(localYaml, "utf8"));
16326
+ period = parseLoopPeriodMinutes(readFileSync30(localYaml, "utf8"));
15522
16327
  } catch {
15523
16328
  }
15524
16329
  }
15525
- const loopRunner = join34(shared, "loop", `run-${id.slug}.sh`);
16330
+ const loopRunner = join35(shared, "loop", `run-${id.slug}.sh`);
15526
16331
  const rollBinOverride = (process.env["ROLL_RUNNER_ROLL_BIN"] ?? "").trim();
15527
16332
  writeExecutable(loopRunner, buildLoopRunnerScript({
15528
16333
  projectPath: id.path,
@@ -15533,7 +16338,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15533
16338
  }));
15534
16339
  const loopLabel = launchdLabel("loop", id.slug);
15535
16340
  const loopPlist = launchdPlistPath("loop", id.slug, ld);
15536
- writeFileSync20(loopPlist, plistContent({
16341
+ writeFileSync21(loopPlist, plistContent({
15537
16342
  label: loopLabel,
15538
16343
  runnerScript: loopRunner,
15539
16344
  projectPath: id.path,
@@ -15541,14 +16346,14 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15541
16346
  schedule: { kind: "interval", periodMinutes: period }
15542
16347
  }));
15543
16348
  const loopMount = await mountService(deps, uid, loopLabel, loopPlist);
15544
- const prRunner = join34(shared, "pr", `run-${id.slug}.sh`);
16349
+ const prRunner = join35(shared, "pr", `run-${id.slug}.sh`);
15545
16350
  writeExecutable(prRunner, buildPrRunnerScript({
15546
16351
  projectPath: id.path,
15547
16352
  ...rollBinOverride !== "" ? { rollBin: rollBinOverride } : {}
15548
16353
  }));
15549
16354
  const prLabel = launchdLabel("pr", id.slug);
15550
16355
  const prPlist = launchdPlistPath("pr", id.slug, ld);
15551
- writeFileSync20(prPlist, plistContent({
16356
+ writeFileSync21(prPlist, plistContent({
15552
16357
  label: prLabel,
15553
16358
  runnerScript: prRunner,
15554
16359
  projectPath: id.path,
@@ -15557,7 +16362,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15557
16362
  }));
15558
16363
  const prMount = await mountService(deps, uid, prLabel, prPlist);
15559
16364
  const dream = dreamScheduleFor(id.path);
15560
- const dreamRunner = join34(shared, "dream", `run-${id.slug}.sh`);
16365
+ const dreamRunner = join35(shared, "dream", `run-${id.slug}.sh`);
15561
16366
  writeExecutable(dreamRunner, buildDreamRunnerScript({
15562
16367
  projectPath: id.path,
15563
16368
  slug: id.slug,
@@ -15565,7 +16370,7 @@ async function loopOnCommand(_args, deps = realDeps3()) {
15565
16370
  }));
15566
16371
  const dreamLabel = launchdLabel("dream", id.slug);
15567
16372
  const dreamPlist = launchdPlistPath("dream", id.slug, ld);
15568
- writeFileSync20(dreamPlist, plistContent({
16373
+ writeFileSync21(dreamPlist, plistContent({
15569
16374
  label: dreamLabel,
15570
16375
  runnerScript: dreamRunner,
15571
16376
  projectPath: id.path,
@@ -15617,10 +16422,10 @@ Loop \u5DF2\u505C\u7528(loop/dream/pr \u5747\u5DF2\u5378\u8F7D)
15617
16422
  async function loopPauseCommand(_args, deps = realDeps3()) {
15618
16423
  const id = await deps.identity();
15619
16424
  const marker = pauseMarkerPath(id.path, id.slug);
15620
- mkdirSync19(dirname17(marker), { recursive: true });
15621
- const already = existsSync37(marker);
16425
+ mkdirSync20(dirname16(marker), { recursive: true });
16426
+ const already = existsSync38(marker);
15622
16427
  if (!already)
15623
- writeFileSync20(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
16428
+ writeFileSync21(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
15624
16429
  `);
15625
16430
  process.stdout.write(already ? `Loop already paused
15626
16431
  Loop \u5DF2\u5904\u4E8E\u6682\u505C
@@ -15632,7 +16437,7 @@ Loop \u5DF2\u6682\u505C \u2014 \u540E\u7EED\u6392\u7A0B\u5468\u671F\u5C06\u8DF3\
15632
16437
  async function loopResumeCommand(_args, deps = realDeps3()) {
15633
16438
  const id = await deps.identity();
15634
16439
  const marker = pauseMarkerPath(id.path, id.slug);
15635
- const existed = existsSync37(marker);
16440
+ const existed = existsSync38(marker);
15636
16441
  rmSync9(marker, { force: true });
15637
16442
  process.stdout.write(existed ? `Loop resumed \u2014 scheduling active again
15638
16443
  Loop \u5DF2\u6062\u590D \u2014 \u6392\u7A0B\u91CD\u65B0\u751F\u6548
@@ -15650,16 +16455,16 @@ function isLegacyRunner(text) {
15650
16455
  }
15651
16456
  async function loopNowCommand(_args, deps = realDeps3()) {
15652
16457
  const id = await deps.identity();
15653
- const runner = join34(deps.sharedRoot(), "loop", `run-${id.slug}.sh`);
16458
+ const runner = join35(deps.sharedRoot(), "loop", `run-${id.slug}.sh`);
15654
16459
  let legacy = false;
15655
- if (existsSync37(runner)) {
16460
+ if (existsSync38(runner)) {
15656
16461
  try {
15657
- legacy = isLegacyRunner(readFileSync29(runner, "utf8"));
16462
+ legacy = isLegacyRunner(readFileSync30(runner, "utf8"));
15658
16463
  } catch {
15659
16464
  legacy = true;
15660
16465
  }
15661
16466
  }
15662
- if (!existsSync37(runner) || legacy) {
16467
+ if (!existsSync38(runner) || legacy) {
15663
16468
  process.stdout.write(legacy ? `Legacy v2 runner detected \u2014 regenerating templates (FIX-197)
15664
16469
  \u68C0\u6D4B\u5230 v2 \u65E7\u7248 runner \u2014 \u6B63\u5728\u518D\u751F\u6210\u6A21\u677F\uFF08FIX-197\uFF09
15665
16470
  ` : `No runner yet \u2014 generating templates
@@ -15687,7 +16492,7 @@ live transcript below \u2014 Ctrl-C stops watching, never the cycle
15687
16492
  }
15688
16493
  const rc = await exec(runner);
15689
16494
  if (rc === 0 && useTmux && deps.observe !== void 0) {
15690
- await deps.observe(join34(id.path, ".roll", "loop"));
16495
+ await deps.observe(join35(id.path, ".roll", "loop"));
15691
16496
  process.stdout.write(`
15692
16497
  cycle finished \u2014 logs: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
15693
16498
  \u5468\u671F\u7ED3\u675F \u2014 \u65E5\u5FD7: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
@@ -15698,8 +16503,8 @@ cycle finished \u2014 logs: .roll/loop/cron.log \xB7 .roll/loop/cycle-logs/
15698
16503
 
15699
16504
  // packages/cli/dist/commands/migrate.js
15700
16505
  import { spawnSync as spawnSync7 } from "node:child_process";
15701
- import { existsSync as existsSync38, mkdirSync as mkdirSync20 } from "node:fs";
15702
- import { dirname as dirname18 } from "node:path";
16506
+ import { existsSync as existsSync39, mkdirSync as mkdirSync21 } from "node:fs";
16507
+ import { dirname as dirname17 } from "node:path";
15703
16508
  function colors() {
15704
16509
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
15705
16510
  return noColor2 ? { RED: "", GREEN: "", YELLOW: "", CYAN: "", NC: "" } : {
@@ -15818,9 +16623,9 @@ function migrateExecute(activeMoves) {
15818
16623
  for (const move of activeMoves) {
15819
16624
  const src = srcOf(move);
15820
16625
  const tgt = tgtOf(move);
15821
- const targetDir = dirname18(tgt);
15822
- if (!existsSync38(targetDir))
15823
- mkdirSync20(targetDir, { recursive: true });
16626
+ const targetDir = dirname17(tgt);
16627
+ if (!existsSync39(targetDir))
16628
+ mkdirSync21(targetDir, { recursive: true });
15824
16629
  const r = git3(["mv", src, tgt]);
15825
16630
  if (r.status !== 0) {
15826
16631
  err11(`git mv failed: ${src} \u2192 ${tgt}`);
@@ -15829,7 +16634,7 @@ function migrateExecute(activeMoves) {
15829
16634
  }
15830
16635
  moved += 1;
15831
16636
  }
15832
- if (existsSync38("docs")) {
16637
+ if (existsSync39("docs")) {
15833
16638
  spawnSync7("find", ["docs", "-type", "d", "-empty", "-delete"], { stdio: "ignore" });
15834
16639
  }
15835
16640
  git3([
@@ -15870,10 +16675,10 @@ function migrateCommand(args) {
15870
16675
  const moves = buildMoves();
15871
16676
  let hasNew = false;
15872
16677
  let hasOld = false;
15873
- if (existsSync38(".roll"))
16678
+ if (existsSync39(".roll"))
15874
16679
  hasNew = true;
15875
16680
  for (const move of moves) {
15876
- if (existsSync38(srcOf(move))) {
16681
+ if (existsSync39(srcOf(move))) {
15877
16682
  hasOld = true;
15878
16683
  break;
15879
16684
  }
@@ -15885,7 +16690,7 @@ function migrateCommand(args) {
15885
16690
  for (const move of moves) {
15886
16691
  const src = srcOf(move);
15887
16692
  const tgt = tgtOf(move);
15888
- if (existsSync38(src) && existsSync38(tgt)) {
16693
+ if (existsSync39(src) && existsSync39(tgt)) {
15889
16694
  process.stderr.write(` - ${src} AND ${tgt} both exist
15890
16695
  `);
15891
16696
  }
@@ -15902,7 +16707,7 @@ function migrateCommand(args) {
15902
16707
  info3(m2("migrate.no_old_structure_detected_nothing_to"));
15903
16708
  return 0;
15904
16709
  }
15905
- const activeMoves = moves.filter((move) => existsSync38(srcOf(move)));
16710
+ const activeMoves = moves.filter((move) => existsSync39(srcOf(move)));
15906
16711
  if (activeMoves.length === 0) {
15907
16712
  warn4(m2("migrate.old_structure_markers_found_but_no"));
15908
16713
  return 0;
@@ -15919,11 +16724,11 @@ function migrateCommand(args) {
15919
16724
  }
15920
16725
 
15921
16726
  // packages/cli/dist/commands/migrate-features.js
15922
- import { existsSync as existsSync39, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readlinkSync as readlinkSync3, statSync as statSync17, writeFileSync as writeFileSync21 } from "node:fs";
15923
- 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";
15924
16729
  function specFrontmatter(specFile, storyId) {
15925
16730
  try {
15926
- const lines = readFileSync30(specFile, "utf8").split("\n").slice(0, 12);
16731
+ const lines = readFileSync31(specFile, "utf8").split("\n").slice(0, 12);
15927
16732
  const pick = (key) => {
15928
16733
  const row2 = lines.find((l) => l.startsWith(`${key}: `));
15929
16734
  return row2?.slice(key.length + 2).trim();
@@ -15943,24 +16748,24 @@ function specFrontmatter(specFile, storyId) {
15943
16748
  function migrateFeaturesCommand(args) {
15944
16749
  const refresh = args.includes("--refresh-html");
15945
16750
  const cwd = process.cwd();
15946
- if (!existsSync39(join35(cwd, ".roll", "index.json"))) {
16751
+ if (!existsSync40(join36(cwd, ".roll", "index.json"))) {
15947
16752
  process.stderr.write("migrate-features: .roll/index.json not found\n");
15948
16753
  return 1;
15949
16754
  }
15950
16755
  const stories = readIndex(cwd);
15951
- const root = join35(cwd, ".roll", "features");
16756
+ const root = join36(cwd, ".roll", "features");
15952
16757
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15953
16758
  let specCreated = 0;
15954
16759
  let htmlWritten = 0;
15955
16760
  let skipped = 0;
15956
16761
  for (const [storyId, epic] of Object.entries(stories)) {
15957
- const storyDir = join35(root, epic, storyId);
15958
- const specFile = join35(storyDir, "spec.md");
15959
- if (!existsSync39(storyDir)) {
15960
- const mdFile = join35(root, epic, `${storyId}.md`);
15961
- 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)) {
15962
16767
  try {
15963
- mkdirSync21(storyDir, { recursive: true });
16768
+ mkdirSync22(storyDir, { recursive: true });
15964
16769
  } catch {
15965
16770
  skipped++;
15966
16771
  continue;
@@ -15970,8 +16775,8 @@ function migrateFeaturesCommand(args) {
15970
16775
  continue;
15971
16776
  }
15972
16777
  }
15973
- if (!existsSync39(specFile)) {
15974
- writeFileSync21(specFile, renderSpecMd({
16778
+ if (!existsSync40(specFile)) {
16779
+ writeFileSync22(specFile, renderSpecMd({
15975
16780
  id: storyId,
15976
16781
  epic,
15977
16782
  created: today,
@@ -15979,25 +16784,25 @@ function migrateFeaturesCommand(args) {
15979
16784
  }), "utf8");
15980
16785
  specCreated++;
15981
16786
  }
15982
- const htmlFile = join35(storyDir, "index.html");
15983
- if (!existsSync39(htmlFile) || refresh) {
16787
+ const htmlFile = join36(storyDir, "index.html");
16788
+ if (!existsSync40(htmlFile) || refresh) {
15984
16789
  const fm = specFrontmatter(specFile, storyId);
15985
16790
  const card = { id: storyId, epic, created: fm.created ?? today, ...fm.title !== void 0 ? { title: fm.title } : {} };
15986
16791
  let html = renderStoryPage(card);
15987
- const latest = join35(storyDir, "latest");
15988
- const report = join35(latest, reportFileName(storyId));
15989
- if (existsSync39(report)) {
16792
+ const latest = join36(storyDir, "latest");
16793
+ const report = join36(latest, reportFileName(storyId));
16794
+ if (existsSync40(report)) {
15990
16795
  let runRel = "latest";
15991
16796
  try {
15992
16797
  runRel = readlinkSync3(latest);
15993
16798
  } catch {
15994
16799
  }
15995
- const deliveredOn = statSync17(report).mtime.toISOString().slice(0, 10);
16800
+ const deliveredOn = statSync18(report).mtime.toISOString().slice(0, 10);
15996
16801
  html = markPhaseDone(html, "delivery", `<p><a href="${runRel}/${reportFileName(storyId)}">${bi("Attestation report", "\u9A8C\u6536\u62A5\u544A")}</a></p>
15997
16802
  <p class="muted">${bi("Delivered", "\u4EA4\u4ED8\u4E8E")} ${deliveredOn}</p>
15998
16803
  `);
15999
16804
  }
16000
- writeFileSync21(htmlFile, html, "utf8");
16805
+ writeFileSync22(htmlFile, html, "utf8");
16001
16806
  htmlWritten++;
16002
16807
  }
16003
16808
  }
@@ -16008,9 +16813,9 @@ function migrateFeaturesCommand(args) {
16008
16813
 
16009
16814
  // packages/cli/dist/commands/offboard.js
16010
16815
  import { spawnSync as spawnSync8 } from "node:child_process";
16011
- import { existsSync as existsSync40, readFileSync as readFileSync31, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync22 } from "node:fs";
16012
- import { homedir as homedir15 } from "node:os";
16013
- import { join as join36, resolve as resolve2 } from "node:path";
16816
+ import { existsSync as existsSync41, readFileSync as readFileSync32, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync23 } from "node:fs";
16817
+ import { homedir as homedir16 } from "node:os";
16818
+ import { join as join37, resolve as resolve2 } from "node:path";
16014
16819
  function pal3() {
16015
16820
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
16016
16821
  return noColor2 ? { RED: "", GREEN: "", YELLOW: "", CYAN: "", BOLD: "", NC: "" } : {
@@ -16048,7 +16853,7 @@ function m3(key, ...args) {
16048
16853
  return t(v2Catalog, msgLang9(), key, ...args);
16049
16854
  }
16050
16855
  function changesetPath(projectDir) {
16051
- return join36(projectDir, ".roll", "onboard-changeset.yaml");
16856
+ return join37(projectDir, ".roll", "onboard-changeset.yaml");
16052
16857
  }
16053
16858
  function loopInCycle() {
16054
16859
  return (process.env["ROLL_LOOP_AGENT"] ?? "") !== "" || (process.env["ROLL_CYCLE_LOG_RAW"] ?? "") !== "";
@@ -16058,7 +16863,7 @@ function runLaunchctl(args) {
16058
16863
  if (args[0] !== void 0 && readOnly.has(args[0])) {
16059
16864
  return spawnSync8("launchctl", args, { stdio: "inherit" }).status ?? 1;
16060
16865
  }
16061
- const canonical = join36(homedir15(), "Library", "LaunchAgents");
16866
+ const canonical = join37(homedir16(), "Library", "LaunchAgents");
16062
16867
  const launchdDir = process.env["_LAUNCHD_DIR"] ?? canonical;
16063
16868
  if (launchdDir !== canonical)
16064
16869
  return 0;
@@ -16122,7 +16927,7 @@ function offboardCommand(args) {
16122
16927
  projectDir = process.cwd();
16123
16928
  }
16124
16929
  const changeset = changesetPath(projectDir);
16125
- if (!existsSync40(changeset)) {
16930
+ if (!existsSync41(changeset)) {
16126
16931
  err12(m3("offboard.no_changeset_en"));
16127
16932
  err12(m3("offboard.no_changeset_zh"));
16128
16933
  process.stderr.write("\n");
@@ -16136,7 +16941,7 @@ function offboardCommand(args) {
16136
16941
  `);
16137
16942
  return 1;
16138
16943
  }
16139
- const parsed = parseChangeset(readFileSync31(changeset, "utf8"));
16944
+ const parsed = parseChangeset(readFileSync32(changeset, "utf8"));
16140
16945
  if (!parsed.ok) {
16141
16946
  err12(m3("offboard.failed_to_parse_changeset"));
16142
16947
  return 1;
@@ -16212,8 +17017,8 @@ function offboardCommand(args) {
16212
17017
  process.stdout.write(m3("offboard.applying_offboard") + "\n");
16213
17018
  for (const item of files) {
16214
17019
  try {
16215
- rmSync10(join36(projectDir, item), { force: true });
16216
- if (!existsSync40(join36(projectDir, item)))
17020
+ rmSync10(join37(projectDir, item), { force: true });
17021
+ if (!existsSync41(join37(projectDir, item)))
16217
17022
  process.stdout.write(` removed file ${item}
16218
17023
  `);
16219
17024
  } catch {
@@ -16221,26 +17026,26 @@ function offboardCommand(args) {
16221
17026
  }
16222
17027
  for (const item of dirs) {
16223
17028
  try {
16224
- rmSync10(join36(projectDir, item), { recursive: true, force: true });
17029
+ rmSync10(join37(projectDir, item), { recursive: true, force: true });
16225
17030
  process.stdout.write(` removed dir ${item}
16226
17031
  `);
16227
17032
  } catch {
16228
17033
  }
16229
17034
  }
16230
17035
  for (const item of giEntries) {
16231
- const gi = join36(projectDir, ".gitignore");
16232
- if (existsSync40(gi)) {
16233
- 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");
16234
17039
  if (lines.includes(item)) {
16235
17040
  const kept = lines.filter((l) => l !== item);
16236
- writeFileSync22(gi, kept.join("\n"));
17041
+ writeFileSync23(gi, kept.join("\n"));
16237
17042
  process.stdout.write(` .gitignore - ${item}
16238
17043
  `);
16239
17044
  }
16240
17045
  }
16241
17046
  }
16242
17047
  for (const item of plists) {
16243
- const plistPath = join36(homedir15(), "Library", "LaunchAgents", item);
17048
+ const plistPath = join37(homedir16(), "Library", "LaunchAgents", item);
16244
17049
  const r = runLaunchctl(["unload", "-w", plistPath]);
16245
17050
  if (r === 0)
16246
17051
  process.stdout.write(` unloaded ${item}
@@ -16253,16 +17058,16 @@ function offboardCommand(args) {
16253
17058
  }
16254
17059
 
16255
17060
  // packages/cli/dist/commands/prices.js
16256
- import { existsSync as existsSync41, readdirSync as readdirSync16, readFileSync as readFileSync32 } from "node:fs";
16257
- 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";
16258
17063
  function loadSnapshots() {
16259
- const dir = join37(repoRoot(), "lib", "prices");
16260
- if (!existsSync41(dir)) {
17064
+ const dir = join38(repoRoot(), "lib", "prices");
17065
+ if (!existsSync42(dir)) {
16261
17066
  throw new Error(`no price snapshots found in ${dir}; run \`roll prices refresh\``);
16262
17067
  }
16263
17068
  const files = readdirSync16(dir).filter((n) => n.startsWith("snapshot-") && n.endsWith(".json")).sort();
16264
17069
  return files.map((name) => {
16265
- const data = JSON.parse(readFileSync32(join37(dir, name), "utf8"));
17070
+ const data = JSON.parse(readFileSync33(join38(dir, name), "utf8"));
16266
17071
  for (const key of ["version", "effective_at", "source_url", "prices"]) {
16267
17072
  if (data[key] === void 0)
16268
17073
  throw new Error(`snapshot ${name} missing required key ${key}`);
@@ -16355,8 +17160,8 @@ function pricesCommand(args) {
16355
17160
  }
16356
17161
 
16357
17162
  // packages/cli/dist/commands/release.js
16358
- import { existsSync as existsSync42, readFileSync as readFileSync33 } from "node:fs";
16359
- 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";
16360
17165
  function label3(lang4, key, ...args) {
16361
17166
  if (v3Catalog[key] !== void 0)
16362
17167
  return t(v3Catalog, lang4, key, ...args);
@@ -16367,25 +17172,34 @@ function dateOf(d) {
16367
17172
  }
16368
17173
  function currentVersion(cwd) {
16369
17174
  try {
16370
- const pkg = JSON.parse(readFileSync33(join38(cwd, "package.json"), "utf8"));
17175
+ const pkg = JSON.parse(readFileSync34(join39(cwd, "package.json"), "utf8"));
16371
17176
  return typeof pkg.version === "string" ? pkg.version : "";
16372
17177
  } catch {
16373
17178
  return "";
16374
17179
  }
16375
17180
  }
16376
- function changelogReady(cwd) {
16377
- const path = join38(cwd, "CHANGELOG.md");
16378
- if (!existsSync42(path))
17181
+ function changelogReady(cwd, current) {
17182
+ const path = join39(cwd, "CHANGELOG.md");
17183
+ if (!existsSync43(path))
16379
17184
  return false;
16380
- const text = readFileSync33(path, "utf8");
16381
- const idx = text.indexOf("## Unreleased");
16382
- if (idx === -1)
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)
17199
+ return false;
17200
+ if (m7[1] === current)
16383
17201
  return false;
16384
- let section = text.slice(idx + "## Unreleased".length);
16385
- const nextHeading = section.search(/\n## /);
16386
- if (nextHeading !== -1)
16387
- section = section.slice(0, nextHeading);
16388
- return /^\s*-\s+\S/m.test(section);
17202
+ return hasBullet(sectionAfter(m7.index, m7[0].length));
16389
17203
  }
16390
17204
  function releaseCommand(args, now) {
16391
17205
  const noColor2 = args.includes("--no-color") || !process.stdout.isTTY || (process.env["NO_COLOR"] ?? "") !== "";
@@ -16407,7 +17221,7 @@ function releaseCommand(args, now) {
16407
17221
  `);
16408
17222
  return 1;
16409
17223
  }
16410
- const ready = changelogReady(cwd);
17224
+ const ready = changelogReady(cwd, cur);
16411
17225
  const plan = planRelease({
16412
17226
  currentVersion: cur,
16413
17227
  date: now ?? dateOf(/* @__PURE__ */ new Date()),
@@ -16444,8 +17258,8 @@ function releaseCommand(args, now) {
16444
17258
 
16445
17259
  // packages/cli/dist/commands/setup.js
16446
17260
  import { spawnSync as spawnSync9 } from "node:child_process";
16447
- 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";
16448
- 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";
16449
17263
  function err13(line) {
16450
17264
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
16451
17265
  const RED = noColor2 ? "" : "\x1B[0;31m";
@@ -16470,7 +17284,7 @@ function setupSnapshot(watch) {
16470
17284
  continue;
16471
17285
  let isDir4 = false;
16472
17286
  try {
16473
- isDir4 = statSync18(d).isDirectory();
17287
+ isDir4 = statSync19(d).isDirectory();
16474
17288
  } catch {
16475
17289
  isDir4 = false;
16476
17290
  }
@@ -16488,7 +17302,7 @@ function walk(dir, lines) {
16488
17302
  return;
16489
17303
  }
16490
17304
  for (const name of names) {
16491
- const p = join39(dir, name);
17305
+ const p = join40(dir, name);
16492
17306
  let isLink = false;
16493
17307
  try {
16494
17308
  isLink = lstatSync3(p).isSymbolicLink();
@@ -16504,7 +17318,7 @@ function walk(dir, lines) {
16504
17318
  }
16505
17319
  let st;
16506
17320
  try {
16507
- st = statSync18(p);
17321
+ st = statSync19(p);
16508
17322
  } catch {
16509
17323
  continue;
16510
17324
  }
@@ -16518,7 +17332,7 @@ function walk(dir, lines) {
16518
17332
  }
16519
17333
  function fileFingerprint(p) {
16520
17334
  try {
16521
- const buf = readFileSync34(p);
17335
+ const buf = readFileSync35(p);
16522
17336
  let sum = 0;
16523
17337
  for (let i = 0; i < buf.length; i++)
16524
17338
  sum = sum * 31 + (buf[i] ?? 0) >>> 0;
@@ -16548,18 +17362,18 @@ function ensureHooksPath(repoPath) {
16548
17362
  }
16549
17363
  }
16550
17364
  function peerEnsureStateDir() {
16551
- const base = join39(rollHome(), ".peer-state");
16552
- mkdirSync22(base, { recursive: true });
16553
- 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 });
16554
17368
  }
16555
17369
  function tmuxPresent() {
16556
17370
  return onPath("tmux");
16557
17371
  }
16558
17372
  function submoduleGuard() {
16559
17373
  const pkg = rollPkgDir();
16560
- const skills = join39(pkg, "skills");
16561
- const hasGit = existsSync43(join39(pkg, ".git"));
16562
- 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"));
16563
17377
  let empty = true;
16564
17378
  try {
16565
17379
  empty = readdirSync17(skills).length === 0;
@@ -16652,7 +17466,7 @@ function setupCommand(args) {
16652
17466
  return 1;
16653
17467
  }
16654
17468
  }
16655
- if (!existsSync43(rollPkgConventions()))
17469
+ if (!existsSync44(rollPkgConventions()))
16656
17470
  return null;
16657
17471
  submoduleGuard();
16658
17472
  const home = rollHome();
@@ -16671,7 +17485,7 @@ function setupCommand(args) {
16671
17485
  ".qwen"
16672
17486
  ];
16673
17487
  const homeDir = process.env["HOME"] ?? "";
16674
- const aiDirs = aiDirsList.map((d) => join39(homeDir, d)).join(":");
17488
+ const aiDirs = aiDirsList.map((d) => join40(homeDir, d)).join(":");
16675
17489
  const steps = [];
16676
17490
  const s1 = runSetupStep(home, () => {
16677
17491
  installLocal(force);
@@ -16685,7 +17499,7 @@ function setupCommand(args) {
16685
17499
  syncSkills(force);
16686
17500
  });
16687
17501
  steps.push({ num: 3, label: "Install skills to ~/.claude", status: stateToMarker(s3, force) });
16688
- const s4 = runSetupStep(join39(home, ".peer-state"), () => {
17502
+ const s4 = runSetupStep(join40(home, ".peer-state"), () => {
16689
17503
  peerEnsureStateDir();
16690
17504
  });
16691
17505
  steps.push({ num: 4, label: "Initialize peer-review state directory", status: stateToMarker(s4, force) });
@@ -16706,12 +17520,12 @@ function setupCommand(args) {
16706
17520
 
16707
17521
  // packages/cli/dist/commands/slides/index.js
16708
17522
  import { spawnSync as spawnSync10 } from "node:child_process";
16709
- import { mkdirSync as mkdirSync23, readFileSync as readFileSync37, readdirSync as readdirSync19, rmSync as rmSync11, statSync as statSync21, writeFileSync as writeFileSync23 } from "node:fs";
16710
- 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";
16711
17525
 
16712
17526
  // packages/cli/dist/commands/slides/render.js
16713
- import { readFileSync as readFileSync35, existsSync as existsSync44, readdirSync as readdirSync18, statSync as statSync19 } from "node:fs";
16714
- import { join as join40, dirname as dirname19, 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";
16715
17529
  function coerceScalar(v) {
16716
17530
  if (v.length >= 2 && v[0] === v[v.length - 1] && (v[0] === "'" || v[0] === '"')) {
16717
17531
  return v.slice(1, -1);
@@ -17027,7 +17841,7 @@ function pyTruthy(v) {
17027
17841
  return v.length > 0;
17028
17842
  return Object.keys(v).length > 0;
17029
17843
  }
17030
- function lookup(ctxStack, key) {
17844
+ function lookup2(ctxStack, key) {
17031
17845
  for (let i = ctxStack.length - 1; i >= 0; i--) {
17032
17846
  const ctx = ctxStack[i];
17033
17847
  if (isPlainObject(ctx) && Object.prototype.hasOwnProperty.call(ctx, key)) {
@@ -17086,19 +17900,19 @@ function mustache(template, context) {
17086
17900
  const tagKey = m7[3];
17087
17901
  const matchEnd = m7.index + m7[0].length;
17088
17902
  if (rawKey) {
17089
- buf.push(pyStr(lookup(ctxStack, rawKey)));
17903
+ buf.push(pyStr(lookup2(ctxStack, rawKey)));
17090
17904
  i = matchEnd;
17091
17905
  continue;
17092
17906
  }
17093
17907
  if (sigil === "") {
17094
- buf.push(escapeHtml2(pyStr(lookup(ctxStack, tagKey))));
17908
+ buf.push(escapeHtml2(pyStr(lookup2(ctxStack, tagKey))));
17095
17909
  i = matchEnd;
17096
17910
  continue;
17097
17911
  }
17098
17912
  if (sigil === "#" || sigil === "^") {
17099
17913
  const closeIdx = findClose(chunk, matchEnd, tagKey);
17100
17914
  const inner = chunk.slice(matchEnd, closeIdx);
17101
- const val = lookup(ctxStack, tagKey);
17915
+ const val = lookup2(ctxStack, tagKey);
17102
17916
  if (sigil === "#") {
17103
17917
  if (Array.isArray(val)) {
17104
17918
  for (const item of val) {
@@ -17196,7 +18010,7 @@ var DEFAULT_LAYOUT = "plain";
17196
18010
  var LayoutResolver = class {
17197
18011
  componentsDir;
17198
18012
  constructor(componentsDir) {
17199
- this.componentsDir = componentsDir ?? join40(libDirFor(import.meta.url), "slides", "components");
18013
+ this.componentsDir = componentsDir ?? join41(libDirFor(import.meta.url), "slides", "components");
17200
18014
  }
17201
18015
  available() {
17202
18016
  if (!isDir2(this.componentsDir))
@@ -17210,7 +18024,7 @@ var LayoutResolver = class {
17210
18024
  if (!/^[a-z0-9-]+$/.test(layout)) {
17211
18025
  throw new ValueError(`Unknown layout: ${layout}; available: ${this.available().join(", ")}`);
17212
18026
  }
17213
- const path = join40(this.componentsDir, `${layout}.html`);
18027
+ const path = join41(this.componentsDir, `${layout}.html`);
17214
18028
  if (!isFile(path)) {
17215
18029
  throw new ValueError(`Unknown layout: ${layout}; available: ${this.available().join(", ")}`);
17216
18030
  }
@@ -17220,7 +18034,7 @@ var LayoutResolver = class {
17220
18034
  function renderSlideInner(slide, resolver) {
17221
18035
  const layout = typeof slide["layout"] === "string" && slide["layout"] || DEFAULT_LAYOUT;
17222
18036
  const partialPath = resolver.resolve(String(layout));
17223
- let partial = readFileSync35(partialPath, "utf8");
18037
+ let partial = readFileSync36(partialPath, "utf8");
17224
18038
  partial = partial.replace(/^\s*<!--[\s\S]*?-->(?:\s*\n)?/, "");
17225
18039
  const rendered = mustache(partial, slide);
17226
18040
  return stripNewlines(rendered);
@@ -17268,30 +18082,30 @@ function pyRepr(s) {
17268
18082
  }
17269
18083
  function isDir2(p) {
17270
18084
  try {
17271
- return statSync19(p).isDirectory();
18085
+ return statSync20(p).isDirectory();
17272
18086
  } catch {
17273
18087
  return false;
17274
18088
  }
17275
18089
  }
17276
18090
  function isFile(p) {
17277
18091
  try {
17278
- return statSync19(p).isFile();
18092
+ return statSync20(p).isFile();
17279
18093
  } catch {
17280
18094
  return false;
17281
18095
  }
17282
18096
  }
17283
18097
  function libDirFor(_metaUrl) {
17284
18098
  void _metaUrl;
17285
- void existsSync44;
17286
- void dirname19;
17287
- return join40(repoLibFallback(), "lib");
18099
+ void existsSync45;
18100
+ void dirname18;
18101
+ return join41(repoLibFallback(), "lib");
17288
18102
  }
17289
18103
  function repoLibFallback() {
17290
18104
  let dir = process.cwd();
17291
18105
  for (let i = 0; i < 12; i++) {
17292
- if (existsSync44(join40(dir, "bin", "roll")))
18106
+ if (existsSync45(join41(dir, "bin", "roll")))
17293
18107
  return dir;
17294
- const parent = dirname19(dir);
18108
+ const parent = dirname18(dir);
17295
18109
  if (parent === dir)
17296
18110
  break;
17297
18111
  dir = parent;
@@ -17300,7 +18114,7 @@ function repoLibFallback() {
17300
18114
  }
17301
18115
 
17302
18116
  // packages/cli/dist/commands/slides/validate.js
17303
- import { readFileSync as readFileSync36, statSync as statSync20 } from "node:fs";
18117
+ import { readFileSync as readFileSync37, statSync as statSync21 } from "node:fs";
17304
18118
  var REQUIRED_FRONTMATTER = [
17305
18119
  "template",
17306
18120
  "slug",
@@ -17504,7 +18318,7 @@ function validateDeckFile(path, errSink, componentsDir) {
17504
18318
  let fm;
17505
18319
  let slides;
17506
18320
  try {
17507
- src = readFileSync36(path, "utf8");
18321
+ src = readFileSync37(path, "utf8");
17508
18322
  [fm] = parseFrontmatter(src);
17509
18323
  const [, body] = parseFrontmatter(src);
17510
18324
  slides = parseSlides(body);
@@ -17538,7 +18352,7 @@ function validateDeckFile(path, errSink, componentsDir) {
17538
18352
  }
17539
18353
  function isFile2(p) {
17540
18354
  try {
17541
- return statSync20(p).isFile();
18355
+ return statSync21(p).isFile();
17542
18356
  } catch {
17543
18357
  return false;
17544
18358
  }
@@ -17617,13 +18431,13 @@ function slidesHelp(out2) {
17617
18431
  out2(SLIDES_HELP);
17618
18432
  }
17619
18433
  function slidesLib() {
17620
- return join41(rollPkgDir(), "lib");
18434
+ return join42(rollPkgDir(), "lib");
17621
18435
  }
17622
18436
  function slidesTemplatePath(name) {
17623
- const projTpl = join41(".roll", "slides", "templates", `${name}.html`);
18437
+ const projTpl = join42(".roll", "slides", "templates", `${name}.html`);
17624
18438
  if (isFile3(projTpl))
17625
18439
  return projTpl;
17626
- const tpl = join41(rollPkgDir(), "lib", "slides", "templates", `${name}.html`);
18440
+ const tpl = join42(rollPkgDir(), "lib", "slides", "templates", `${name}.html`);
17627
18441
  if (isFile3(tpl))
17628
18442
  return tpl;
17629
18443
  return null;
@@ -17631,7 +18445,7 @@ function slidesTemplatePath(name) {
17631
18445
  function slidesTemplateForDeck(deckPath) {
17632
18446
  let tpl = "";
17633
18447
  try {
17634
- const src = readFileSync37(deckPath, "utf8");
18448
+ const src = readFileSync38(deckPath, "utf8");
17635
18449
  let d = 0;
17636
18450
  for (const line of src.split("\n")) {
17637
18451
  if (/^---[ \t]*$/.test(line)) {
@@ -17657,21 +18471,21 @@ function slidesTemplateForDeck(deckPath) {
17657
18471
  return tpl;
17658
18472
  }
17659
18473
  function slidesEnsureGitignore() {
17660
- const gi = join41(".roll", ".gitignore");
17661
- mkdirSync23(".roll", { recursive: true });
18474
+ const gi = join42(".roll", ".gitignore");
18475
+ mkdirSync24(".roll", { recursive: true });
17662
18476
  if (isFile3(gi)) {
17663
- const content = readFileSync37(gi, "utf8");
18477
+ const content = readFileSync38(gi, "utf8");
17664
18478
  if (content.split("\n").some((l) => /^slides\/\*\.html$/.test(l)))
17665
18479
  return;
17666
18480
  if (content.length > 0 && !content.endsWith("\n")) {
17667
- writeFileSync23(gi, content + "\n");
18481
+ writeFileSync24(gi, content + "\n");
17668
18482
  }
17669
18483
  }
17670
18484
  appendFile(gi, "slides/*.html\n");
17671
18485
  }
17672
18486
  function appendFile(path, data) {
17673
- const prev = isFile3(path) ? readFileSync37(path, "utf8") : "";
17674
- writeFileSync23(path, prev + data);
18487
+ const prev = isFile3(path) ? readFileSync38(path, "utf8") : "";
18488
+ writeFileSync24(path, prev + data);
17675
18489
  }
17676
18490
  function slidesOpenCmd() {
17677
18491
  const sys = spawnSync10("uname", ["-s"], { encoding: "utf8" });
@@ -17730,7 +18544,7 @@ function cmdBuild(args) {
17730
18544
  process.stderr.write(m5("slides_build.usage_roll_slides_build_slug_no") + "\n");
17731
18545
  return 1;
17732
18546
  }
17733
- const deck = join41(".roll", "slides", slug, "deck.md");
18547
+ const deck = join42(".roll", "slides", slug, "deck.md");
17734
18548
  if (!isFile3(deck)) {
17735
18549
  err15(`Deck not found: ${deck}`);
17736
18550
  process.stderr.write(m5("slides_build.en_deck", deck) + "\n");
@@ -17740,14 +18554,14 @@ function cmdBuild(args) {
17740
18554
  return 1;
17741
18555
  }
17742
18556
  const libDir = slidesLib();
17743
- const validator = join41(libDir, "slides-validate.py");
17744
- const renderer = join41(libDir, "slides-render.py");
18557
+ const validator = join42(libDir, "slides-validate.py");
18558
+ const renderer = join42(libDir, "slides-render.py");
17745
18559
  if (!isFile3(validator) || !isFile3(renderer)) {
17746
18560
  err15(m5("slides_build.slides_toolchain_missing_re_run_roll"));
17747
18561
  return 1;
17748
18562
  }
17749
- const errFile = join41(".roll", "slides", slug, ".last-build.err");
17750
- const componentsDir = join41(libDir, "slides", "components");
18563
+ const errFile = join42(".roll", "slides", slug, ".last-build.err");
18564
+ const componentsDir = join42(libDir, "slides", "components");
17751
18565
  const valLines = [];
17752
18566
  const valExit = validateDeckFile(deck, (l) => valLines.push(l), componentsDir);
17753
18567
  const valOut = valLines.join("\n");
@@ -17756,8 +18570,8 @@ function cmdBuild(args) {
17756
18570
  `);
17757
18571
  } else if (valExit !== 0) {
17758
18572
  const ts = utcTs();
17759
- mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
17760
- writeFileSync23(errFile, `[${ts}] stage=validate
18573
+ mkdirSync24(join42(".roll", "slides", slug), { recursive: true });
18574
+ writeFileSync24(errFile, `[${ts}] stage=validate
17761
18575
  ${valOut}
17762
18576
  `);
17763
18577
  if (valOut !== "")
@@ -17774,15 +18588,15 @@ ${valOut}
17774
18588
  const tplPath = slidesTemplatePath(tplName);
17775
18589
  if (tplPath === null) {
17776
18590
  const ts = utcTs();
17777
- mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
17778
- writeFileSync23(errFile, `[${ts}] stage=template
18591
+ mkdirSync24(join42(".roll", "slides", slug), { recursive: true });
18592
+ writeFileSync24(errFile, `[${ts}] stage=template
17779
18593
  template not found: ${tplName}
17780
18594
  `);
17781
18595
  const { RED, NC } = pal4();
17782
18596
  process.stderr.write(`${RED}[FAIL]${NC} ${m5("slides_build.template_not_found", tplName)}
17783
18597
  `);
17784
18598
  process.stderr.write(" " + m5("slides_build.available_templates") + "\n");
17785
- const builtinDir = join41(rollPkgDir(), "lib", "slides", "templates");
18599
+ const builtinDir = join42(rollPkgDir(), "lib", "slides", "templates");
17786
18600
  if (isDir3(builtinDir)) {
17787
18601
  for (const t2 of listHtml(builtinDir)) {
17788
18602
  const n = basenameNoHtml(t2);
@@ -17790,7 +18604,7 @@ template not found: ${tplName}
17790
18604
  `);
17791
18605
  }
17792
18606
  }
17793
- const projDir = join41(".roll", "slides", "templates");
18607
+ const projDir = join42(".roll", "slides", "templates");
17794
18608
  if (isDir3(projDir)) {
17795
18609
  for (const t2 of listHtml(projDir)) {
17796
18610
  const n = basenameNoHtml(t2);
@@ -17801,18 +18615,18 @@ template not found: ${tplName}
17801
18615
  process.stderr.write(" " + m5("slides_build.templates_list_hint") + "\n");
17802
18616
  return 1;
17803
18617
  }
17804
- const out2 = join41(".roll", "slides", `${slug}.html`);
17805
- mkdirSync23(join41(".roll", "slides"), { recursive: true });
18618
+ const out2 = join42(".roll", "slides", `${slug}.html`);
18619
+ mkdirSync24(join42(".roll", "slides"), { recursive: true });
17806
18620
  let htmlOut;
17807
18621
  try {
17808
- const src = readFileSync37(deck, "utf8");
17809
- const template = readFileSync37(tplPath, "utf8");
18622
+ const src = readFileSync38(deck, "utf8");
18623
+ const template = readFileSync38(tplPath, "utf8");
17810
18624
  htmlOut = renderDeck(src, template, { componentsDir });
17811
18625
  } catch (e) {
17812
18626
  const renderOut = renderErrorBlock(e);
17813
18627
  const ts = utcTs();
17814
- mkdirSync23(join41(".roll", "slides", slug), { recursive: true });
17815
- writeFileSync23(errFile, `[${ts}] stage=render
18628
+ mkdirSync24(join42(".roll", "slides", slug), { recursive: true });
18629
+ writeFileSync24(errFile, `[${ts}] stage=render
17816
18630
  ${renderOut}
17817
18631
  `);
17818
18632
  const { RED, NC } = pal4();
@@ -17826,7 +18640,7 @@ ${renderOut}
17826
18640
  }
17827
18641
  return 1;
17828
18642
  }
17829
- writeFileSync23(out2, htmlOut);
18643
+ writeFileSync24(out2, htmlOut);
17830
18644
  try {
17831
18645
  rmSync11(errFile, { force: true });
17832
18646
  } catch {
@@ -17851,7 +18665,7 @@ function tailLines(s, n) {
17851
18665
  }
17852
18666
  function frontmatterField2(deckPath, field) {
17853
18667
  try {
17854
- const src = readFileSync37(deckPath, "utf8");
18668
+ const src = readFileSync38(deckPath, "utf8");
17855
18669
  let d = 0;
17856
18670
  const pat = new RegExp("^" + field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[ ]*:[ ]*");
17857
18671
  for (const line of src.split("\n")) {
@@ -17894,7 +18708,7 @@ function cmdList(args) {
17894
18708
  err15(m5("slides_list.unexpected_argument_1"));
17895
18709
  return 1;
17896
18710
  }
17897
- const slidesDir = join41(".roll", "slides");
18711
+ const slidesDir = join42(".roll", "slides");
17898
18712
  if (!isDir3(slidesDir)) {
17899
18713
  info5(m5("slides_list.no_decks_found_under_roll_slides"));
17900
18714
  process.stdout.write(` Hint: run 'roll slides new "<topic>"' to create one.
@@ -17904,8 +18718,8 @@ function cmdList(args) {
17904
18718
  }
17905
18719
  const slugs = [];
17906
18720
  for (const entry of readdirSync19(slidesDir)) {
17907
- const d = join41(slidesDir, entry);
17908
- if (isDir3(d) && isFile3(join41(d, "deck.md")))
18721
+ const d = join42(slidesDir, entry);
18722
+ if (isDir3(d) && isFile3(join42(d, "deck.md")))
17909
18723
  slugs.push(entry);
17910
18724
  }
17911
18725
  if (slugs.length === 0) {
@@ -17919,9 +18733,9 @@ function cmdList(args) {
17919
18733
  process.stdout.write(rowSix("slug", "template", "total_slides", "created", "built", "size") + "\n");
17920
18734
  process.stdout.write(rowSix("----", "--------", "------------", "-------", "------", "----") + "\n");
17921
18735
  for (const s of sorted) {
17922
- const deck = join41(slidesDir, s, "deck.md");
17923
- const html = join41(slidesDir, `${s}.html`);
17924
- 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");
17925
18739
  let template = frontmatterField2(deck, "template");
17926
18740
  if (template === "")
17927
18741
  template = "-";
@@ -17944,7 +18758,7 @@ function cmdList(args) {
17944
18758
  built = "\u2713 built";
17945
18759
  let bytes = 0;
17946
18760
  try {
17947
- bytes = statSync21(html).size;
18761
+ bytes = statSync22(html).size;
17948
18762
  } catch {
17949
18763
  bytes = 0;
17950
18764
  }
@@ -17960,7 +18774,7 @@ function cmdList(args) {
17960
18774
  }
17961
18775
  function isNewer(a, b) {
17962
18776
  try {
17963
- return statSync21(a).mtimeMs > statSync21(b).mtimeMs;
18777
+ return statSync22(a).mtimeMs > statSync22(b).mtimeMs;
17964
18778
  } catch {
17965
18779
  return false;
17966
18780
  }
@@ -17995,7 +18809,7 @@ function cmdPreview(args) {
17995
18809
  process.stderr.write(m5("slides_preview.usage_roll_slides_preview_slug_no") + "\n");
17996
18810
  return 1;
17997
18811
  }
17998
- const html = join41(".roll", "slides", `${slug}.html`);
18812
+ const html = join42(".roll", "slides", `${slug}.html`);
17999
18813
  if (!isFile3(html)) {
18000
18814
  err15(`Rendered HTML not found: ${html}`);
18001
18815
  process.stderr.write(m5("slides_preview.en_html", html) + "\n");
@@ -18036,9 +18850,9 @@ function cmdLogs(args) {
18036
18850
  process.stderr.write(m5("slides_logs.usage_roll_slides_logs_slug") + "\n");
18037
18851
  return 1;
18038
18852
  }
18039
- const deckDir = join41(".roll", "slides", slug);
18040
- const errFile = join41(deckDir, ".last-build.err");
18041
- 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"))) {
18042
18856
  err15(m5("slides_logs.deck_not_found", slug));
18043
18857
  return 1;
18044
18858
  }
@@ -18046,7 +18860,7 @@ function cmdLogs(args) {
18046
18860
  info5(m5("slides_logs.no_failure_records_for", slug));
18047
18861
  return 0;
18048
18862
  }
18049
- process.stdout.write(readFileSync37(errFile, "utf8"));
18863
+ process.stdout.write(readFileSync38(errFile, "utf8"));
18050
18864
  return 0;
18051
18865
  }
18052
18866
  function cmdDelete(args) {
@@ -18079,9 +18893,9 @@ function cmdDelete(args) {
18079
18893
  process.stderr.write(m5("slides_delete.usage_roll_slides_delete_slug_force") + "\n");
18080
18894
  return 1;
18081
18895
  }
18082
- const deckDir = join41(".roll", "slides", slug);
18083
- const html = join41(".roll", "slides", `${slug}.html`);
18084
- 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"))) {
18085
18899
  err15(m5("slides_delete.deck_not_found", slug));
18086
18900
  return 1;
18087
18901
  }
@@ -18120,7 +18934,7 @@ function cmdTemplates(args) {
18120
18934
  let found = false;
18121
18935
  process.stdout.write(rowThree("name", "source", "path") + "\n");
18122
18936
  process.stdout.write(rowThree("----", "------", "----") + "\n");
18123
- const builtinDir = join41(rollPkgDir(), "lib", "slides", "templates");
18937
+ const builtinDir = join42(rollPkgDir(), "lib", "slides", "templates");
18124
18938
  if (isDir3(builtinDir)) {
18125
18939
  for (const tpl of listHtml(builtinDir)) {
18126
18940
  const name = basenameNoHtml(tpl);
@@ -18128,11 +18942,11 @@ function cmdTemplates(args) {
18128
18942
  found = true;
18129
18943
  }
18130
18944
  }
18131
- const projDir = join41(".roll", "slides", "templates");
18945
+ const projDir = join42(".roll", "slides", "templates");
18132
18946
  if (isDir3(projDir)) {
18133
18947
  for (const tpl of listHtml(projDir)) {
18134
18948
  const name = basenameNoHtml(tpl);
18135
- const source = isFile3(join41(builtinDir, `${name}.html`)) ? "project (override)" : "project";
18949
+ const source = isFile3(join42(builtinDir, `${name}.html`)) ? "project (override)" : "project";
18136
18950
  process.stdout.write(rowThree(name, source, tpl) + "\n");
18137
18951
  found = true;
18138
18952
  }
@@ -18180,20 +18994,20 @@ function slidesCommand(args) {
18180
18994
  }
18181
18995
  function isFile3(p) {
18182
18996
  try {
18183
- return statSync21(p).isFile();
18997
+ return statSync22(p).isFile();
18184
18998
  } catch {
18185
18999
  return false;
18186
19000
  }
18187
19001
  }
18188
19002
  function isDir3(p) {
18189
19003
  try {
18190
- return statSync21(p).isDirectory();
19004
+ return statSync22(p).isDirectory();
18191
19005
  } catch {
18192
19006
  return false;
18193
19007
  }
18194
19008
  }
18195
19009
  function listHtml(dir) {
18196
- 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));
18197
19011
  }
18198
19012
  function basenameNoHtml(path) {
18199
19013
  const b = path.slice(path.lastIndexOf("/") + 1);
@@ -18211,21 +19025,21 @@ function rowThree(a, b, c2) {
18211
19025
  }
18212
19026
 
18213
19027
  // packages/cli/dist/commands/status.js
18214
- import { execFileSync as execFileSync8 } from "node:child_process";
19028
+ import { execFileSync as execFileSync9 } from "node:child_process";
18215
19029
  import { createHash as createHash5 } from "node:crypto";
18216
- import { existsSync as existsSync45, lstatSync as lstatSync4, readdirSync as readdirSync20, readFileSync as readFileSync38, realpathSync as realpathSync6, statSync as statSync22 } from "node:fs";
18217
- import { homedir as homedir16 } from "node:os";
18218
- import { basename as basename10, dirname as dirname20, join as join42 } from "node:path";
19030
+ import { existsSync as existsSync46, lstatSync as lstatSync4, readdirSync as readdirSync20, readFileSync as readFileSync39, realpathSync as realpathSync6, statSync as statSync23 } from "node:fs";
19031
+ import { homedir as homedir17 } from "node:os";
19032
+ import { basename as basename10, dirname as dirname19, join as join43 } from "node:path";
18219
19033
  function rollHome3() {
18220
- return process.env["ROLL_HOME"] ?? join42(homedir16(), ".roll");
19034
+ return process.env["ROLL_HOME"] ?? join43(homedir17(), ".roll");
18221
19035
  }
18222
- var globalDir = () => join42(rollHome3(), "conventions", "global");
18223
- var templatesDir = () => join42(rollHome3(), "conventions", "templates");
18224
- 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");
18225
19039
  function projectSlugPy() {
18226
19040
  let path = realpathSync6(process.cwd());
18227
19041
  try {
18228
- const common = execFileSync8("git", ["-C", path, "rev-parse", "--git-common-dir"], {
19042
+ const common = execFileSync9("git", ["-C", path, "rev-parse", "--git-common-dir"], {
18229
19043
  encoding: "utf8",
18230
19044
  stdio: ["ignore", "pipe", "ignore"]
18231
19045
  }).trim();
@@ -18240,18 +19054,18 @@ function projectSlugPy() {
18240
19054
  var CONVENTION_FILES = ["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursor-rules", "project_rules.md"];
18241
19055
  var TEMPLATES = ["fullstack", "frontend-only", "backend-service", "cli"];
18242
19056
  function globalConventions() {
18243
- return CONVENTION_FILES.map((f) => [f, existsSync45(join42(globalDir(), f))]);
19057
+ return CONVENTION_FILES.map((f) => [f, existsSync46(join43(globalDir(), f))]);
18244
19058
  }
18245
19059
  function parseAiEntries() {
18246
19060
  const cfg = configPath();
18247
- if (!existsSync45(cfg))
19061
+ if (!existsSync46(cfg))
18248
19062
  return [];
18249
19063
  const entries = [];
18250
- for (const line of readFileSync38(cfg, "utf8").split("\n")) {
19064
+ for (const line of readFileSync39(cfg, "utf8").split("\n")) {
18251
19065
  const m7 = /^ai_[a-z]+:\s*(.+)/.exec(line);
18252
19066
  if (m7 === null)
18253
19067
  continue;
18254
- const val = (m7[1] ?? "").trim().replaceAll("~", homedir16());
19068
+ const val = (m7[1] ?? "").trim().replaceAll("~", homedir17());
18255
19069
  const parts = val.split("|");
18256
19070
  if (parts.length < 3)
18257
19071
  continue;
@@ -18260,28 +19074,28 @@ function parseAiEntries() {
18260
19074
  const srcFile = (parts[2] ?? "").trim();
18261
19075
  let name = basename10(aiDir).replace(/^\.+/, "");
18262
19076
  if (name === "workspace" || name === "agent") {
18263
- name = basename10(dirname20(aiDir)).replace(/^\.+/, "");
19077
+ name = basename10(dirname19(aiDir)).replace(/^\.+/, "");
18264
19078
  }
18265
19079
  entries.push({ name, ai_dir: aiDir, cfg_file: cfgFile, src_file: srcFile });
18266
19080
  }
18267
19081
  return entries;
18268
19082
  }
18269
19083
  function aiSyncStatus(e) {
18270
- const cfgFile = join42(e.ai_dir, e.cfg_file);
18271
- const rollMd = join42(e.ai_dir, "roll.md");
18272
- const src = join42(globalDir(), e.src_file);
18273
- 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))
18274
19088
  return "missing";
18275
- if (!existsSync45(rollMd))
19089
+ if (!existsSync46(rollMd))
18276
19090
  return "out-of-sync";
18277
19091
  try {
18278
- if (existsSync45(src) && !readFileSync38(rollMd).equals(readFileSync38(src)))
19092
+ if (existsSync46(src) && !readFileSync39(rollMd).equals(readFileSync39(src)))
18279
19093
  return "out-of-sync";
18280
19094
  } catch {
18281
19095
  return "out-of-sync";
18282
19096
  }
18283
19097
  try {
18284
- if (!readFileSync38(cfgFile, "utf8").includes("@roll.md"))
19098
+ if (!readFileSync39(cfgFile, "utf8").includes("@roll.md"))
18285
19099
  return "out-of-sync";
18286
19100
  } catch {
18287
19101
  return "out-of-sync";
@@ -18289,15 +19103,15 @@ function aiSyncStatus(e) {
18289
19103
  return "sync";
18290
19104
  }
18291
19105
  function aiSkillCount(e) {
18292
- const skillsDir2 = join42(e.ai_dir, "skills");
18293
- if (!existsSync45(skillsDir2))
19106
+ const skillsDir2 = join43(e.ai_dir, "skills");
19107
+ if (!existsSync46(skillsDir2))
18294
19108
  return 0;
18295
19109
  try {
18296
19110
  let n = 0;
18297
19111
  for (const name of readdirSync20(skillsDir2)) {
18298
19112
  if (!name.startsWith("roll-"))
18299
19113
  continue;
18300
- const p = join42(skillsDir2, name);
19114
+ const p = join43(skillsDir2, name);
18301
19115
  const st = lstatSync4(p);
18302
19116
  if (st.isSymbolicLink() || st.isDirectory())
18303
19117
  n++;
@@ -18310,8 +19124,8 @@ function aiSkillCount(e) {
18310
19124
  function countFilesRecursive(dir) {
18311
19125
  let n = 0;
18312
19126
  for (const name of readdirSync20(dir)) {
18313
- const p = join42(dir, name);
18314
- const st = statSync22(p);
19127
+ const p = join43(dir, name);
19128
+ const st = statSync23(p);
18315
19129
  if (st.isDirectory())
18316
19130
  n += countFilesRecursive(p);
18317
19131
  else if (st.isFile())
@@ -18320,8 +19134,8 @@ function countFilesRecursive(dir) {
18320
19134
  return n;
18321
19135
  }
18322
19136
  function templateCount(tpl) {
18323
- const d = join42(templatesDir(), tpl);
18324
- if (!existsSync45(d))
19137
+ const d = join43(templatesDir(), tpl);
19138
+ if (!existsSync46(d))
18325
19139
  return 0;
18326
19140
  try {
18327
19141
  return countFilesRecursive(d);
@@ -18330,22 +19144,22 @@ function templateCount(tpl) {
18330
19144
  }
18331
19145
  }
18332
19146
  function skillsInstalled() {
18333
- const sd = join42(rollHome3(), "skills");
18334
- if (!existsSync45(sd))
19147
+ const sd = join43(rollHome3(), "skills");
19148
+ if (!existsSync46(sd))
18335
19149
  return 0;
18336
19150
  try {
18337
- return readdirSync20(sd).filter((n) => statSync22(join42(sd, n)).isDirectory()).length;
19151
+ return readdirSync20(sd).filter((n) => statSync23(join43(sd, n)).isDirectory()).length;
18338
19152
  } catch {
18339
19153
  return 0;
18340
19154
  }
18341
19155
  }
18342
19156
  function launchdState(service, slug) {
18343
19157
  const label4 = `com.roll.${service}.${slug}`;
18344
- const plist = join42(homedir16(), "Library", "LaunchAgents", `${label4}.plist`);
18345
- if (!existsSync45(plist))
19158
+ const plist = join43(homedir17(), "Library", "LaunchAgents", `${label4}.plist`);
19159
+ if (!existsSync46(plist))
18346
19160
  return "not-installed";
18347
19161
  try {
18348
- const out2 = execFileSync8("launchctl", ["list", label4], {
19162
+ const out2 = execFileSync9("launchctl", ["list", label4], {
18349
19163
  encoding: "utf8",
18350
19164
  stdio: ["ignore", "pipe", "ignore"]
18351
19165
  });
@@ -18476,17 +19290,17 @@ function renderThisProject(out2, d) {
18476
19290
  }
18477
19291
  function liveData() {
18478
19292
  const slug = projectSlugPy();
18479
- const home = homedir16();
19293
+ const home = homedir17();
18480
19294
  const aiClients = parseAiEntries().map((e) => ({
18481
19295
  name: e.name,
18482
19296
  cfg_file: e.cfg_file,
18483
- path: join42(e.ai_dir, e.cfg_file).replaceAll(home, "~"),
19297
+ path: join43(e.ai_dir, e.cfg_file).replaceAll(home, "~"),
18484
19298
  sync: aiSyncStatus(e),
18485
19299
  skills: aiSkillCount(e)
18486
19300
  }));
18487
19301
  const featDir = ".roll/features";
18488
19302
  let featCount = 0;
18489
- if (existsSync45(featDir)) {
19303
+ if (existsSync46(featDir)) {
18490
19304
  featCount = readdirSync20(featDir).filter((n) => n.endsWith(".md")).length;
18491
19305
  }
18492
19306
  return {
@@ -18494,8 +19308,8 @@ function liveData() {
18494
19308
  ai_clients: aiClients,
18495
19309
  templates: TEMPLATES.map((t2) => [t2, templateCount(t2)]),
18496
19310
  skills_installed: skillsInstalled(),
18497
- project_has_agents: existsSync45("AGENTS.md"),
18498
- project_has_backlog: existsSync45(".roll/backlog.md"),
19311
+ project_has_agents: existsSync46("AGENTS.md"),
19312
+ project_has_backlog: existsSync46(".roll/backlog.md"),
18499
19313
  project_features_count: featCount,
18500
19314
  loop_state: launchdState("loop", slug),
18501
19315
  dream_state: launchdState("dream", slug)
@@ -18519,8 +19333,8 @@ function statusCommand(args) {
18519
19333
 
18520
19334
  // packages/cli/dist/commands/test.js
18521
19335
  import { spawnSync as spawnSync11 } from "node:child_process";
18522
- import { existsSync as existsSync46, mkdirSync as mkdirSync24, readFileSync as readFileSync39, rmSync as rmSync12, writeFileSync as writeFileSync24 } from "node:fs";
18523
- import { dirname as dirname21, 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";
18524
19338
  function pal5() {
18525
19339
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
18526
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" };
@@ -18537,12 +19351,12 @@ function err16(line) {
18537
19351
  }
18538
19352
  var SUPPORTED_TYPES = ["none"];
18539
19353
  function isolationGetType() {
18540
- const file = join43(process.cwd(), ".roll", "local.yaml");
18541
- if (!existsSync46(file))
19354
+ const file = join44(process.cwd(), ".roll", "local.yaml");
19355
+ if (!existsSync47(file))
18542
19356
  return "none";
18543
19357
  let text;
18544
19358
  try {
18545
- text = readFileSync39(file, "utf8");
19359
+ text = readFileSync40(file, "utf8");
18546
19360
  } catch {
18547
19361
  return "none";
18548
19362
  }
@@ -18590,14 +19404,14 @@ function resetLockPath() {
18590
19404
  return ".roll/.iso-reset.lock";
18591
19405
  }
18592
19406
  function resetLockHeld() {
18593
- return existsSync46(resetLockPath());
19407
+ return existsSync47(resetLockPath());
18594
19408
  }
18595
19409
  function resetAcquireLock() {
18596
19410
  const lock = resetLockPath();
18597
- if (existsSync46(lock))
19411
+ if (existsSync47(lock))
18598
19412
  return false;
18599
- mkdirSync24(dirname21(lock), { recursive: true });
18600
- writeFileSync24(lock, `${process.pid}
19413
+ mkdirSync25(dirname20(lock), { recursive: true });
19414
+ writeFileSync25(lock, `${process.pid}
18601
19415
  `);
18602
19416
  return true;
18603
19417
  }
@@ -18614,7 +19428,7 @@ function runForward(cmd, argv) {
18614
19428
  }
18615
19429
  function isolationDispatch(method, args) {
18616
19430
  const type = isolationGetType();
18617
- if (type === "none" && !existsSync46(join43(process.cwd(), ".roll", "local.yaml"))) {
19431
+ if (type === "none" && !existsSync47(join44(process.cwd(), ".roll", "local.yaml"))) {
18618
19432
  infoErr("isolation: no test_isolation config, falling back to type=none (host)");
18619
19433
  }
18620
19434
  if (!SUPPORTED_TYPES.includes(type)) {
@@ -18708,9 +19522,9 @@ function testCommand(args) {
18708
19522
 
18709
19523
  // packages/cli/dist/commands/update.js
18710
19524
  import { spawnSync as spawnSync12 } from "node:child_process";
18711
- 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";
18712
19526
  import { tmpdir as tmpdir4 } from "node:os";
18713
- import { join as join44 } from "node:path";
19527
+ import { join as join45 } from "node:path";
18714
19528
  function pal6() {
18715
19529
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
18716
19530
  return noColor2 ? { CYAN: "", GREEN: "", YELLOW: "", RED: "", BOLD: "", NC: "" } : {
@@ -18774,20 +19588,20 @@ function resolveRemoteVersion() {
18774
19588
  }
18775
19589
  function downloadAndInstallCurl(tag) {
18776
19590
  const url = `https://github.com/seanyao/roll/archive/refs/tags/${tag}.tar.gz`;
18777
- const tmpDir = mkdtempSync4(join44(tmpdir4(), "roll-update-"));
19591
+ const tmpDir = mkdtempSync4(join45(tmpdir4(), "roll-update-"));
18778
19592
  try {
18779
19593
  info6(`[roll] Downloading roll ${tag} ...`);
18780
- 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")]);
18781
19595
  if (dl !== 0) {
18782
19596
  err17(m6("update.curl_download_failed"));
18783
19597
  return { ok: false };
18784
19598
  }
18785
19599
  info6("[roll] Extracting ...");
18786
- const extractDir = join44(tmpDir, "extract");
19600
+ const extractDir = join45(tmpDir, "extract");
18787
19601
  spawnSync12("mkdir", ["-p", extractDir], { stdio: "ignore" });
18788
19602
  const ex = runForward2("tar", [
18789
19603
  "-xzf",
18790
- join44(tmpDir, "roll.tar.gz"),
19604
+ join45(tmpDir, "roll.tar.gz"),
18791
19605
  "--strip-components=1",
18792
19606
  "-C",
18793
19607
  extractDir
@@ -18804,7 +19618,7 @@ function downloadAndInstallCurl(tag) {
18804
19618
  function checkInstalledVersionOrRetry() {
18805
19619
  const expected = (spawnSync12("npm", ["view", "@seanyao/roll", "version"], { encoding: "utf8" }).stdout ?? "").trim();
18806
19620
  const pkgRoot = (spawnSync12("npm", ["root", "-g"], { encoding: "utf8" }).stdout ?? "").trim();
18807
- const installedTree = join44(pkgRoot, "@seanyao", "roll");
19621
+ const installedTree = join45(pkgRoot, "@seanyao", "roll");
18808
19622
  const installed = treeVersion(installedTree);
18809
19623
  if (expected === "" || installed === "")
18810
19624
  return;
@@ -18818,18 +19632,18 @@ function checkInstalledVersionOrRetry() {
18818
19632
  }
18819
19633
  }
18820
19634
  function invalidateUpdateCache() {
18821
- rmSync13(join44(rollHome(), ".update-check"), { force: true });
19635
+ rmSync13(join45(rollHome(), ".update-check"), { force: true });
18822
19636
  }
18823
19637
  function showChangelog() {
18824
- const changelog = join44(rollPkgDir(), "CHANGELOG.md");
18825
- if (!existsSync47(changelog))
19638
+ const changelog = join45(rollPkgDir(), "CHANGELOG.md");
19639
+ if (!existsSync48(changelog))
18826
19640
  return;
18827
19641
  const { BOLD: BOLD2, CYAN, NC } = pal6();
18828
19642
  process.stdout.write(`${BOLD2}${m6("changelog.heading")}:${NC}
18829
19643
  `);
18830
19644
  let count = 0;
18831
19645
  let inSection = false;
18832
- for (const line of readFileSync40(changelog, "utf8").split("\n")) {
19646
+ for (const line of readFileSync41(changelog, "utf8").split("\n")) {
18833
19647
  if (/^## /.test(line)) {
18834
19648
  count += 1;
18835
19649
  if (count > 3)
@@ -18849,9 +19663,9 @@ function updateCommand(args) {
18849
19663
  void args;
18850
19664
  info6(m6("update.current_version", rollVersion()));
18851
19665
  let installMethod = "npm";
18852
- const methodFile = join44(rollPkgDir(), ".install-method");
18853
- if (existsSync47(methodFile)) {
18854
- installMethod = readFileSync40(methodFile, "utf8").trim() || "npm";
19666
+ const methodFile = join45(rollPkgDir(), ".install-method");
19667
+ if (existsSync48(methodFile)) {
19668
+ installMethod = readFileSync41(methodFile, "utf8").trim() || "npm";
18855
19669
  }
18856
19670
  if (installMethod === "curl") {
18857
19671
  info6(m6("update.upgrading_via_curl"));
@@ -18898,6 +19712,12 @@ function registerAll() {
18898
19712
  registerPorted("doctor", doctorCommand);
18899
19713
  registerPorted("attest", attestCommand);
18900
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
+ });
18901
19721
  registerPorted("gc", gcCommand);
18902
19722
  registerPorted("archive", (args) => {
18903
19723
  if (args[0] === "migrate")