@open-tomato/rafa 0.24.0 → 0.26.0

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.
@@ -12,17 +12,18 @@ import {
12
12
  DEFAULT_RELEASE_SEAMS,
13
13
  DEMOTION_VERDICTS,
14
14
  DESCRIPTION_LIMIT,
15
- DISK_FILE_SYSTEM,
16
15
  EPIC_LABEL_PREFIX,
17
16
  EPIC_TYPE_LABEL,
18
17
  HINT_FLAG_SPEC,
19
18
  HORIZONS,
20
19
  HORIZON_LABEL_PREFIX,
20
+ ISSUE_LIST_FLAGS,
21
21
  LABEL_LIST_LIMIT,
22
22
  LEARNED_DIRECTORY,
23
23
  LISTING_LIMIT,
24
24
  MECHANICAL_CONFLICT_SENTENCE,
25
25
  NEXT_COMMAND_ACTIONS,
26
+ NO_NEXT_STEP,
26
27
  ORIGIN_FIELD,
27
28
  POSITION_FILE,
28
29
  PREVIOUS_COPY_WARN_ABOVE,
@@ -30,7 +31,6 @@ import {
30
31
  REASON_FLAG,
31
32
  REMOTE,
32
33
  ROADMAP_LABEL,
33
- ROADMAP_REFUSAL_EXIT,
34
34
  ROADMAP_SETTING,
35
35
  ROADMAP_TITLE,
36
36
  RUNNING_MANIFEST_SEAMS,
@@ -38,7 +38,6 @@ import {
38
38
  RUNTIME_SUBDIR,
39
39
  RefVerifyError,
40
40
  RefsBlockError,
41
- SCOPE_DIR,
42
41
  SERVE_CLI_VERSION,
43
42
  SESSION_ID_FLAG,
44
43
  SKILL_FILE as SKILL_FILE2,
@@ -47,7 +46,6 @@ import {
47
46
  SPEC_BLOCKED_LABEL,
48
47
  SPEC_EXTENSION,
49
48
  SPEC_TEMPLATE_PATH,
50
- SessionRecordError,
51
49
  TEMPLATES_DIRNAME,
52
50
  TRIAGE_MODULE,
53
51
  VENDOR_COMMAND,
@@ -98,12 +96,12 @@ import {
98
96
  createLinePrompter,
99
97
  createPlanDirNames,
100
98
  createPlanRefsVerifier,
99
+ developmentApplied,
101
100
  endWithNextStep,
102
101
  endingWith,
103
102
  epicEndLines,
104
103
  epicProblemMessage,
105
104
  epicSlugsOf,
106
- errorCode,
107
105
  estimateEta,
108
106
  etaLine,
109
107
  expectAtMostOneArgument,
@@ -119,6 +117,7 @@ import {
119
117
  formatCounts,
120
118
  formatDuration,
121
119
  formatRows,
120
+ gateMeaning,
122
121
  ghHostOf,
123
122
  ghMissingMessage,
124
123
  ghNextBoard,
@@ -128,7 +127,6 @@ import {
128
127
  gitSaid,
129
128
  groupByEpicLabel,
130
129
  hasDiverged,
131
- hasEpicLines,
132
130
  hasPlanFor,
133
131
  hasSpecBlockedLabel,
134
132
  hop,
@@ -147,13 +145,13 @@ import {
147
145
  lineRefusal,
148
146
  listBoardLabels,
149
147
  list_default,
148
+ list_default1 as list_default2,
150
149
  loadConfig,
151
150
  localDay,
152
151
  memoiseVerifier,
153
152
  minutesBetween,
154
153
  missingBoardLabels,
155
154
  missingPlanAgents,
156
- name,
157
155
  namedSecrets,
158
156
  nextQuestion,
159
157
  notesFileName,
@@ -200,12 +198,10 @@ import {
200
198
  readRecords,
201
199
  readRefsText,
202
200
  readRequiredFlag,
203
- readRoadmapEpicRows,
204
201
  readRoadmapSetting,
205
- readSession,
202
+ readSchemaReport,
206
203
  readSessionChecklist,
207
204
  readSessionFinishes,
208
- readSessions,
209
205
  readSwitch,
210
206
  readTags,
211
207
  readTextFlag,
@@ -220,12 +216,10 @@ import {
220
216
  renderCloseComment,
221
217
  renderDependentComment,
222
218
  renderDoctorRefs,
223
- renderEpicTable,
224
219
  renderHorizonComment,
225
220
  renderMoveComment,
226
221
  renderParkedPullRequestComment,
227
222
  renderRiskText,
228
- renderRoadmapTable,
229
223
  renderUnblockNote,
230
224
  report,
231
225
  requireGhProvider,
@@ -244,13 +238,10 @@ import {
244
238
  resolveReleaseEnabled,
245
239
  resolveTracker,
246
240
  restampCopyRefs,
247
- roadmapRefsCells,
248
241
  runAction,
249
- runsDir,
242
+ runIssueList,
250
243
  samePlace,
251
244
  scanClaimBranches,
252
- scopeAt,
253
- selfAndAncestors,
254
245
  serveVerdict,
255
246
  sessionIdFlag,
256
247
  sessionLine,
@@ -269,6 +260,8 @@ import {
269
260
  triageReport,
270
261
  trustRefusalClause,
271
262
  unaskedReasonMessage,
263
+ unknownAdditive,
264
+ unknownAdditiveWarning,
272
265
  unlabelledRoadmapMessage,
273
266
  unresolvedTracker,
274
267
  urlLines,
@@ -276,18 +269,15 @@ import {
276
269
  validateManifest,
277
270
  validate_default,
278
271
  verdictOf,
279
- version,
280
- versionLine,
281
272
  waitForChecks,
282
273
  watchDryEpic,
283
274
  writePositionFile,
284
275
  writeSession,
285
276
  writeShippedFile
286
- } from "./index-gt9acb1y.js";
277
+ } from "./index-59x3zsh4.js";
287
278
  import {
288
279
  PROBE_TIMEOUT_MS,
289
280
  findNextTask,
290
- isStampableStub,
291
281
  loadPlanPrerequisites,
292
282
  malformedPrerequisiteLines,
293
283
  mergePlanPrerequisites,
@@ -297,42 +287,71 @@ import {
297
287
  runPreflight,
298
288
  splitBlockerComment,
299
289
  trackerPathFor
300
- } from "./index-21qkxk63.js";
290
+ } from "./index-p1265xgj.js";
301
291
  import {
302
292
  ACTION_HEADING,
293
+ ADOPTION_NAME,
303
294
  CAUSE_HEADING,
304
295
  CLAUDE_BIN,
305
296
  CORE_ADAPTER_REGISTRY,
297
+ DISK_FILE_SYSTEM,
298
+ DevelopmentBuildRefusedError,
299
+ EFFORT_DIR_VARIABLE,
306
300
  EFFORT_STORE_DIR,
307
301
  ISSUE_PRIORITIES,
308
302
  ISSUE_STATES,
309
303
  ISSUE_TYPES,
310
304
  MARKDOWN_EXTENSION,
305
+ MIGRATION_LOG_TABLE,
306
+ SCOPE_DIR,
311
307
  SKILL_FILE,
312
308
  SQLITE_MIGRATIONS,
309
+ SQLITE_STORE_FILE_NAME,
310
+ SessionRecordError,
311
+ activeStoreSettings,
313
312
  allRecords,
313
+ appliedByName,
314
+ bringForward,
314
315
  claudeArgs,
315
316
  createGhRunner,
316
- effortStorePath,
317
+ effortStoreDir,
318
+ effortStoreFileName,
319
+ errorCode,
317
320
  findRecords,
318
321
  gitRemoteUrl,
322
+ guardTestProcess,
319
323
  instinctProject,
324
+ isPidAlive,
320
325
  mergeFrontmatter,
321
- migrateSchema,
326
+ name,
322
327
  normalizeRemote,
328
+ planSchema,
323
329
  readFrontmatter,
324
330
  readFrontmatterDocument,
331
+ readRuntimeIdentity,
325
332
  readScopes,
333
+ readSession,
334
+ readSessions,
335
+ readStoreSchema,
336
+ refuseUnownedDevelopmentWrite,
326
337
  renderFrontmatter,
327
338
  runClaudeCaptured,
339
+ runsDir,
328
340
  scanLayout,
341
+ scopeAt,
329
342
  selectEffortStore,
343
+ selfAndAncestors,
330
344
  sessionSpawnEnv,
331
345
  spawnClaudeCaptured,
346
+ sqliteCatalogue,
332
347
  sqliteStorePath,
348
+ storeProjectRoot,
333
349
  toHeldRecords,
334
- writeFrontmatter
335
- } from "./index-7qgng1rf.js";
350
+ version,
351
+ versionLine,
352
+ writeFrontmatter,
353
+ writeNames
354
+ } from "./index-azptzvp3.js";
336
355
  import {
337
356
  CONFIG_DEFAULTS,
338
357
  CONFIG_FILE,
@@ -341,12 +360,13 @@ import {
341
360
  activeOutput,
342
361
  configFilePath,
343
362
  findTierItem,
363
+ isStampableStub,
344
364
  parseConfigText,
345
365
  pinKey,
346
366
  readItemBytes,
347
367
  readRafaBlocks,
348
368
  resolveTiers
349
- } from "./index-twbp38kz.js";
369
+ } from "./index-rv7y8y1j.js";
350
370
  import {
351
371
  promotable
352
372
  } from "./index-d6d0942q.js";
@@ -354,10 +374,10 @@ import {
354
374
  createCommandRegistry,
355
375
  describeRegistry,
356
376
  mountKey
357
- } from "./index-w1dnwja1.js";
377
+ } from "./index-xwr54rzt.js";
358
378
  import {
359
379
  CommandExit
360
- } from "./index-4fwgqveh.js";
380
+ } from "./index-4j1e3h26.js";
361
381
  import {
362
382
  MERGE_METHODS,
363
383
  RELEASE_AUTO,
@@ -369,7 +389,7 @@ import {
369
389
  isSkillTier,
370
390
  messageOf,
371
391
  parseBudgetUsd
372
- } from "./index-pmkgvtfe.js";
392
+ } from "./index-sf2brh4w.js";
373
393
  import {
374
394
  triggerKey
375
395
  } from "./index-wp8c28fd.js";
@@ -1929,7 +1949,7 @@ function createSkillListCommand(seams = DEFAULT_SKILL_LIST_SEAMS) {
1929
1949
  };
1930
1950
  return Object.freeze(command);
1931
1951
  }
1932
- var list_default2 = createSkillListCommand();
1952
+ var list_default3 = createSkillListCommand();
1933
1953
 
1934
1954
  // src/commands/agent/list.ts
1935
1955
  var DEFAULT_AGENT_LIST_SEAMS = Object.freeze({
@@ -2131,7 +2151,7 @@ function createAgentListCommand(seams = DEFAULT_AGENT_LIST_SEAMS) {
2131
2151
  };
2132
2152
  return Object.freeze(command);
2133
2153
  }
2134
- var list_default3 = createAgentListCommand();
2154
+ var list_default4 = createAgentListCommand();
2135
2155
 
2136
2156
  // src/inventory/search/index.ts
2137
2157
  import { randomUUID } from "crypto";
@@ -3362,7 +3382,7 @@ function createBoardListCommand(seams = {}) {
3362
3382
  };
3363
3383
  return Object.freeze(command);
3364
3384
  }
3365
- var list_default4 = createBoardListCommand();
3385
+ var list_default5 = createBoardListCommand();
3366
3386
 
3367
3387
  // src/cleanup/branches.ts
3368
3388
  var BRANCH_FORMAT = [
@@ -4634,7 +4654,7 @@ var describeCommand = {
4634
4654
  var describe_default = Object.freeze(describeCommand);
4635
4655
 
4636
4656
  // src/commands/doctor.ts
4637
- import { basename as basename6, resolve as resolve5 } from "path";
4657
+ import { basename as basename5, resolve as resolve5 } from "path";
4638
4658
 
4639
4659
  // src/board/status.ts
4640
4660
  import { statSync as statSync4 } from "fs";
@@ -6676,14 +6696,77 @@ function renderDeep(reading) {
6676
6696
  return deepSections(reading).flatMap(renderDeepSection);
6677
6697
  }
6678
6698
 
6699
+ // src/commands/doctor-effort-schema.ts
6700
+ import { join as join15 } from "path";
6701
+ var EFFORT_SCHEMA_ROW = "effort store schema";
6702
+ var SCHEMA_COMMAND = "rafa effort schema";
6703
+ function developmentAppliedWarning(path, id2, appliedBy) {
6704
+ return `\u26A0 effort store ${path} logs migration ${id2} applied by a development build (${appliedBy});` + ` a development build migrates only a copy. Read the log with ${SCHEMA_COMMAND}.`;
6705
+ }
6706
+ function unreadable(path, live, problem) {
6707
+ return { outcome: "fail", path, live, status: "unreadable", problem, warnings: [], nextStep: NO_NEXT_STEP };
6708
+ }
6709
+ function readingOf(report2, live) {
6710
+ const { path, status, nextStep } = report2;
6711
+ if (report2.refused) {
6712
+ const problem = `this rafa refuses ${path} (${status}): ${String(report2.refusal)}`;
6713
+ return { outcome: "fail", path, live, status, problem, warnings: [], nextStep };
6714
+ }
6715
+ const developed = live ? developmentApplied(report2).map(({ id: id2, appliedBy }) => developmentAppliedWarning(path, id2, appliedBy)) : [];
6716
+ const warnings = [...unknownAdditive(report2).map(unknownAdditiveWarning), ...developed];
6717
+ const outcome = warnings.length === 0 ? "ok" : "warn";
6718
+ return { outcome, path, live, status, problem: null, warnings, nextStep };
6719
+ }
6720
+ function readDoctorEffortSchema(root, env, options = {}) {
6721
+ const override = env[EFFORT_DIR_VARIABLE];
6722
+ const live = override === undefined || override === "";
6723
+ let path;
6724
+ try {
6725
+ path = join15(effortStoreDir(root, env), SQLITE_STORE_FILE_NAME);
6726
+ } catch (error) {
6727
+ return unreadable(null, live, messageOf(error));
6728
+ }
6729
+ try {
6730
+ return readingOf(readSchemaReport(path, options), live);
6731
+ } catch (error) {
6732
+ return unreadable(path, live, `${path} cannot be read as an effort store: ${messageOf(error)}`);
6733
+ }
6734
+ }
6735
+ function wayOut(reading) {
6736
+ return reading.nextStep === NO_NEXT_STEP ? "" : ` Next safe step: ${reading.nextStep}`;
6737
+ }
6738
+ function renderDoctorEffortSchema(reading) {
6739
+ if (reading.outcome === "ok" && reading.status === "absent")
6740
+ return [];
6741
+ const where = reading.path === null ? "" : ` (${reading.path})`;
6742
+ const row = `Effort store schema: ${reading.outcome}, ${reading.status}${where}`;
6743
+ if (reading.problem === null)
6744
+ return [row];
6745
+ const next = reading.nextStep === NO_NEXT_STEP ? [] : [` Next safe step: ${reading.nextStep}`];
6746
+ return [row, ` ${reading.problem}`, ...next];
6747
+ }
6748
+ function writeDoctorEffortSchema(context, reading) {
6749
+ if (context.outputMode !== "json") {
6750
+ for (const line of renderDoctorEffortSchema(reading))
6751
+ context.output.info(line);
6752
+ }
6753
+ for (const warning of reading.warnings)
6754
+ context.output.warn(warning);
6755
+ }
6756
+ function effortSchemaRefusal(reading) {
6757
+ if (reading.outcome !== "fail")
6758
+ return null;
6759
+ return `rafa doctor: ${EFFORT_SCHEMA_ROW}: ${String(reading.problem)}${wayOut(reading)}`;
6760
+ }
6761
+
6679
6762
  // src/effort/store/legacy.ts
6680
6763
  import { statSync as statSync6 } from "fs";
6681
- import { basename as basename4, join as join15 } from "path";
6682
- var LEGACY_EFFORT_STORE_DIR = join15(".ralph", "effort");
6764
+ import { join as join16 } from "path";
6765
+ var LEGACY_EFFORT_STORE_DIR = join16(".ralph", "effort");
6683
6766
  var NDJSON_KINDS = { sessions: true, commits: true };
6684
6767
  var STORE_FILE_NAMES = Object.freeze([
6685
- basename4(sqliteStorePath("")),
6686
- ...Object.keys(NDJSON_KINDS).map((kind) => basename4(effortStorePath("", kind)))
6768
+ SQLITE_STORE_FILE_NAME,
6769
+ ...Object.keys(NDJSON_KINDS).map((kind) => effortStoreFileName(kind))
6687
6770
  ]);
6688
6771
  function isAbsence(error) {
6689
6772
  if (typeof error !== "object" || error === null || !("code" in error))
@@ -6700,7 +6783,7 @@ function isStoreFile(path) {
6700
6783
  }
6701
6784
  }
6702
6785
  function storeFilesIn(dir) {
6703
- return Object.freeze(STORE_FILE_NAMES.filter((name2) => isStoreFile(join15(dir, name2))));
6786
+ return Object.freeze(STORE_FILE_NAMES.filter((name2) => isStoreFile(join16(dir, name2))));
6704
6787
  }
6705
6788
  function shellQuoted(path) {
6706
6789
  return `'${path.replaceAll("'", "'\\''")}'`;
@@ -6710,8 +6793,8 @@ function legacyWarning(legacyDir, storeDir, legacyFiles) {
6710
6793
  return `${legacyDir} holds an effort store (${legacyFiles.join(", ")}) and ${storeDir} holds none;` + ` rafa reads ${storeDir} alone, so its reports start from an empty store. Once no loop is running,` + ` move it: ${move3}`;
6711
6794
  }
6712
6795
  function readLegacyStore(root) {
6713
- const legacyDir = join15(root, LEGACY_EFFORT_STORE_DIR);
6714
- const storeDir = join15(root, EFFORT_STORE_DIR);
6796
+ const legacyDir = join16(root, LEGACY_EFFORT_STORE_DIR);
6797
+ const storeDir = join16(root, EFFORT_STORE_DIR);
6715
6798
  const legacyFiles = storeFilesIn(legacyDir);
6716
6799
  const storeFiles = storeFilesIn(storeDir);
6717
6800
  const warning = legacyFiles.length > 0 && storeFiles.length === 0 ? legacyWarning(legacyDir, storeDir, legacyFiles) : null;
@@ -6719,9 +6802,9 @@ function readLegacyStore(root) {
6719
6802
  }
6720
6803
 
6721
6804
  // src/project/bin-path.ts
6722
- import { delimiter, isAbsolute as isAbsolute4, join as join16, resolve as resolve4 } from "path";
6723
- var RAFA_BIN_DIR = join16(SCOPE_DIR, "bin");
6724
- var BUN_BIN_DIR = join16(".bun", "bin");
6805
+ import { delimiter, isAbsolute as isAbsolute4, join as join17, resolve as resolve4 } from "path";
6806
+ var RAFA_BIN_DIR = join17(SCOPE_DIR, "bin");
6807
+ var BUN_BIN_DIR = join17(".bun", "bin");
6725
6808
  function exportLine(rafaBin) {
6726
6809
  return `export PATH="${rafaBin}:$PATH"`;
6727
6810
  }
@@ -6754,8 +6837,8 @@ function stateOf(rafaIndex, bunIndex) {
6754
6837
  return "ahead";
6755
6838
  }
6756
6839
  function readBinPath(pathValue, home, fs = DISK_FILE_SYSTEM) {
6757
- const rafaBin = join16(home, RAFA_BIN_DIR);
6758
- const bunBin = join16(home, BUN_BIN_DIR);
6840
+ const rafaBin = join17(home, RAFA_BIN_DIR);
6841
+ const bunBin = join17(home, BUN_BIN_DIR);
6759
6842
  const entries = (pathValue ?? "").split(delimiter);
6760
6843
  const rafaIndex = indexOf(entries, rafaBin, fs);
6761
6844
  const bunIndex = indexOf(entries, bunBin, fs);
@@ -6859,7 +6942,7 @@ function writeInstall(context, install) {
6859
6942
 
6860
6943
  // src/commands/doctor-tiers.ts
6861
6944
  import { readFileSync as readFileSync7, realpathSync as realpathSync4 } from "fs";
6862
- import { basename as basename5, dirname as dirname2 } from "path";
6945
+ import { basename as basename4, dirname as dirname2 } from "path";
6863
6946
  var TIERS_SECTION_TITLE = "Skill tiers";
6864
6947
  var CLAUDE_VERSION_TIMEOUT_MS = 1e4;
6865
6948
  var VERSION_PATTERN = /^\s*(\d+\.\d+\.\d+)/;
@@ -6920,7 +7003,7 @@ function keptHolder(item) {
6920
7003
  return [item.winner, ...item.copies].find((holder) => holder.source === "rafa") ?? item.winner;
6921
7004
  }
6922
7005
  function deletion(holder) {
6923
- return holder.kind === "skill" && basename5(holder.path) === SKILL_FILE2 ? `delete ${dirname2(holder.path)} (only its ${SKILL_FILE2} was compared)` : `delete ${holder.path}`;
7006
+ return holder.kind === "skill" && basename4(holder.path) === SKILL_FILE2 ? `delete ${dirname2(holder.path)} (only its ${SKILL_FILE2} was compared)` : `delete ${holder.path}`;
6924
7007
  }
6925
7008
  function copyRows(items, realPath = realPathOf) {
6926
7009
  return items.flatMap((item) => {
@@ -7064,7 +7147,7 @@ async function loadItems(plan2, config) {
7064
7147
  function refuseMalformedItems(plan2, items) {
7065
7148
  if (plan2 === null || items.malformed.length === 0)
7066
7149
  return;
7067
- const file = basename6(prerequisitesPathForPlan(plan2) ?? plan2);
7150
+ const file = basename5(prerequisitesPathForPlan(plan2) ?? plan2);
7068
7151
  const [head = "", ...rest] = malformedPrerequisiteLines(file, items.malformed);
7069
7152
  throw refusal3([
7070
7153
  `rafa doctor: ${head}`,
@@ -7091,7 +7174,7 @@ function readStartTier(plan2, items) {
7091
7174
  return { tier: NO_START_TIER, items: NO_ITEMS };
7092
7175
  if (isFirstDispatch(plan2))
7093
7176
  return { tier: { checked: start2.length, skipped: 0, tracker: null }, items: start2 };
7094
- const tracker = basename6(trackerPathFor(plan2));
7177
+ const tracker = basename5(trackerPathFor(plan2));
7095
7178
  return { tier: { checked: 0, skipped: start2.length, tracker }, items: NO_ITEMS };
7096
7179
  }
7097
7180
  async function checkPreflight(context, project, seams) {
@@ -7128,11 +7211,15 @@ async function checkPreflight(context, project, seams) {
7128
7211
  deep
7129
7212
  });
7130
7213
  }
7131
- function haltRefusal(halt) {
7132
- return new CommandExit(1, [
7214
+ function doctorRefusal(halt, schema) {
7215
+ if (halt === null) {
7216
+ return schema === null ? null : new CommandExit(1, schema);
7217
+ }
7218
+ const lines2 = [
7133
7219
  `rafa doctor: ${halt}`,
7134
7220
  "rafa loop start would halt here, before any session. No run was started and nothing was stored."
7135
- ].join(`
7221
+ ];
7222
+ return new CommandExit(1, [...lines2, ...schema === null ? [] : [schema]].join(`
7136
7223
  `));
7137
7224
  }
7138
7225
  async function checkDeep(context, preflight, seams) {
@@ -7162,6 +7249,7 @@ function resultOf(preflight, install, readings, deep) {
7162
7249
  cleanup: readings.cleanup,
7163
7250
  refs: readings.refs,
7164
7251
  tiers: readings.tiers,
7252
+ effortSchema: readings.effortSchema,
7165
7253
  deep
7166
7254
  };
7167
7255
  }
@@ -7195,16 +7283,20 @@ async function runDoctor(context, seams) {
7195
7283
  const cleanup = await readDoctorCleanup({ root: project.root, home: project.home, config: preflight.config, gh }, seams);
7196
7284
  const refs = await readDoctorRefs({ root: project.root, specsDir: preflight.config.specsDir, gh, env: context.env }, seams);
7197
7285
  const tiers = await checkDoctorTiers({ project, env: context.env, resolved: preflight.resolved, plan: null }, seams);
7198
- const readings = { ...await readDoctorBoard(gh, project.root, preflight.config.roadmapIssue), cleanup, refs, tiers };
7286
+ const effortSchema = readDoctorEffortSchema(project.root, context.env);
7287
+ const board = await readDoctorBoard(gh, project.root, preflight.config.roadmapIssue);
7288
+ const readings = { ...board, cleanup, refs, tiers, effortSchema };
7199
7289
  writeText(context, renderDoctor(preflight));
7200
7290
  await announceRisk(context, preflight, seams);
7201
7291
  const repository = [...renderDoctorBoard(readings), ...renderDoctorCleanup(readings.cleanup)];
7202
7292
  writeText(context, [...repository, ...renderDoctorRefs(readings.refs)]);
7203
7293
  writeText(context, renderDoctorTiers(readings.tiers));
7294
+ writeDoctorEffortSchema(context, effortSchema);
7204
7295
  const deep = await checkDeep(context, preflight, seams);
7205
7296
  writeText(context, deep === null ? [] : renderDeep(deep));
7206
- if (preflight.report.halt !== null)
7207
- throw haltRefusal(preflight.report.halt);
7297
+ const refusal4 = doctorRefusal(preflight.report.halt, effortSchemaRefusal(effortSchema));
7298
+ if (refusal4 !== null)
7299
+ throw refusal4;
7208
7300
  if (context.outputMode === "json")
7209
7301
  context.output.result(resultOf(preflight, install, readings, deep));
7210
7302
  } finally {
@@ -7217,7 +7309,7 @@ function createDoctorCommand(seams = DEFAULT_DOCTOR_SEAMS) {
7217
7309
  subject: "doctor",
7218
7310
  action: "doctor",
7219
7311
  summary: "check the prerequisites rafa loop start checks, and the install, starting no run",
7220
- description: "Checks the prerequisites `rafa loop start` checks before its first session and prints each" + " check, starting no run and storing nothing: the required and optional items of" + " `.rafa/config.yaml`, with the `PREREQUISITES-<stub>.md` beside the plan merged in, and, when the" + " repository resolves to `pr.provider: gh`, the two required items that provider adds ahead of them:" + " `gh` on PATH and `gh auth status` for the remote's host. A plan's `[start]` items are checked" + " after those and ahead of the configured tiers, on a first dispatch alone: a" + " `PLAN_TRACKER-<stub>.md` beside the plan already holding a ticked task makes the next run a" + " resume, which checks none of them and says in one line how many it passed over." + " The plan is the" + " one `--plan=<file>` names, relative to the project root, or the default plan `rafa loop start`" + ` runs. Each probe runs in the project root with stdin closed and a ${String(PROBE_TIMEOUT_MS / 1000)}-second` + " timeout. It exits 1 when a required item fails, naming the item, its probe and its exit code or" + " first line of stderr, where `rafa loop start` would halt, and 0 otherwise. It warns when" + " `.ralph/effort/` holds an effort store and `.rafa/effort/` holds none, and when `~/.rafa/bin` is" + " not on PATH ahead of `~/.bun/bin`, and when `previous/` under `specs.dir` holds more than fifty" + " previous copies of issue specs, which are safe to delete; a warning never changes the exit code. On a repository whose" + " provider is `gh` it also reads the GitHub board `rafa init --board` sets up and prints one row per" + " part \u2014 the seven labels, the spec issue template, the Roadmap issue and `roadmap.issue` \u2014 as present," + " missing, or unknown for a reading that failed, naming `rafa init --board` as the fix; it writes" + " nothing to the board and a row never changes the exit code. It then names, under `Blocked" + " issues:`, every open issue labelled `spec:blocked` whose `Blocked by:` line is missing, names no" + " issue, names itself, or names an id the board has no issue for, with what an author does about" + " it; that reading writes nothing and never changes the exit code either. It then names, under" + " `Epic labels:`, every issue carrying two `epic:` labels and every `epic:` label no `type:epic` issue" + " carries, read off one board listing, writing nothing and never changing the exit code. It then names, under" + " `Boards:`, every type:roadmap board whose Owner: handle resolves to nobody GitHub shows, every open issue titled" + ` "Roadmap" without type:roadmap while labelled boards exist, and every slot of this checkout's position on a` + " board or epic that is closed, unlabelled or gone, read off that same listing and printing nothing when there is" + " none; it writes nothing and never changes the exit code. It then counts, without" + " fetching, the branches and worktrees `rafa cleanup` would list, and prints them in one row naming" + " `rafa cleanup` when any group holds one. It then counts the suspect, dangling and unknown references" + " of every saved copy under `specs.dir`, writing nothing, and names `rafa issue check <n>` for each" + " copy holding a suspect or dangling one. It then prints, under `Skill tiers`, a warning per" + " skill or agent name two tiers hold with different contents, per unreviewed third-party rafa or" + " add-on item, and for an installed Claude Code other than the version skill serving was probed" + " against, and a note per byte-identical copy that can be deleted and per user-tier item with no" + " `provenance`; none changes the exit code. With `--output=json` the" + " checks, both readings, those rows, those issues, those labels and those boards are the data of the terminal result event," + " unless a required item failed. A plan `--plan` names also gets the one-line risk total" + " `rafa loop start` prints before its notices, which never changes the exit code. With `--deep` it" + " also prints the machine as a loop session sees it, starting no session: the session's" + " Environment and working directory against the shell's, the Settings each agent, skill and MCP" + " server is read from and whether a session sees it, the Providers `gh` answers under the session's" + " environment, and the Stack tools the project's stack needs, with Plan needs for a plan `--plan`" + " names; each row is ok, warn or note with its fix, printed after the blocked issues and before a" + " refusal, and none changes the exit code. With `--output=json` they are the `deep` of the data.",
7312
+ description: "Checks the prerequisites `rafa loop start` checks before its first session and prints each" + " check, starting no run and storing nothing: the required and optional items of" + " `.rafa/config.yaml`, with the `PREREQUISITES-<stub>.md` beside the plan merged in, and, when the" + " repository resolves to `pr.provider: gh`, the two required items that provider adds ahead of them:" + " `gh` on PATH and `gh auth status` for the remote's host. A plan's `[start]` items are checked" + " after those and ahead of the configured tiers, on a first dispatch alone: a" + " `PLAN_TRACKER-<stub>.md` beside the plan already holding a ticked task makes the next run a" + " resume, which checks none of them and says in one line how many it passed over." + " The plan is the" + " one `--plan=<file>` names, relative to the project root, or the default plan `rafa loop start`" + ` runs. Each probe runs in the project root with stdin closed and a ${String(PROBE_TIMEOUT_MS / 1000)}-second` + " timeout. It exits 1 when a required item fails, naming the item, its probe and its exit code or" + " first line of stderr, where `rafa loop start` would halt. It reads the effort store as" + " `rafa effort schema --check` does, changing nothing, and prints its `effort store schema` row:" + " it exits 1 where that check fails, naming the next safe step, and warns on a migration this rafa" + " does not know that is additive and on one the project's own store logs as applied by a development" + " build. It exits 0 otherwise. It warns when" + " `.ralph/effort/` holds an effort store and `.rafa/effort/` holds none, and when `~/.rafa/bin` is" + " not on PATH ahead of `~/.bun/bin`, and when `previous/` under `specs.dir` holds more than fifty" + " previous copies of issue specs, which are safe to delete; a warning never changes the exit code. On a repository whose" + " provider is `gh` it also reads the GitHub board `rafa init --board` sets up and prints one row per" + " part \u2014 the seven labels, the spec issue template, the Roadmap issue and `roadmap.issue` \u2014 as present," + " missing, or unknown for a reading that failed, naming `rafa init --board` as the fix; it writes" + " nothing to the board and a row never changes the exit code. It then names, under `Blocked" + " issues:`, every open issue labelled `spec:blocked` whose `Blocked by:` line is missing, names no" + " issue, names itself, or names an id the board has no issue for, with what an author does about" + " it; that reading writes nothing and never changes the exit code either. It then names, under" + " `Epic labels:`, every issue carrying two `epic:` labels and every `epic:` label no `type:epic` issue" + " carries, read off one board listing, writing nothing and never changing the exit code. It then names, under" + " `Boards:`, every type:roadmap board whose Owner: handle resolves to nobody GitHub shows, every open issue titled" + ` "Roadmap" without type:roadmap while labelled boards exist, and every slot of this checkout's position on a` + " board or epic that is closed, unlabelled or gone, read off that same listing and printing nothing when there is" + " none; it writes nothing and never changes the exit code. It then counts, without" + " fetching, the branches and worktrees `rafa cleanup` would list, and prints them in one row naming" + " `rafa cleanup` when any group holds one. It then counts the suspect, dangling and unknown references" + " of every saved copy under `specs.dir`, writing nothing, and names `rafa issue check <n>` for each" + " copy holding a suspect or dangling one. It then prints, under `Skill tiers`, a warning per" + " skill or agent name two tiers hold with different contents, per unreviewed third-party rafa or" + " add-on item, and for an installed Claude Code other than the version skill serving was probed" + " against, and a note per byte-identical copy that can be deleted and per user-tier item with no" + " `provenance`; none changes the exit code. With `--output=json` the" + " checks, both readings, those rows, those issues, those labels and those boards are the data of the terminal result event," + " unless a required item failed. A plan `--plan` names also gets the one-line risk total" + " `rafa loop start` prints before its notices, which never changes the exit code. With `--deep` it" + " also prints the machine as a loop session sees it, starting no session: the session's" + " Environment and working directory against the shell's, the Settings each agent, skill and MCP" + " server is read from and whether a session sees it, the Providers `gh` answers under the session's" + " environment, and the Stack tools the project's stack needs, with Plan needs for a plan `--plan`" + " names; each row is ok, warn or note with its fix, printed after the blocked issues and before a" + " refusal, and none changes the exit code. With `--output=json` they are the `deep` of the data.",
7221
7313
  args: [],
7222
7314
  flags: [
7223
7315
  {
@@ -7349,52 +7441,209 @@ var collect_default = wrapPhaseZeroCommand({
7349
7441
  outputs: ["text", "json"]
7350
7442
  }, collect);
7351
7443
 
7352
- // src/effort/store/fix-schema.ts
7353
- import { existsSync as existsSync5, renameSync, rmSync as rmSync2 } from "fs";
7444
+ // src/commands/effort/copy.ts
7445
+ import { join as join19, resolve as resolve7 } from "path";
7446
+
7447
+ // src/effort/store/copy.ts
7448
+ import { copyFileSync as copyFileSync3, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, rmSync as rmSync2, statSync as statSync7 } from "fs";
7449
+ import { join as join18, resolve as resolve6 } from "path";
7354
7450
  import { Database } from "bun:sqlite";
7355
- class SchemaFixRefusal extends Error {
7451
+ var NDJSON_KINDS2 = { sessions: true, commits: true };
7452
+ var EFFORT_COPY_FILE_NAMES = [
7453
+ SQLITE_STORE_FILE_NAME,
7454
+ ...Object.keys(NDJSON_KINDS2).map((kind) => effortStoreFileName(kind))
7455
+ ];
7456
+
7457
+ class EffortCopyRefusal extends Error {
7458
+ name = "EffortCopyRefusal";
7459
+ }
7460
+
7461
+ class EffortCopyFailure extends Error {
7462
+ name = "EffortCopyFailure";
7463
+ }
7464
+ function refuseUsedTarget(target) {
7465
+ if (!existsSync5(target))
7466
+ return;
7467
+ let entries;
7468
+ try {
7469
+ if (!statSync7(target).isDirectory()) {
7470
+ throw new EffortCopyRefusal(`${target} is a file, not a directory; name an empty or absent directory with --to=<dir>.`);
7471
+ }
7472
+ entries = readdirSync(target);
7473
+ } catch (error) {
7474
+ if (error instanceof EffortCopyRefusal)
7475
+ throw error;
7476
+ throw new EffortCopyFailure(`${target} cannot be read: ${String(error)}`);
7477
+ }
7478
+ if (entries.length === 0)
7479
+ return;
7480
+ throw new EffortCopyRefusal(`${target} is not empty (${String(entries.length)} entries); nothing was copied.` + " Name an empty or absent directory with --to=<dir>.");
7481
+ }
7482
+ function vacuumInto(source, target) {
7483
+ const db = new Database(source, { readonly: true });
7484
+ try {
7485
+ db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
7486
+ db.run("VACUUM INTO ?", [target]);
7487
+ } finally {
7488
+ db.close();
7489
+ }
7490
+ }
7491
+ function copyOne2(name2, source, target) {
7492
+ if (name2 === SQLITE_STORE_FILE_NAME) {
7493
+ vacuumInto(join18(source, name2), join18(target, name2));
7494
+ return;
7495
+ }
7496
+ copyFileSync3(join18(source, name2), join18(target, name2), constants.COPYFILE_EXCL);
7497
+ }
7498
+ function undo(target, written, madeDir) {
7499
+ try {
7500
+ if (madeDir === undefined) {
7501
+ for (const name2 of written)
7502
+ rmSync2(join18(target, name2), { force: true });
7503
+ } else {
7504
+ rmSync2(madeDir, { recursive: true, force: true });
7505
+ }
7506
+ return "";
7507
+ } catch (error) {
7508
+ return ` Removing what it made failed too: ${String(error)}`;
7509
+ }
7510
+ }
7511
+ function copyEffortStore(request) {
7512
+ const source = resolve6(request.source);
7513
+ const target = resolve6(request.target);
7514
+ guardTestProcess(source);
7515
+ guardTestProcess(target);
7516
+ const files = EFFORT_COPY_FILE_NAMES.filter((name2) => existsSync5(join18(source, name2)));
7517
+ if (files.length === 0) {
7518
+ throw new EffortCopyRefusal(`no effort store at ${source}: it holds none of ${EFFORT_COPY_FILE_NAMES.join(", ")}. Nothing to copy.`);
7519
+ }
7520
+ refuseUsedTarget(target);
7521
+ const written = [];
7522
+ let madeDir;
7523
+ try {
7524
+ madeDir = mkdirSync3(target, { recursive: true });
7525
+ for (const name2 of files) {
7526
+ written.push(name2);
7527
+ copyOne2(name2, source, target);
7528
+ }
7529
+ } catch (error) {
7530
+ const left = undo(target, written, madeDir);
7531
+ throw new EffortCopyFailure(`copying ${source} to ${target} failed: ${String(error)}.${left}`);
7532
+ }
7533
+ return Object.freeze({ source, directory: target, files: Object.freeze(files) });
7534
+ }
7535
+
7536
+ // src/effort/store/fix-schema.ts
7537
+ import { existsSync as existsSync7 } from "fs";
7538
+ import { Database as Database2 } from "bun:sqlite";
7539
+
7540
+ // src/effort/store/rebuild-aside.ts
7541
+ import { existsSync as existsSync6, renameSync, rmSync as rmSync3 } from "fs";
7542
+
7543
+ class RebuildRefusal extends Error {
7356
7544
  constructor(message) {
7357
7545
  super(message);
7358
- this.name = "SchemaFixRefusal";
7546
+ this.name = "RebuildRefusal";
7359
7547
  }
7360
7548
  }
7361
- var LIVE = "live";
7362
7549
  function quoted(name2) {
7363
7550
  return `"${name2.replaceAll('"', '""')}"`;
7364
7551
  }
7552
+ function count(db, sql) {
7553
+ return db.query(sql).get()?.n ?? 0;
7554
+ }
7555
+ function refuseInFlight(path) {
7556
+ for (const suffix of ["-journal", "-wal"]) {
7557
+ if (existsSync6(`${path}${suffix}`)) {
7558
+ throw new RebuildRefusal(`${path}${suffix} is there, so a write is in flight or was interrupted; stop whatever uses the store` + " and open it once with rafa before repairing it");
7559
+ }
7560
+ }
7561
+ }
7562
+ function checkedCounts(db, tables, original) {
7563
+ return tables.map((table) => {
7564
+ const rows = count(db, `SELECT count(*) AS n FROM main.${quoted(table)}`);
7565
+ const originalRows = count(db, `SELECT count(*) AS n FROM ${original}.${quoted(table)}`);
7566
+ if (rows !== originalRows) {
7567
+ throw new RebuildRefusal(`the rebuilt ${table} holds ${String(rows)} rows, the store ${String(originalRows)}`);
7568
+ }
7569
+ return { table, rows };
7570
+ });
7571
+ }
7572
+ function refuseCorrupt(db) {
7573
+ const answer = db.query("PRAGMA main.integrity_check").get()?.integrity_check;
7574
+ if (answer !== "ok")
7575
+ throw new RebuildRefusal(`the rebuilt store failed integrity_check: ${answer ?? "no answer"}`);
7576
+ }
7577
+ function removeParallel(parallelPath) {
7578
+ rmSync3(parallelPath, { force: true });
7579
+ rmSync3(`${parallelPath}-journal`, { force: true });
7580
+ }
7581
+ function swapIn(path, parallelPath, backupPath) {
7582
+ renameSync(path, backupPath);
7583
+ try {
7584
+ renameSync(parallelPath, path);
7585
+ } catch (error) {
7586
+ renameSync(backupPath, path);
7587
+ throw error;
7588
+ }
7589
+ }
7590
+ function refuseTaken(paths) {
7591
+ for (const taken of paths) {
7592
+ if (existsSync6(taken))
7593
+ throw new RebuildRefusal(`${taken} is already there; move it aside or pass another stamp`);
7594
+ }
7595
+ }
7596
+ function rebuildAside(options) {
7597
+ const { path, parallelPath, backupPath, dryRun, build } = options;
7598
+ refuseInFlight(path);
7599
+ refuseTaken([parallelPath, backupPath]);
7600
+ let built;
7601
+ try {
7602
+ built = build(parallelPath);
7603
+ } catch (error) {
7604
+ removeParallel(parallelPath);
7605
+ throw error;
7606
+ }
7607
+ if (dryRun) {
7608
+ removeParallel(parallelPath);
7609
+ return { built, backupPath: null };
7610
+ }
7611
+ swapIn(path, parallelPath, backupPath);
7612
+ return { built, backupPath };
7613
+ }
7614
+
7615
+ // src/effort/store/fix-schema.ts
7616
+ var REPAIRABLE_REASONS = [
7617
+ "pre-log-unreleased",
7618
+ "gate-mismatch",
7619
+ "edited",
7620
+ "unknown-breaks-readers",
7621
+ "unknown-breaks-writers"
7622
+ ];
7623
+ var LIVE = "live";
7365
7624
  function tableNames(db, schema) {
7366
- return db.query(`SELECT name FROM ${schema}.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`).all().map((row2) => row2.name);
7625
+ return db.query(`SELECT name FROM ${schema}.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ? ORDER BY name`).all(MIGRATION_LOG_TABLE).map((row2) => row2.name);
7367
7626
  }
7368
7627
  function columnNames(db, schema, table) {
7369
7628
  return db.query("SELECT name FROM pragma_table_info(?, ?)").all(table, schema).map((row2) => row2.name);
7370
7629
  }
7371
- function count(db, sql) {
7372
- return db.query(sql).get()?.n ?? 0;
7373
- }
7374
- function versionOf(path) {
7375
- const db = new Database(path, { readonly: true });
7630
+ function readOnlySchema(path) {
7631
+ const db = new Database2(path, { readonly: true });
7376
7632
  try {
7377
- return db.query("PRAGMA user_version").get()?.user_version ?? 0;
7633
+ return readStoreSchema(db, path);
7378
7634
  } finally {
7379
7635
  db.close();
7380
7636
  }
7381
7637
  }
7382
- function refuseInFlight(path) {
7383
- for (const suffix of ["-journal", "-wal"]) {
7384
- if (existsSync5(`${path}${suffix}`)) {
7385
- throw new SchemaFixRefusal(`${path}${suffix} is there, so a write is in flight or was interrupted; stop whatever uses the store` + " and open it once with rafa before repairing it");
7386
- }
7387
- }
7388
- }
7389
7638
  function refuseRemoved(db, known, live) {
7390
7639
  for (const table of known) {
7391
7640
  if (!live.has(table)) {
7392
- throw new SchemaFixRefusal(`the store holds no table ${table}, which this rafa writes; its newer schema is not additive`);
7641
+ throw new RebuildRefusal(`the store holds no table ${table}, which this rafa writes; its newer schema is not additive`);
7393
7642
  }
7394
7643
  const liveColumns = new Set(columnNames(db, LIVE, table));
7395
7644
  const missing = columnNames(db, "main", table).filter((column) => !liveColumns.has(column));
7396
7645
  if (missing.length > 0) {
7397
- throw new SchemaFixRefusal(`the store holds no column ${missing.map((column) => `${table}.${column}`).join(", ")}, which this rafa` + " writes; its newer schema is not additive");
7646
+ throw new RebuildRefusal(`the store holds no column ${missing.map((column) => `${table}.${column}`).join(", ")}, which this rafa` + " writes; its newer schema is not additive");
7398
7647
  }
7399
7648
  }
7400
7649
  }
@@ -7406,16 +7655,6 @@ function copyKnown(db, known) {
7406
7655
  }
7407
7656
  })();
7408
7657
  }
7409
- function checkedCounts(db, known) {
7410
- return known.map((table) => {
7411
- const rows = count(db, `SELECT count(*) AS n FROM main.${quoted(table)}`);
7412
- const liveRows = count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)}`);
7413
- if (rows !== liveRows) {
7414
- throw new SchemaFixRefusal(`the rebuilt ${table} holds ${String(rows)} rows, the store ${String(liveRows)}`);
7415
- }
7416
- return { table, rows };
7417
- });
7418
- }
7419
7658
  function leftBehind(db, known, live) {
7420
7659
  const leftTables = live.filter((table) => !known.has(table)).map((table) => ({ table, rows: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)}`) }));
7421
7660
  const leftColumns = live.filter((table) => known.has(table)).flatMap((table) => {
@@ -7428,78 +7667,91 @@ function leftBehind(db, known, live) {
7428
7667
  });
7429
7668
  return { leftTables, leftColumns };
7430
7669
  }
7431
- function refuseCorrupt(db) {
7432
- const answer = db.query("PRAGMA main.integrity_check").get()?.integrity_check;
7433
- if (answer !== "ok")
7434
- throw new SchemaFixRefusal(`the rebuilt store failed integrity_check: ${answer ?? "no answer"}`);
7670
+ function refuseUnusable(db, parallelPath, migrations) {
7671
+ const plan2 = planSchema(readStoreSchema(db, parallelPath), sqliteCatalogue(migrations), "write", "open");
7672
+ if (plan2.verdict === "refuse")
7673
+ throw new RebuildRefusal(`the rebuilt store is refused as ${plan2.reason}: ${plan2.message}`);
7674
+ const needs = writeNames(plan2);
7675
+ if (needs.length > 0)
7676
+ throw new RebuildRefusal(`the rebuilt store still needs ${needs.join(", ")}`);
7435
7677
  }
7436
- function buildParallel(path, parallelPath, migrations) {
7437
- const db = new Database(parallelPath, { create: true, readwrite: true });
7678
+ function buildParallel(path, parallelPath, migrations, stamp) {
7679
+ const db = new Database2(parallelPath, { create: true, readwrite: true });
7438
7680
  try {
7439
- migrateSchema(db, parallelPath, migrations);
7681
+ bringForward(db, parallelPath, "write", "open", { migrations, ...stamp, builtAside: true });
7440
7682
  db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
7441
7683
  const known = tableNames(db, "main");
7442
7684
  const live = tableNames(db, LIVE);
7443
7685
  refuseRemoved(db, known, new Set(live));
7444
7686
  copyKnown(db, known);
7445
- const kept = checkedCounts(db, known);
7687
+ const kept = checkedCounts(db, known, LIVE);
7446
7688
  const left = leftBehind(db, new Set(known), live);
7447
7689
  db.run(`DETACH DATABASE ${LIVE}`);
7448
7690
  refuseCorrupt(db);
7691
+ refuseUnusable(db, parallelPath, migrations);
7449
7692
  return { kept, ...left };
7450
7693
  } finally {
7451
7694
  db.close();
7452
7695
  }
7453
7696
  }
7454
- function removeParallel(parallelPath) {
7455
- rmSync2(parallelPath, { force: true });
7456
- rmSync2(`${parallelPath}-journal`, { force: true });
7697
+ function asidePaths(path, version2, stamp) {
7698
+ return { parallelPath: `${path}.fix-${stamp}`, backupPath: `${path}.v${String(version2)}-${stamp}.bak` };
7457
7699
  }
7458
- function swapIn(path, parallelPath, backupPath) {
7459
- renameSync(path, backupPath);
7460
- try {
7461
- renameSync(parallelPath, path);
7462
- } catch (error) {
7463
- renameSync(backupPath, path);
7464
- throw error;
7465
- }
7700
+ function isRepairable(reason) {
7701
+ return REPAIRABLE_REASONS.includes(reason);
7466
7702
  }
7467
- function freshPaths(path, version2, stamp) {
7468
- const parallelPath = `${path}.fix-${stamp}`;
7469
- const backupPath = `${path}.v${String(version2)}-${stamp}.bak`;
7470
- for (const taken of [parallelPath, backupPath]) {
7471
- if (existsSync5(taken))
7472
- throw new SchemaFixRefusal(`${taken} is already there; move it aside or pass another stamp`);
7473
- }
7474
- return { parallelPath, backupPath };
7703
+ function refuseDevelopmentSwap(path, reason, identity) {
7704
+ if (identity.kind !== "development")
7705
+ return;
7706
+ throw new RebuildRefusal(`${path} needs a rebuild (${reason}), and this rafa is a development build (${identity.checkout}), whose` + " migrations may be a branch's; a development build never swaps a rebuild in. Nothing was changed, and" + " --dry-run runs from here. Run the swap from an installed rafa. Next safe step: rafa effort fix-schema");
7475
7707
  }
7476
7708
  function fixStoreSchema(options) {
7477
7709
  const { path, dryRun, stamp } = options;
7710
+ guardTestProcess(path);
7478
7711
  const migrations = options.migrations ?? SQLITE_MIGRATIONS;
7479
- const knownVersion = migrations.length;
7480
- const nothing = { kept: [], leftTables: [], leftColumns: [], backupPath: null };
7481
- if (!existsSync5(path))
7482
- return { path, status: "missing", storeVersion: null, knownVersion, ...nothing };
7712
+ const identity = options.identity ?? readRuntimeIdentity();
7713
+ const appliedBy = appliedByName(identity);
7714
+ const known = migrations.map(({ id: id2 }) => id2);
7715
+ const nothing = { known, appliedBy, pending: [], kept: [], leftTables: [], leftColumns: [], backupPath: null };
7716
+ if (!existsSync7(path)) {
7717
+ return { path, status: "missing", storeVersion: null, logged: false, reason: null, refusal: null, unknown: [], ...nothing };
7718
+ }
7483
7719
  refuseInFlight(path);
7484
- const storeVersion = versionOf(path);
7485
- if (storeVersion === knownVersion)
7486
- return { path, status: "current", storeVersion, knownVersion, ...nothing };
7487
- if (storeVersion < knownVersion)
7488
- return { path, status: "behind", storeVersion, knownVersion, ...nothing };
7489
- const { parallelPath, backupPath } = freshPaths(path, storeVersion, stamp);
7490
- let built;
7491
- try {
7492
- built = buildParallel(path, parallelPath, migrations);
7493
- } catch (error) {
7494
- removeParallel(parallelPath);
7495
- throw error;
7720
+ const store = readOnlySchema(path);
7721
+ const knownIds = new Set(known);
7722
+ const found = {
7723
+ path,
7724
+ storeVersion: store.userVersion,
7725
+ logged: store.log !== null,
7726
+ unknown: (store.log ?? []).filter(({ id: id2 }) => !knownIds.has(id2))
7727
+ };
7728
+ const plan2 = planSchema(store, sqliteCatalogue(migrations), "write", "open");
7729
+ if (plan2.verdict === "use") {
7730
+ const pending = writeNames(plan2);
7731
+ const status2 = pending.length === 0 ? "current" : "behind";
7732
+ return { ...found, ...nothing, status: status2, reason: null, refusal: null, pending };
7496
7733
  }
7497
- if (dryRun) {
7498
- removeParallel(parallelPath);
7499
- return { path, status: "would-rebuild", storeVersion, knownVersion, ...built, backupPath: null };
7734
+ if (!isRepairable(plan2.reason)) {
7735
+ throw new RebuildRefusal(`a rebuild at this rafa's migrations does not repair ${plan2.reason}. ${plan2.message}`);
7500
7736
  }
7501
- swapIn(path, parallelPath, backupPath);
7502
- return { path, status: "rebuilt", storeVersion, knownVersion, ...built, backupPath };
7737
+ if (!dryRun)
7738
+ refuseDevelopmentSwap(path, plan2.reason, identity);
7739
+ const { built, backupPath } = rebuildAside({
7740
+ path,
7741
+ ...asidePaths(path, store.userVersion, stamp),
7742
+ dryRun,
7743
+ build: (parallelPath) => buildParallel(path, parallelPath, migrations, {
7744
+ appliedBy,
7745
+ now: options.now ?? (() => new Date)
7746
+ })
7747
+ });
7748
+ const status = dryRun ? "would-rebuild" : "rebuilt";
7749
+ const refusal4 = withoutWayOut(plan2.message, plan2.nextStep);
7750
+ return { ...found, ...nothing, status, reason: plan2.reason, refusal: refusal4, ...built, backupPath };
7751
+ }
7752
+ function withoutWayOut(message, nextStep) {
7753
+ const ending = ` Next safe step: ${nextStep}`;
7754
+ return message.endsWith(ending) ? message.slice(0, -ending.length) : message;
7503
7755
  }
7504
7756
 
7505
7757
  // src/commands/effort/fix-schema.ts
@@ -7521,102 +7773,401 @@ function refuseLiveLoop(root, isAlive) {
7521
7773
  return;
7522
7774
  throw new CommandExit(1, `\u274C ${COMMAND_NAME5}: loop session ${live.sessionId} on ${live.branch} is ${live.state} and writes the store;` + ` stop it with \`rafa loop stop -s ${live.sessionId}\` first. \`--dry-run\` runs beside it.`);
7523
7775
  }
7776
+ function migrationNames(result) {
7777
+ return result.unknown.map(({ id: id2, appliedBy }) => `${id2} (applied by ${appliedBy})`).join(", ");
7778
+ }
7524
7779
  function rebuildLines(result) {
7525
7780
  const rows = result.kept.reduce((sum, table) => sum + table.rows, 0);
7526
7781
  const kept = result.kept.map((table) => `${table.table} ${String(table.rows)}`).join(", ");
7527
7782
  const left = [
7783
+ ...result.unknown.map(({ id: id2, appliedBy }) => `migration ${id2} (applied by ${appliedBy})`),
7528
7784
  ...result.leftTables.map((table) => `table ${table.table} (${String(table.rows)} rows)`),
7529
7785
  ...result.leftColumns.map((column) => `column ${column.table}.${column.column} (${String(column.values)} values)`)
7530
7786
  ];
7787
+ const newer = result.unknown.length === 0 ? [] : [`A rafa that knows ${result.unknown.map(({ id: id2 }) => id2).join(", ")} uses the store as it is and keeps that data` + " in it; install one instead where you can."];
7788
+ return [
7789
+ `Refused as ${String(result.reason)}: ${String(result.refusal)}`,
7790
+ `Rebuilds it at the ${String(result.known.length)} migrations this rafa knows, logged as applied by ${result.appliedBy}.`,
7791
+ `Keeps ${String(result.kept.length)} tables, ${String(rows)} rows: ${kept}.`,
7792
+ left.length === 0 ? "Leaves nothing behind." : `Leaves behind, kept only in the backup: ${left.join("; ")}.`,
7793
+ ...newer
7794
+ ];
7795
+ }
7796
+ function unknownAdditiveLines(result) {
7797
+ return result.unknown.length === 0 ? [] : [`It logs ${migrationNames(result)}, which this rafa does not know; each is additive, so this rafa uses the store` + " through them."];
7798
+ }
7799
+ function renderFixSchema(result) {
7800
+ switch (result.status) {
7801
+ case "missing":
7802
+ return [`No effort store at ${result.path}: nothing to repair.`];
7803
+ case "current":
7804
+ return [
7805
+ `\u2705 ${result.path} is current: this rafa reads and writes it as it is. Nothing to repair.`,
7806
+ ...unknownAdditiveLines(result)
7807
+ ];
7808
+ case "behind":
7809
+ return [
7810
+ `${result.path} is behind this rafa's migrations; the next command that opens it records` + ` ${result.pending.join(", ")}. Nothing to repair.`,
7811
+ ...unknownAdditiveLines(result)
7812
+ ];
7813
+ case "would-rebuild":
7814
+ return [
7815
+ ...rebuildLines(result),
7816
+ "\uD83D\uDD0D Dry run: the rebuild was built, checked (row counts, integrity_check, this rafa's schema plan) and" + " deleted. Run without `--dry-run` to swap it in."
7817
+ ];
7818
+ case "rebuilt":
7819
+ return [
7820
+ ...rebuildLines(result),
7821
+ `\u2705 Rebuilt at this rafa's migrations. The original is kept whole at ${String(result.backupPath)};` + " rename it back to undo."
7822
+ ];
7823
+ }
7824
+ }
7825
+ function runFixSchema(context, seams) {
7826
+ expectNoArgument(context.args, FIX_SCHEMA_USAGE);
7827
+ const dryRun = readSwitch(DRY_RUN_FLAG2, context.flags[DRY_RUN_FLAG2], `Usage: ${FIX_SCHEMA_USAGE}`);
7828
+ if (context.project === null)
7829
+ throw new Error(`${COMMAND_NAME5} runs inside a project, and was handed none`);
7830
+ const root = context.project.root;
7831
+ if (!dryRun)
7832
+ refuseLiveLoop(root, seams.isAlive);
7833
+ const now = seams.now ?? (() => new Date);
7834
+ let result;
7835
+ try {
7836
+ result = fixStoreSchema({
7837
+ path: sqliteStorePath(root),
7838
+ dryRun,
7839
+ stamp: fileStamp(now()),
7840
+ now,
7841
+ ...seams.identity === undefined ? {} : { identity: seams.identity }
7842
+ });
7843
+ } catch (error) {
7844
+ if (error instanceof RebuildRefusal)
7845
+ throw new CommandExit(1, `\u274C ${COMMAND_NAME5}: ${error.message}`);
7846
+ throw error;
7847
+ }
7848
+ if (context.outputMode === "json") {
7849
+ context.output.result(result);
7850
+ return;
7851
+ }
7852
+ for (const line of renderFixSchema(result))
7853
+ context.output.info(line);
7854
+ }
7855
+ function createFixSchemaCommand(seams = {}) {
7856
+ const command = {
7857
+ name: "effort fix-schema",
7858
+ subject: "effort",
7859
+ action: "fix-schema",
7860
+ summary: "rebuild an effort store this rafa refuses at the migrations it knows, keeping the original",
7861
+ description: "Repairs `.rafa/effort/effort.sqlite`, or the store under `RAFA_EFFORT_DIR`, when this rafa refuses" + " it for a reason a rebuild repairs: no migration log past the legacy entries (pre-log-unreleased), a log" + " with a schema version it does not match (gate-mismatch), a logged checksum this rafa holds otherwise" + " (edited), or a logged migration this rafa does not know that breaks older readers or writers" + " (unknown-breaks-readers, unknown-breaks-writers). Installing a rafa that knows the store's migrations" + " comes first where one exists. It builds a parallel store beside it at this rafa's migrations, with a" + " migration log naming this rafa, copies every table and column this rafa knows, checks the row counts," + " SQLite's integrity_check and the schema plan, and lists what only the newer schema holds, unknown" + " migrations included. Then it renames the original to `effort.sqlite.v<user_version>-<stamp>.bak`," + " whole, and moves the rebuild into its place. A store this rafa uses, current or behind, or no store, is" + " left alone. It refuses, changing nothing, while a loop session under the project is running or paused," + " from a development build, while a journal beside the store shows a write in flight, when the newer" + " schema lacks a table or column this rafa writes, and on a refusal a rebuild does not repair. With" + " `--output=json` the outcome is the data of the terminal result event. Starts no session.",
7862
+ args: [],
7863
+ flags: [
7864
+ {
7865
+ name: DRY_RUN_FLAG2,
7866
+ description: "Build and check the rebuild, print what it keeps and leaves behind, then delete it. The" + " live store is only read, so this runs beside a live loop and from a development build, and can be" + " repeated.",
7867
+ type: "boolean"
7868
+ }
7869
+ ],
7870
+ examples: [
7871
+ {
7872
+ cmd: "rafa effort fix-schema --dry-run",
7873
+ note: "Prints what a rebuild would keep and leave behind, changing nothing."
7874
+ },
7875
+ {
7876
+ cmd: "rafa effort fix-schema",
7877
+ note: "Swaps the rebuild in and prints where the original was kept."
7878
+ }
7879
+ ],
7880
+ outputs: ["text", "json"],
7881
+ run: async (context) => {
7882
+ runFixSchema(context, seams);
7883
+ await Promise.resolve();
7884
+ }
7885
+ };
7886
+ return Object.freeze(command);
7887
+ }
7888
+ var fix_schema_default = createFixSchemaCommand();
7889
+
7890
+ // src/commands/effort/copy.ts
7891
+ var COMMAND_NAME6 = "rafa effort copy";
7892
+ var COPY_USAGE = "rafa effort copy [--to=<dir>]";
7893
+ var TO_FLAG = "to";
7894
+ var REFUSED_EXIT = 1;
7895
+ var FAILED_EXIT = 2;
7896
+ var SCRATCH_DIR = join19(".rafa", "scratch");
7897
+ function effortDirLine(directory) {
7898
+ return `${EFFORT_DIR_VARIABLE}=${directory}`;
7899
+ }
7900
+ function readTarget(context, root) {
7901
+ const value = context.flags[TO_FLAG];
7902
+ if (value === undefined)
7903
+ return null;
7904
+ if (typeof value !== "string" || value === "") {
7905
+ throw new CommandExit(REFUSED_EXIT, `\u274C --${TO_FLAG} takes a directory: --${TO_FLAG}=<dir>
7906
+ Usage: ${COPY_USAGE}`);
7907
+ }
7908
+ return resolve7(root, value);
7909
+ }
7910
+ function refuseEffortDirOverride(context) {
7911
+ const value = context.env[EFFORT_DIR_VARIABLE];
7912
+ if (value === undefined || value === "")
7913
+ return;
7914
+ throw new CommandExit(REFUSED_EXIT, `\u274C ${COMMAND_NAME6}: ${EFFORT_DIR_VARIABLE} is set (${value}); a copy is taken from the project's own store.` + ` Nothing was copied. Unset it and run again: ${EFFORT_DIR_VARIABLE}= ${COMMAND_NAME6}`);
7915
+ }
7916
+ function renderCopy(result) {
7917
+ return [
7918
+ `\u2705 Copied ${result.files.join(", ")} from ${result.source} to ${result.directory}.`,
7919
+ "Put this in front of each command that should use the copy, on the same line:",
7920
+ effortDirLine(result.directory)
7921
+ ];
7922
+ }
7923
+ function runCopy(context, seams) {
7924
+ expectNoArgument(context.args, COPY_USAGE);
7925
+ const root = requireProject(context, COMMAND_NAME6).root;
7926
+ const named = readTarget(context, root);
7927
+ refuseEffortDirOverride(context);
7928
+ const target = named ?? join19(root, SCRATCH_DIR, `effort-${fileStamp((seams.now ?? (() => new Date))())}`);
7929
+ let result;
7930
+ try {
7931
+ result = copyEffortStore({ source: join19(root, EFFORT_STORE_DIR), target });
7932
+ } catch (error) {
7933
+ if (error instanceof EffortCopyRefusal)
7934
+ throw new CommandExit(REFUSED_EXIT, `\u274C ${COMMAND_NAME6}: ${error.message}`);
7935
+ if (error instanceof EffortCopyFailure)
7936
+ throw new CommandExit(FAILED_EXIT, `\u274C ${COMMAND_NAME6}: ${error.message}`);
7937
+ throw error;
7938
+ }
7939
+ if (context.outputMode === "json") {
7940
+ context.output.result({ ...result, env: effortDirLine(result.directory) });
7941
+ return;
7942
+ }
7943
+ for (const line of renderCopy(result))
7944
+ context.output.info(line);
7945
+ }
7946
+ function createCopyCommand(seams = {}) {
7947
+ const command = {
7948
+ name: "effort copy",
7949
+ subject: "effort",
7950
+ action: "copy",
7951
+ summary: "copy the effort store into a scratch directory and print the RAFA_EFFORT_DIR= line that points a command at it",
7952
+ description: "Copies `.rafa/effort/` into a directory of its own: the SQLite file through `VACUUM INTO`, which only" + " reads the store and keeps its schema version and migration log, and the NDJSON files by file copy. The" + " copy lands in `--to`, or in `.rafa/scratch/effort-<stamp>/`. It prints the directory and the" + " `RAFA_EFFORT_DIR=` line to put in front of each command that should run over the copy instead of the" + " live store. It plans no schema, so a development build may run it and a running loop is not disturbed." + " Exit code 1, making nothing, when `RAFA_EFFORT_DIR` is set, when there is no store, or when the target" + " is not empty; exit code 2 when a read or write failed, with what it made removed. With `--output=json`" + " the copy is the data of the terminal result event. Starts no session.",
7953
+ args: [],
7954
+ flags: [
7955
+ {
7956
+ name: TO_FLAG,
7957
+ description: "The directory to copy into, absent or empty; a relative one is read from the project root." + " Defaults to `.rafa/scratch/effort-<stamp>/`.",
7958
+ type: "string"
7959
+ }
7960
+ ],
7961
+ examples: [
7962
+ {
7963
+ cmd: "rafa effort copy",
7964
+ note: "Copies the store into `.rafa/scratch/effort-<stamp>/` and prints the `RAFA_EFFORT_DIR=` line."
7965
+ },
7966
+ {
7967
+ cmd: "rafa effort copy --to=.rafa/scratch/rafa-234-effort",
7968
+ note: "Copies the store into a directory a plan names."
7969
+ }
7970
+ ],
7971
+ outputs: ["text", "json"],
7972
+ run: async (context) => {
7973
+ runCopy(context, seams);
7974
+ await Promise.resolve();
7975
+ }
7976
+ };
7977
+ return Object.freeze(command);
7978
+ }
7979
+ var copy_default = createCopyCommand();
7980
+
7981
+ // src/commands/effort/migrate.ts
7982
+ import { join as join20 } from "path";
7983
+
7984
+ // src/effort/store/migrate.ts
7985
+ import { existsSync as existsSync8 } from "fs";
7986
+ import { Database as Database3 } from "bun:sqlite";
7987
+ var LIVE2 = "live";
7988
+ function asidePaths2(path, first, stamp) {
7989
+ return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
7990
+ }
7991
+ function liveLoop(path, options) {
7992
+ const root = storeProjectRoot(path);
7993
+ if (root === null)
7994
+ return;
7995
+ let records;
7996
+ try {
7997
+ records = readSessions(root, { isAlive: options.isAlive ?? isPidAlive });
7998
+ } catch (error) {
7999
+ throw new RebuildRefusal(`the loop's run records cannot be read, so a live loop on ${path} cannot be ruled out` + ` (${messageOf(error)}). Nothing was migrated.`);
8000
+ }
8001
+ return records.find((record) => record.state === "running" || record.state === "paused");
8002
+ }
8003
+ function refuseLiveLoop2(path, breaking, options) {
8004
+ const loop = liveLoop(path, options);
8005
+ if (loop === undefined)
8006
+ return;
8007
+ const named = `loop ${loop.sessionId} (pid ${String(loop.pid)}, plan ${loop.planStub ?? loop.plan}) is running on this store`;
8008
+ const [first] = breaking;
8009
+ const opening = first === undefined ? `REFUSED \u2014 ${named}, and swapping the migrated store in under it would lose what it writes.` : `REFUSED \u2014 migration ${first} breaks older runtimes, and ${named}.`;
8010
+ throw new RebuildRefusal(`${opening} Nothing was migrated. Finish or stop that loop, then run it again.`);
8011
+ }
8012
+ function buildMigrated(path, parallelPath, migrations, stamp) {
8013
+ vacuumInto(path, parallelPath);
8014
+ const db = new Database3(parallelPath, { readwrite: true });
8015
+ try {
8016
+ const brought = bringForward(db, parallelPath, "write", "migrate", { migrations, ...stamp, builtAside: true });
8017
+ db.run(`ATTACH DATABASE ? AS ${LIVE2}`, [path]);
8018
+ const live = new Set(tableNames(db, LIVE2));
8019
+ const counted2 = checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE2);
8020
+ db.run(`DETACH DATABASE ${LIVE2}`);
8021
+ refuseCorrupt(db);
8022
+ refuseUnusable(db, parallelPath, migrations);
8023
+ return { userVersion: brought.userVersion, counted: counted2 };
8024
+ } finally {
8025
+ db.close();
8026
+ }
8027
+ }
8028
+ function migrateStore(options) {
8029
+ const { path, dryRun, stamp } = options;
8030
+ guardTestProcess(path);
8031
+ const migrations = options.migrations ?? SQLITE_MIGRATIONS;
8032
+ const appliedBy = appliedByName(options.identity ?? readRuntimeIdentity());
8033
+ const nothing = { applying: [], breaking: [], unknown: [], userVersion: null, counted: [], backupPath: null };
8034
+ if (!existsSync8(path)) {
8035
+ return { path, status: "missing", storeVersion: null, logged: false, appliedBy, ...nothing };
8036
+ }
8037
+ refuseInFlight(path);
8038
+ const store = readOnlySchema(path);
8039
+ const found = { path, storeVersion: store.userVersion, logged: store.log !== null, appliedBy };
8040
+ const plan2 = planSchema(store, sqliteCatalogue(migrations), "write", "migrate");
8041
+ if (plan2.verdict === "refuse")
8042
+ throw new RebuildRefusal(plan2.message);
8043
+ const applying = writeNames(plan2);
8044
+ const planned = {
8045
+ applying,
8046
+ breaking: plan2.pending.filter(({ breaks }) => breaks.length > 0).map(({ id: id2 }) => id2),
8047
+ unknown: plan2.unknown
8048
+ };
8049
+ if (applying.length === 0)
8050
+ return { ...found, ...nothing, ...planned, status: "current" };
8051
+ if (!dryRun) {
8052
+ refuseUnownedDevelopmentWrite(path, applying, options);
8053
+ refuseLiveLoop2(path, planned.breaking, options);
8054
+ }
8055
+ const first = plan2.pending[0]?.id ?? ADOPTION_NAME;
8056
+ const { built, backupPath } = rebuildAside({
8057
+ path,
8058
+ ...asidePaths2(path, first, stamp),
8059
+ dryRun,
8060
+ build: (parallelPath) => buildMigrated(path, parallelPath, migrations, {
8061
+ appliedBy,
8062
+ now: options.now ?? (() => new Date)
8063
+ })
8064
+ });
8065
+ const status = dryRun ? "would-migrate" : "migrated";
8066
+ return { ...found, ...planned, ...built, status, backupPath };
8067
+ }
8068
+
8069
+ // src/commands/effort/migrate.ts
8070
+ var COMMAND_NAME7 = "rafa effort migrate";
8071
+ var MIGRATE_USAGE = "rafa effort migrate [--dry-run]";
8072
+ var DRY_RUN_FLAG3 = "dry-run";
8073
+ var REFUSED_EXIT2 = 1;
8074
+ function builtLines(result) {
8075
+ const rows = result.counted.reduce((sum, table) => sum + table.rows, 0);
8076
+ const breaking = result.breaking.length === 0 ? "Every one is additive." : `${result.breaking.join(", ")} breaks older runtimes: a rafa that does not know it refuses the store once it runs.`;
7531
8077
  return [
7532
- `${result.path} is at schema version ${String(result.storeVersion)}, past the ${String(result.knownVersion)} this rafa knows.`,
7533
- `Keeps ${String(result.kept.length)} tables, ${String(rows)} rows: ${kept}.`,
7534
- left.length === 0 ? "Leaves nothing behind." : `Leaves behind, kept only in the backup: ${left.join("; ")}.`
8078
+ `Applies ${result.applying.join(", ")} to ${result.path}, logged as applied by ${result.appliedBy}. ${breaking}`,
8079
+ `Row counts match in ${String(result.counted.length)} tables, ${String(rows)} rows; integrity_check ok;` + ` schema version ${String(result.userVersion)} after.`
7535
8080
  ];
7536
8081
  }
7537
- function renderFixSchema(result) {
8082
+ function renderMigrate(result) {
7538
8083
  switch (result.status) {
7539
8084
  case "missing":
7540
- return [`No effort store at ${result.path}: nothing to repair.`];
8085
+ return [`No effort store at ${result.path}: nothing to migrate.`];
7541
8086
  case "current":
7542
- return [`\u2705 ${result.path} is at schema version ${String(result.knownVersion)}, the one this rafa knows: nothing to repair.`];
7543
- case "behind":
7544
- return [
7545
- `${result.path} is at schema version ${String(result.storeVersion)}, behind the ${String(result.knownVersion)} this rafa` + " knows; the next command that opens it migrates it. Nothing to repair."
7546
- ];
7547
- case "would-rebuild":
8087
+ return [`\u2705 ${result.path} is current: no migration is pending. Nothing to migrate.`];
8088
+ case "would-migrate":
7548
8089
  return [
7549
- ...rebuildLines(result),
7550
- `\uD83D\uDD0D Dry run: the rebuild at version ${String(result.knownVersion)} was built, checked (row counts, integrity_check)` + " and deleted. Run without `--dry-run` to swap it in."
8090
+ ...builtLines(result),
8091
+ "\uD83D\uDD0D Dry run: the migrated store was built beside it, checked (row counts, integrity_check, this rafa's" + " schema plan) and deleted. Run `rafa effort migrate` to swap it in."
7551
8092
  ];
7552
- case "rebuilt":
8093
+ case "migrated":
7553
8094
  return [
7554
- ...rebuildLines(result),
7555
- `\u2705 Rebuilt at version ${String(result.knownVersion)}. The original is kept whole at ${String(result.backupPath)};` + " rename it back to undo."
8095
+ ...builtLines(result),
8096
+ `\u2705 Migrated. The original is kept whole at ${String(result.backupPath)}; rename it back to undo.`
7556
8097
  ];
7557
8098
  }
7558
8099
  }
7559
- function runFixSchema(context, seams) {
7560
- expectNoArgument(context.args, FIX_SCHEMA_USAGE);
7561
- const dryRun = readSwitch(DRY_RUN_FLAG2, context.flags[DRY_RUN_FLAG2], `Usage: ${FIX_SCHEMA_USAGE}`);
7562
- if (context.project === null)
7563
- throw new Error(`${COMMAND_NAME5} runs inside a project, and was handed none`);
7564
- const root = context.project.root;
7565
- if (!dryRun)
7566
- refuseLiveLoop(root, seams.isAlive);
8100
+ function storePath(context, root) {
8101
+ try {
8102
+ return join20(effortStoreDir(root, context.env), SQLITE_STORE_FILE_NAME);
8103
+ } catch (error) {
8104
+ throw new CommandExit(REFUSED_EXIT2, `\u274C ${COMMAND_NAME7}: ${messageOf(error)}`);
8105
+ }
8106
+ }
8107
+ function runMigrate(context, seams) {
8108
+ expectNoArgument(context.args, MIGRATE_USAGE);
8109
+ const dryRun = readSwitch(DRY_RUN_FLAG3, context.flags[DRY_RUN_FLAG3], `Usage: ${MIGRATE_USAGE}`);
8110
+ const path = storePath(context, requireProject(context, COMMAND_NAME7).root);
8111
+ const now = seams.now ?? (() => new Date);
7567
8112
  let result;
7568
8113
  try {
7569
- result = fixStoreSchema({
7570
- path: sqliteStorePath(root),
8114
+ result = migrateStore({
8115
+ path,
7571
8116
  dryRun,
7572
- stamp: fileStamp((seams.now ?? (() => new Date))())
8117
+ stamp: fileStamp(now()),
8118
+ now,
8119
+ env: context.env,
8120
+ ...seams.identity === undefined ? {} : { identity: seams.identity },
8121
+ ...seams.isAlive === undefined ? {} : { isAlive: seams.isAlive },
8122
+ ...seams.migrations === undefined ? {} : { migrations: seams.migrations }
7573
8123
  });
7574
8124
  } catch (error) {
7575
- if (error instanceof SchemaFixRefusal)
7576
- throw new CommandExit(1, `\u274C ${COMMAND_NAME5}: ${error.message}`);
8125
+ if (error instanceof RebuildRefusal || error instanceof DevelopmentBuildRefusedError) {
8126
+ throw new CommandExit(REFUSED_EXIT2, `\u274C ${COMMAND_NAME7}: ${error.message}`);
8127
+ }
7577
8128
  throw error;
7578
8129
  }
7579
8130
  if (context.outputMode === "json") {
7580
8131
  context.output.result(result);
7581
8132
  return;
7582
8133
  }
7583
- for (const line of renderFixSchema(result))
8134
+ for (const line of renderMigrate(result))
7584
8135
  context.output.info(line);
7585
8136
  }
7586
- function createFixSchemaCommand(seams = {}) {
8137
+ function createMigrateCommand(seams = {}) {
7587
8138
  const command = {
7588
- name: "effort fix-schema",
8139
+ name: "effort migrate",
7589
8140
  subject: "effort",
7590
- action: "fix-schema",
7591
- summary: "rebuild an effort store a newer rafa migrated at the version this rafa knows, keeping the original",
7592
- description: "Repairs `.rafa/effort/effort.sqlite` when it is past the schema version this rafa knows, which" + " happens when newer code opens it, such as a plan's own branch code run while an older installed rafa" + " drives the loop. It builds a parallel store beside it at this rafa's version, copies every table and" + " column this rafa knows, checks the row counts and SQLite's integrity_check, and lists what only the" + " newer schema holds. Then it renames the original to `effort.sqlite.v<version>-<stamp>.bak`, whole, and" + " moves the rebuild into its place. A store at or behind this version, or no store, is left alone. It" + " refuses, changing nothing, while a loop session under the project is running or paused, while a" + " journal beside the store shows a write in flight, and when the newer schema lacks a table or column" + " this rafa writes. With `--output=json` the outcome is the data of the terminal result event. Starts no" + " session.",
8141
+ action: "migrate",
8142
+ summary: "apply the effort store's pending migrations, one that breaks older runtimes included, keeping the original",
8143
+ description: "Brings `.rafa/effort/effort.sqlite`, or the store under `RAFA_EFFORT_DIR`, to the migrations this" + " rafa knows, including one that breaks older runtimes, which no ordinary open applies. It writes the" + " store to `effort.sqlite.migrate-<stamp>` beside it with `VACUUM INTO`, applies the pending migrations" + " there with log rows naming this rafa, checks the row count of every table against the store," + " SQLite's integrity_check and the schema plan, then renames the original to" + " `effort.sqlite.before-<id>-<stamp>.bak`, whole, and moves the migrated file into its place. A store" + " with nothing pending, or no store, is left alone. It refuses, changing nothing, while a loop session" + " under the project is running or paused, from a development build over any store but a copy under" + " `RAFA_EFFORT_DIR`, while a journal beside the store shows a write in flight, when a row count" + " changes, and on any refusal of the schema plan, whose text ends with the next safe step. With" + " `--output=json` the outcome is the data of the terminal result event. Starts no session.",
7593
8144
  args: [],
7594
8145
  flags: [
7595
8146
  {
7596
- name: DRY_RUN_FLAG2,
7597
- description: "Build and check the rebuild, print what it keeps and leaves behind, then delete it. The" + " live store is only read, so this runs beside a live loop and can be repeated.",
8147
+ name: DRY_RUN_FLAG3,
8148
+ description: "Build and check the migrated store, print what it applies, then delete it. The live store" + " is only read, so this runs beside a live loop and from a development build, and can be repeated.",
7598
8149
  type: "boolean"
7599
8150
  }
7600
8151
  ],
7601
8152
  examples: [
7602
8153
  {
7603
- cmd: "rafa effort fix-schema --dry-run",
7604
- note: "Prints what a rebuild would keep and leave behind, changing nothing."
8154
+ cmd: "rafa effort migrate --dry-run",
8155
+ note: "Prints what the migration applies and how it checked, changing nothing."
7605
8156
  },
7606
8157
  {
7607
- cmd: "rafa effort fix-schema",
7608
- note: "Swaps the rebuild in and prints where the original was kept."
8158
+ cmd: "rafa effort migrate",
8159
+ note: "Swaps the migrated store in and prints where the original was kept."
7609
8160
  }
7610
8161
  ],
7611
8162
  outputs: ["text", "json"],
7612
8163
  run: async (context) => {
7613
- runFixSchema(context, seams);
8164
+ runMigrate(context, seams);
7614
8165
  await Promise.resolve();
7615
8166
  }
7616
8167
  };
7617
8168
  return Object.freeze(command);
7618
8169
  }
7619
- var fix_schema_default = createFixSchemaCommand();
8170
+ var migrate_default = createMigrateCommand();
7620
8171
 
7621
8172
  // src/commands/effort/report.ts
7622
8173
  var report_default = wrapPhaseZeroCommand({
@@ -7671,6 +8222,113 @@ var report_default = wrapPhaseZeroCommand({
7671
8222
  outputs: ["text", "json"]
7672
8223
  }, report);
7673
8224
 
8225
+ // src/commands/effort/schema.ts
8226
+ import { join as join21 } from "path";
8227
+ var COMMAND_NAME8 = "rafa effort schema";
8228
+ var SCHEMA_USAGE = "rafa effort schema [--check]";
8229
+ var CHECK_FLAG = "check";
8230
+ var REFUSED_EXIT3 = 1;
8231
+ var FAILED_EXIT2 = 2;
8232
+ function listLine(name2, items) {
8233
+ const shown = items.length === 0 ? "none" : items.join(", ");
8234
+ return `${name2} (${String(items.length)}): ${shown}`;
8235
+ }
8236
+ function gateLine(report2) {
8237
+ const held = report2.userVersion ?? 0;
8238
+ const now = report2.logged ? `${String(held)} (${gateMeaning(held)})` : `${String(held)} (a count of legacy migrations: no log yet)`;
8239
+ const next = report2.gate === null || report2.gate === report2.userVersion ? "" : `; ${String(report2.gate)} (${gateMeaning(report2.gate)}) once the next open brings it forward`;
8240
+ return `Gate (user_version): ${now}${next}`;
8241
+ }
8242
+ function verdictLine(report2) {
8243
+ switch (report2.status) {
8244
+ case "absent":
8245
+ return "No store yet: the first write creates it at this rafa's migrations.";
8246
+ case "current":
8247
+ return "\u2705 Current: this rafa reads and writes the store as it is.";
8248
+ case "behind":
8249
+ return "\u2705 Behind: the next command that opens the store brings it forward in one transaction.";
8250
+ default:
8251
+ return `\u274C Refused (${report2.status}): ${String(report2.refusal)}`;
8252
+ }
8253
+ }
8254
+ function renderSchema(report2) {
8255
+ if (!report2.exists)
8256
+ return [`Effort store: ${report2.path}`, verdictLine(report2), `Next safe step: ${report2.nextStep}`];
8257
+ const log = report2.logged ? "Migration log: present." : "Migration log: none; a release before the log wrote this store, and the next open adopts it.";
8258
+ return [
8259
+ `Effort store: ${report2.path}`,
8260
+ log,
8261
+ gateLine(report2),
8262
+ listLine("Applied", report2.applied.map(({ id: id2, appliedBy }) => appliedBy === null ? id2 : `${id2} (${appliedBy})`)),
8263
+ listLine("Pending", report2.pending),
8264
+ listLine("Unknown", report2.unknown.map(({ id: id2, breaks, appliedBy, appliedAt }) => `${id2} (breaks ${breaks.length === 0 ? "nothing" : breaks.join(" and ")}; applied by ${appliedBy} on ${appliedAt})`)),
8265
+ listLine("Edited", report2.edited.map(({ id: id2, recorded, expected }) => `${id2} (store ${recorded}, this rafa ${expected})`)),
8266
+ verdictLine(report2),
8267
+ `Next safe step: ${report2.nextStep}`
8268
+ ];
8269
+ }
8270
+ function storePath2(context, root) {
8271
+ try {
8272
+ return join21(effortStoreDir(root, context.env), SQLITE_STORE_FILE_NAME);
8273
+ } catch (error) {
8274
+ throw new CommandExit(REFUSED_EXIT3, `\u274C ${COMMAND_NAME8}: ${messageOf(error)}`);
8275
+ }
8276
+ }
8277
+ function runSchema(context, seams) {
8278
+ expectNoArgument(context.args, SCHEMA_USAGE);
8279
+ const check = readSwitch(CHECK_FLAG, context.flags[CHECK_FLAG], `Usage: ${SCHEMA_USAGE}`);
8280
+ const path = storePath2(context, requireProject(context, COMMAND_NAME8).root);
8281
+ let report2;
8282
+ try {
8283
+ report2 = readSchemaReport(path, seams);
8284
+ } catch (error) {
8285
+ throw new CommandExit(FAILED_EXIT2, `\u274C ${COMMAND_NAME8}: ${path} cannot be read as an effort store: ${messageOf(error)}`);
8286
+ }
8287
+ if (context.outputMode !== "json") {
8288
+ for (const line of renderSchema(report2))
8289
+ context.output.info(line);
8290
+ }
8291
+ if (check && report2.refused) {
8292
+ throw new CommandExit(REFUSED_EXIT3, `\u274C ${COMMAND_NAME8} --check: this rafa refuses ${path} (${report2.status}). Next safe step: ${report2.nextStep}`);
8293
+ }
8294
+ if (context.outputMode === "json")
8295
+ context.output.result(report2);
8296
+ }
8297
+ function createSchemaCommand(seams = {}) {
8298
+ const command = {
8299
+ name: "effort schema",
8300
+ subject: "effort",
8301
+ action: "schema",
8302
+ summary: "say whether this rafa can read and write the effort store, and what to run next when it cannot",
8303
+ description: "Opens the SQLite effort store read-only, the one under `RAFA_EFFORT_DIR` when it is set, and" + " migrates nothing. Prints the migrations it holds that this rafa knows (applied), the ones it lacks" + " (pending), the ones this rafa does not know with what they break (unknown), and the ones recorded under" + " another checksum (edited), then the legacy gate `user_version` holds and whether this rafa would use" + " the store or refuse it, and why. It ends with `Next safe step: <command>`, the command the matching" + " refusal names, or `none`. With `--check` it exits 1 when this rafa would refuse to read or to write" + " the store. Exit code 2 when the file cannot be read as a store. With `--output=json` the report, with" + " its `nextStep`, is the data of the terminal result event. Starts no session.",
8304
+ args: [],
8305
+ flags: [
8306
+ {
8307
+ name: CHECK_FLAG,
8308
+ description: "Exit 1 when this rafa would refuse to read or to write the store; exit 0 otherwise.",
8309
+ type: "boolean"
8310
+ }
8311
+ ],
8312
+ examples: [
8313
+ {
8314
+ cmd: "rafa effort schema",
8315
+ note: "Prints the store's migrations, the gate and the verdict, ending with the next safe step."
8316
+ },
8317
+ {
8318
+ cmd: "rafa effort schema --check",
8319
+ note: "The same report, exiting 1 when this rafa would refuse the store."
8320
+ }
8321
+ ],
8322
+ outputs: ["text", "json"],
8323
+ run: async (context) => {
8324
+ runSchema(context, seams);
8325
+ await Promise.resolve();
8326
+ }
8327
+ };
8328
+ return Object.freeze(command);
8329
+ }
8330
+ var schema_default = createSchemaCommand();
8331
+
7674
8332
  // src/board/roadmap-tick.ts
7675
8333
  var PREFIX2 = "board roadmap";
7676
8334
  var REPO_PATH = "{owner}/{repo}";
@@ -8037,7 +8695,7 @@ async function applyHorizonChange(board, reading, reason) {
8037
8695
  }
8038
8696
 
8039
8697
  // src/commands/epic/horizon-change.ts
8040
- var TO_FLAG = "to";
8698
+ var TO_FLAG2 = "to";
8041
8699
  var ISSUE_NUMBER2 = /^[1-9]\d*$/u;
8042
8700
  var NO_ANSWERS = ["n", "no"];
8043
8701
  var DEFER_ACTION = Object.freeze({
@@ -8063,9 +8721,9 @@ function readHorizonLine(context, action) {
8063
8721
  if (!ISSUE_NUMBER2.test(word)) {
8064
8722
  throw lineRefusal(`"${word}" is no epic number, which is a whole number from 1`, action.usage);
8065
8723
  }
8066
- const to = readChoiceFlag(flags, TO_FLAG, action.targets, action.usage);
8724
+ const to = readChoiceFlag(flags, TO_FLAG2, action.targets, action.usage);
8067
8725
  if (to === undefined)
8068
- throw lineRefusal(`--${TO_FLAG} is required: --${TO_FLAG}=<horizon>`, action.usage);
8726
+ throw lineRefusal(`--${TO_FLAG2} is required: --${TO_FLAG2}=<horizon>`, action.usage);
8069
8727
  const reason = readTextFlag(flags, REASON_FLAG, action.usage) ?? null;
8070
8728
  return Object.freeze({ epic: Number(word), to, reason });
8071
8729
  }
@@ -8078,7 +8736,7 @@ function wrongDirection(action, change) {
8078
8736
  return null;
8079
8737
  const other = action.name === "defer" ? "promote" : "defer";
8080
8738
  const verb = later ? "a deferral" : "a promotion";
8081
- return new CommandExit(EPIC_HORIZON_REFUSAL_EXIT, `\u274C Epic ${ref2(change.epic)} stands on ${change.from}, so ${change.from} \u2192 ${change.to} is ${verb};` + ` run rafa epic ${other} ${String(change.epic)} --${TO_FLAG}=${change.to}. Nothing was changed`);
8739
+ return new CommandExit(EPIC_HORIZON_REFUSAL_EXIT, `\u274C Epic ${ref2(change.epic)} stands on ${change.from}, so ${change.from} \u2192 ${change.to} is ${verb};` + ` run rafa epic ${other} ${String(change.epic)} --${TO_FLAG2}=${change.to}. Nothing was changed`);
8082
8740
  }
8083
8741
  function workPhrase(work) {
8084
8742
  return [
@@ -8265,9 +8923,9 @@ function readMoveLine(context) {
8265
8923
  throw lineRefusal(`Expected one issue number, got ${got}`, USAGE8);
8266
8924
  }
8267
8925
  const issue = issueNumber(word, "issue number");
8268
- const target = readTextFlag(flags, TO_FLAG, USAGE8);
8926
+ const target = readTextFlag(flags, TO_FLAG2, USAGE8);
8269
8927
  if (target === undefined)
8270
- throw lineRefusal(`--${TO_FLAG} is required: --${TO_FLAG}=<epic>`, USAGE8);
8928
+ throw lineRefusal(`--${TO_FLAG2} is required: --${TO_FLAG2}=<epic>`, USAGE8);
8271
8929
  const to = issueNumber(target, "epic number");
8272
8930
  const reason = readTextFlag(flags, REASON_FLAG, USAGE8) ?? null;
8273
8931
  return Object.freeze({ issue, to, reason });
@@ -8526,7 +9184,7 @@ function createEpicMoveCommand(seams = {}) {
8526
9184
  ],
8527
9185
  flags: [
8528
9186
  {
8529
- name: TO_FLAG,
9187
+ name: TO_FLAG2,
8530
9188
  description: "The number of the open epic it moves to. Required.",
8531
9189
  type: "string",
8532
9190
  required: true
@@ -8955,11 +9613,11 @@ var cancel_default = createEpicCancelCommand();
8955
9613
 
8956
9614
  // src/commands/epic/close.ts
8957
9615
  import { randomUUID as randomUUID3 } from "crypto";
8958
- import { join as join20 } from "path";
9616
+ import { join as join25 } from "path";
8959
9617
 
8960
9618
  // src/board/epic-template.ts
8961
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
8962
- import { dirname as dirname3, join as join17 } from "path";
9619
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
9620
+ import { dirname as dirname3, join as join22 } from "path";
8963
9621
  import { fileURLToPath as fileURLToPath2 } from "url";
8964
9622
  var MODULE_DIR2 = dirname3(fileURLToPath2(import.meta.url));
8965
9623
  var EPIC_TEMPLATE_FILE = "epic.md";
@@ -8968,11 +9626,11 @@ var CRITERIA_PLACEHOLDERS = Object.freeze([
8968
9626
  ]);
8969
9627
  var PLACEHOLDER_REASON = "the line is still the epic template's placeholder, so there is nothing to check";
8970
9628
  function epicTemplateSource(moduleDir = MODULE_DIR2) {
8971
- return join17(moduleDir, TEMPLATES_DIRNAME, EPIC_TEMPLATE_FILE);
9629
+ return join22(moduleDir, TEMPLATES_DIRNAME, EPIC_TEMPLATE_FILE);
8972
9630
  }
8973
9631
  function readEpicTemplate(moduleDir = MODULE_DIR2) {
8974
9632
  const source = epicTemplateSource(moduleDir);
8975
- if (!existsSync6(source)) {
9633
+ if (!existsSync9(source)) {
8976
9634
  throw new Error(`epic template: the epic issue template is missing: no file at ${source}`);
8977
9635
  }
8978
9636
  return readFileSync8(source, "utf8");
@@ -9066,8 +9724,8 @@ function renderEpicCost(cost, estimate) {
9066
9724
  }
9067
9725
 
9068
9726
  // src/epic/verify-plan.ts
9069
- import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
9070
- import { dirname as dirname4, join as join18 } from "path";
9727
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
9728
+ import { dirname as dirname4, join as join23 } from "path";
9071
9729
  import { fileURLToPath as fileURLToPath3 } from "url";
9072
9730
  var MODULE_DIR3 = dirname4(fileURLToPath3(import.meta.url));
9073
9731
  var VERIFY_PROMPT_FILE = "epic-verify-prompt.md";
@@ -9124,11 +9782,11 @@ function criteriaToAsk(criteria) {
9124
9782
  return Object.freeze(criteria.filter((criterion) => !criterion.placeholder));
9125
9783
  }
9126
9784
  function verifyPromptCandidates(moduleDir = MODULE_DIR3) {
9127
- return [join18(moduleDir, VERIFY_PROMPT_FILE), join18(dirname4(moduleDir), VERIFY_PROMPT_FILE)];
9785
+ return [join23(moduleDir, VERIFY_PROMPT_FILE), join23(dirname4(moduleDir), VERIFY_PROMPT_FILE)];
9128
9786
  }
9129
9787
  function readVerifyPrompt(moduleDir = MODULE_DIR3) {
9130
9788
  const candidates = verifyPromptCandidates(moduleDir);
9131
- const found = candidates.find((candidate) => existsSync7(candidate));
9789
+ const found = candidates.find((candidate) => existsSync10(candidate));
9132
9790
  if (found === undefined) {
9133
9791
  throw new Error(`epic verify prompt: the verification prompt is missing: no file at ${candidates.join(" or ")}`);
9134
9792
  }
@@ -9269,7 +9927,7 @@ function parseVerifyPlan(output, criteria) {
9269
9927
 
9270
9928
  // src/epic/verify-run.ts
9271
9929
  import { randomUUID as randomUUID2 } from "crypto";
9272
- import { join as join19 } from "path";
9930
+ import { join as join24 } from "path";
9273
9931
  var CHECK_TOOLS = ["Read", "Grep", "Glob", "Bash"];
9274
9932
  var DEFAULT_VERIFY_REMOTE = "origin";
9275
9933
  var CHECK_PROMPT_PREFIX = "# Epic acceptance check instructions";
@@ -9278,7 +9936,7 @@ var CHECK_RESULTS = ["pass", "fail"];
9278
9936
  var WORKTREE_PREFIX2 = "epic-";
9279
9937
  var VERDICT_KIND = VERDICT_BLOCK_FENCE.slice("rafa:".length);
9280
9938
  function verifyWorktreePath(home, epic) {
9281
- return join19(home, WORKTREES_SUBDIR, `${WORKTREE_PREFIX2}${String(epic)}`);
9939
+ return join24(home, WORKTREES_SUBDIR, `${WORKTREE_PREFIX2}${String(epic)}`);
9282
9940
  }
9283
9941
  function fetchStep(remote) {
9284
9942
  return { id: "fetch", args: ["fetch", remote] };
@@ -9538,7 +10196,7 @@ async function planVerification(input) {
9538
10196
  return Object.freeze({ status: "planned", sessionId, verdicts: reading.verdicts, issues: reading.issues });
9539
10197
  }
9540
10198
  function closeTriageFile(root, epic) {
9541
- return join20(root, ".rafa", `epic-${String(epic)}-close`);
10199
+ return join25(root, ".rafa", `epic-${String(epic)}-close`);
9542
10200
  }
9543
10201
  function failedCheckArtifact(epic, criterion) {
9544
10202
  return `epic ${ref5(epic)} acceptance criterion: ${oneLine(criterion.text)}`;
@@ -9814,7 +10472,7 @@ function createEpicDeferCommand(seams = {}) {
9814
10472
  ],
9815
10473
  flags: [
9816
10474
  {
9817
- name: TO_FLAG,
10475
+ name: TO_FLAG2,
9818
10476
  description: `The horizon it moves to: one of ${DEFER_ACTION.targets.join(", ")}. Required.`,
9819
10477
  type: "string",
9820
10478
  required: true
@@ -10050,7 +10708,7 @@ function createEpicPromoteCommand(seams = {}) {
10050
10708
  ],
10051
10709
  flags: [
10052
10710
  {
10053
- name: TO_FLAG,
10711
+ name: TO_FLAG2,
10054
10712
  description: `The horizon it moves to: one of ${PROMOTE_ACTION.targets.join(", ")}. Required.`,
10055
10713
  type: "string",
10056
10714
  required: true
@@ -10075,17 +10733,17 @@ function createEpicPromoteCommand(seams = {}) {
10075
10733
  var promote_default = createEpicPromoteCommand();
10076
10734
 
10077
10735
  // src/commands/init.ts
10078
- import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
10736
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
10079
10737
  import { homedir } from "os";
10080
- import { join as join25, relative as relative6, sep as sep4 } from "path";
10738
+ import { join as join30, relative as relative6, sep as sep4 } from "path";
10081
10739
 
10082
10740
  // src/agents/vendorable.ts
10083
- import { readdirSync, readFileSync as readFileSync10 } from "fs";
10084
- import { isAbsolute as isAbsolute5, join as join21 } from "path";
10741
+ import { readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
10742
+ import { isAbsolute as isAbsolute5, join as join26 } from "path";
10085
10743
  function planFiles(dir) {
10086
10744
  let entries;
10087
10745
  try {
10088
- entries = readdirSync(dir, { withFileTypes: true });
10746
+ entries = readdirSync2(dir, { withFileTypes: true });
10089
10747
  } catch {
10090
10748
  return [];
10091
10749
  }
@@ -10103,7 +10761,7 @@ function rafaHolds(roster, name2) {
10103
10761
  return item?.holders.some((holder) => holder.source === "rafa") === true;
10104
10762
  }
10105
10763
  function vendorableAgents(scan) {
10106
- const dir = isAbsolute5(scan.planDir) ? scan.planDir : join21(scan.repoRoot, scan.planDir);
10764
+ const dir = isAbsolute5(scan.planDir) ? scan.planDir : join26(scan.repoRoot, scan.planDir);
10107
10765
  const roots = {
10108
10766
  repoRoot: scan.repoRoot,
10109
10767
  home: scan.home,
@@ -10112,7 +10770,7 @@ function vendorableAgents(scan) {
10112
10770
  const roster = resolveAgentRoster(roots, scan.settings);
10113
10771
  const found = [];
10114
10772
  for (const file of planFiles(dir)) {
10115
- const path = join21(dir, file);
10773
+ const path = join26(dir, file);
10116
10774
  const markdown = readText2(path);
10117
10775
  if (markdown === null)
10118
10776
  continue;
@@ -10138,12 +10796,13 @@ function vendorableAgentWarnings(agents, root) {
10138
10796
 
10139
10797
  // src/project/gitignore.ts
10140
10798
  import { createHash } from "crypto";
10141
- import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "fs";
10142
- import { dirname as dirname5, isAbsolute as isAbsolute6, join as join22 } from "path";
10799
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "fs";
10800
+ import { dirname as dirname5, isAbsolute as isAbsolute6, join as join27 } from "path";
10143
10801
  var GITIGNORE_FILE = ".gitignore";
10144
10802
  var DIGEST_NAME = "tracking.digest";
10145
10803
  var PRIVATE_TRIAGE = "triage/private";
10146
- var TRACKING_DIGEST_FILE = join22(SCOPE_DIR, DIGEST_NAME);
10804
+ var CACHE_DIR = "cache";
10805
+ var TRACKING_DIGEST_FILE = join27(SCOPE_DIR, DIGEST_NAME);
10147
10806
  var BLOCK_BEGIN = "# >>> rafa tracking: rafa rewrites the lines up to the end marker";
10148
10807
  var BLOCK_END = "# <<< rafa tracking";
10149
10808
  var SUB_PATHS = [
@@ -10163,7 +10822,8 @@ function trackingEntry(flags) {
10163
10822
  `!/${SCOPE_DIR}/`,
10164
10823
  `${SCOPE_DIR}/${PRIVATE_TRIAGE}/`,
10165
10824
  `${SCOPE_DIR}/${DIGEST_NAME}`,
10166
- `${SCOPE_DIR}/${POSITION_FILE}`
10825
+ `${SCOPE_DIR}/${POSITION_FILE}`,
10826
+ `${SCOPE_DIR}/${CACHE_DIR}/`
10167
10827
  ];
10168
10828
  }
10169
10829
  const tracked = SUB_PATHS.filter((sub) => flags[sub.flag]).map((sub) => `!${SCOPE_DIR}/${sub.dir}/`);
@@ -10230,13 +10890,13 @@ function attempt(file, failure3, act) {
10230
10890
  }
10231
10891
  }
10232
10892
  function readIfPresent2(file) {
10233
- if (!existsSync8(file))
10893
+ if (!existsSync11(file))
10234
10894
  return null;
10235
10895
  return attempt(file, "cannot be read", () => readFileSync11(file, "utf8"));
10236
10896
  }
10237
10897
  function writeTrackingGitignore(root, flags) {
10238
10898
  requireAbsolute(root);
10239
- const file = join22(root, GITIGNORE_FILE);
10899
+ const file = join27(root, GITIGNORE_FILE);
10240
10900
  const text = readIfPresent2(file);
10241
10901
  const next = withTrackingBlock(text, flags, file);
10242
10902
  if (next === text)
@@ -10258,7 +10918,7 @@ function trackingDigest(flags) {
10258
10918
  }
10259
10919
  function exposureNotice(root) {
10260
10920
  return [
10261
- `tracking.all is true, so ${join22(root, GITIGNORE_FILE)} no longer ignores ${SCOPE_DIR}/` + " and a commit can carry what it holds, including:",
10921
+ `tracking.all is true, so ${join27(root, GITIGNORE_FILE)} no longer ignores ${SCOPE_DIR}/` + " and a commit can carry what it holds, including:",
10262
10922
  " - unpatched issues described in specs and plans",
10263
10923
  " - effort rows with model names and token counts",
10264
10924
  " - instincts that may quote error output",
@@ -10271,7 +10931,7 @@ function printWarning(message) {
10271
10931
  }
10272
10932
  function noticeTrackingChange(root, flags, print2 = printWarning) {
10273
10933
  requireAbsolute(root);
10274
- const digestFile = join22(root, TRACKING_DIGEST_FILE);
10934
+ const digestFile = join27(root, TRACKING_DIGEST_FILE);
10275
10935
  const recorded = `${trackingDigest(flags)}
10276
10936
  `;
10277
10937
  if (readIfPresent2(digestFile) === recorded) {
@@ -10280,7 +10940,7 @@ function noticeTrackingChange(root, flags, print2 = printWarning) {
10280
10940
  if (flags.trackingAll)
10281
10941
  print2(exposureNotice(root));
10282
10942
  attempt(digestFile, "cannot be written", () => {
10283
- mkdirSync3(dirname5(digestFile), { recursive: true });
10943
+ mkdirSync4(dirname5(digestFile), { recursive: true });
10284
10944
  writeFileSync2(digestFile, recorded);
10285
10945
  });
10286
10946
  return { digestFile, changed: true, printed: flags.trackingAll };
@@ -10291,12 +10951,12 @@ function applyTracking(root, flags, print2 = printWarning) {
10291
10951
  }
10292
10952
 
10293
10953
  // src/project/root-choice.ts
10294
- import { resolve as resolve7 } from "path";
10954
+ import { resolve as resolve9 } from "path";
10295
10955
 
10296
10956
  // src/project/roots.ts
10297
10957
  import { spawnSync } from "child_process";
10298
- import { accessSync, constants, readFileSync as readFileSync12, statSync as statSync7 } from "fs";
10299
- import { isAbsolute as isAbsolute7, join as join23, resolve as resolve6 } from "path";
10958
+ import { accessSync, constants as constants2, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
10959
+ import { isAbsolute as isAbsolute7, join as join28, resolve as resolve8 } from "path";
10300
10960
  var REFUSED_ROOTS = Object.freeze([
10301
10961
  { path: "/", why: "is the filesystem root" },
10302
10962
  { path: "/var", why: "holds system state" },
@@ -10317,8 +10977,8 @@ var NOT_A_REPOSITORY = "fatal: not a git repository (or any ";
10317
10977
  var DISK_ROOTS_FILE_SYSTEM = Object.freeze({
10318
10978
  ...DISK_FILE_SYSTEM,
10319
10979
  readFile: (path) => readFileSync12(path, "utf8"),
10320
- isDirectory: (path) => statSync7(path).isDirectory(),
10321
- assertWritable: (path) => accessSync(path, constants.W_OK)
10980
+ isDirectory: (path) => statSync8(path).isDirectory(),
10981
+ assertWritable: (path) => accessSync(path, constants2.W_OK)
10322
10982
  });
10323
10983
 
10324
10984
  class RootsError extends Error {
@@ -10359,7 +11019,7 @@ function realOrSpelled(path, fs) {
10359
11019
  try {
10360
11020
  return fs.realpath(path);
10361
11021
  } catch {
10362
- return resolve6(path);
11022
+ return resolve8(path);
10363
11023
  }
10364
11024
  }
10365
11025
  function namedRoots(home, fs) {
@@ -10401,7 +11061,7 @@ function writeRefusal(given, real, fs) {
10401
11061
  }
10402
11062
  function refusalOf(given, named2, fs) {
10403
11063
  const resolved = resolveReal(given, fs);
10404
- const byName2 = namedRefusal(given, resolved.real ?? resolve6(given), named2);
11064
+ const byName2 = namedRefusal(given, resolved.real ?? resolve8(given), named2);
10405
11065
  if (byName2 !== null)
10406
11066
  return byName2;
10407
11067
  if (resolved.real === null) {
@@ -10429,8 +11089,8 @@ function readManifest2(file, fs) {
10429
11089
  }
10430
11090
  }
10431
11091
  function readDir(dir, fs) {
10432
- const manifest = readManifest2(join23(dir, PACKAGE_JSON), fs);
10433
- const files = MONOREPO_MARKER_FILES.filter((name2) => fs.exists(join23(dir, name2)));
11092
+ const manifest = readManifest2(join28(dir, PACKAGE_JSON), fs);
11093
+ const files = MONOREPO_MARKER_FILES.filter((name2) => fs.exists(join28(dir, name2)));
10434
11094
  const markers = manifest.marks ? [PACKAGE_JSON, ...files] : files;
10435
11095
  return { ...manifest, dir, markers };
10436
11096
  }
@@ -10500,7 +11160,7 @@ function firstCandidate(found) {
10500
11160
  return first.refusal === null ? chosen(first.path, first.source) : problem(first.refusal.reason);
10501
11161
  }
10502
11162
  function namedRoot(typed, start2, source, seams) {
10503
- const path = resolve7(start2, typed);
11163
+ const path = resolve9(start2, typed);
10504
11164
  const refusal9 = rootRefusal(path, seams);
10505
11165
  if (refusal9 !== null)
10506
11166
  return problem(refusal9.reason);
@@ -10541,12 +11201,14 @@ async function promptForRoot(found, prompter, seams) {
10541
11201
  }
10542
11202
 
10543
11203
  // src/project/scaffold.ts
10544
- import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync3 } from "fs";
10545
- import { join as join24 } from "path";
11204
+ import { existsSync as existsSync12, lstatSync, mkdirSync as mkdirSync5, statSync as statSync9, writeFileSync as writeFileSync3 } from "fs";
11205
+ import { join as join29 } from "path";
10546
11206
  var PROJECT_TREE = Object.freeze(["specs", "plans", "runs", "effort", "instincts"]);
10547
11207
  var USER_TREE = Object.freeze(["instincts"]);
10548
11208
  var CONFIG_SETTINGS_LINES = Object.freeze([
10549
11209
  `# store: ${CONFIG_DEFAULTS.store} # sqlite | ndjson`,
11210
+ "# effort:",
11211
+ `# busyTimeoutMs: ${String(CONFIG_DEFAULTS.effortBusyTimeoutMs)} # 1..60000 ms; how long a store open waits for another process's lock`,
10550
11212
  "# plan:",
10551
11213
  `# inject: ${CONFIG_DEFAULTS.inject} # full | stage | task`,
10552
11214
  `# dir: ${CONFIG_DEFAULTS.planDir}`,
@@ -10643,18 +11305,18 @@ class ScaffoldError extends Error {
10643
11305
  }
10644
11306
  function layoutOf2(base, tree) {
10645
11307
  const scope = scopeAt(base);
10646
- return { dirs: [scope.dir, ...tree.map((name2) => join24(scope.dir, name2))], configFile: configFilePath(base) };
11308
+ return { dirs: [scope.dir, ...tree.map((name2) => join29(scope.dir, name2))], configFile: configFilePath(base) };
10647
11309
  }
10648
11310
  function occupied(path) {
10649
11311
  return lstatSync(path, { throwIfNoEntry: false }) !== undefined;
10650
11312
  }
10651
11313
  function isDirectory2(path) {
10652
- return statSync8(path, { throwIfNoEntry: false })?.isDirectory() === true;
11314
+ return statSync9(path, { throwIfNoEntry: false })?.isDirectory() === true;
10653
11315
  }
10654
11316
  function directoryConflict(dir) {
10655
11317
  if (!occupied(dir) || isDirectory2(dir))
10656
11318
  return null;
10657
- return existsSync9(dir) ? `${dir} is not a directory` : `${dir} is a link to nothing`;
11319
+ return existsSync12(dir) ? `${dir} is not a directory` : `${dir} is a link to nothing`;
10658
11320
  }
10659
11321
  function layoutConflicts(layout) {
10660
11322
  const [scopeDir, ...tree] = layout.dirs;
@@ -10662,7 +11324,7 @@ function layoutConflicts(layout) {
10662
11324
  if (opening !== null)
10663
11325
  return [opening];
10664
11326
  const dirs = tree.flatMap((dir) => directoryConflict(dir) ?? []);
10665
- const file = occupied(layout.configFile) && !existsSync9(layout.configFile) ? [`${layout.configFile} is a link to nothing`] : [];
11327
+ const file = occupied(layout.configFile) && !existsSync12(layout.configFile) ? [`${layout.configFile} is a link to nothing`] : [];
10666
11328
  return [...dirs, ...file];
10667
11329
  }
10668
11330
  function scaffoldConflicts(root, home) {
@@ -10676,15 +11338,15 @@ function attempt2(path, failure3, act) {
10676
11338
  }
10677
11339
  }
10678
11340
  function ensureDirectory(path) {
10679
- if (existsSync9(path))
11341
+ if (existsSync12(path))
10680
11342
  return { path, kind: "directory", change: "unchanged" };
10681
11343
  attempt2(path, "cannot be created", () => {
10682
- mkdirSync4(path, { recursive: true });
11344
+ mkdirSync5(path, { recursive: true });
10683
11345
  });
10684
11346
  return { path, kind: "directory", change: "created" };
10685
11347
  }
10686
11348
  function ensureFile(path, text) {
10687
- if (existsSync9(path))
11349
+ if (existsSync12(path))
10688
11350
  return { path, kind: "file", change: "unchanged" };
10689
11351
  attempt2(path, "cannot be written", () => {
10690
11352
  writeFileSync3(path, text, { encoding: "utf8", flag: "wx" });
@@ -11000,9 +11662,9 @@ function resolvedConfig2(root, home, warn3) {
11000
11662
  }
11001
11663
  }
11002
11664
  function checkGitignore(root, flags) {
11003
- const file = join25(root, GITIGNORE_FILE);
11665
+ const file = join30(root, GITIGNORE_FILE);
11004
11666
  try {
11005
- const text = existsSync10(file) ? readFileSync14(file, "utf8") : null;
11667
+ const text = existsSync13(file) ? readFileSync14(file, "utf8") : null;
11006
11668
  withTrackingBlock(text, flags, file);
11007
11669
  } catch (error) {
11008
11670
  const message = error instanceof GitignoreError ? error.message : `rafa init: ${file} cannot be read (${messageOf(error)})`;
@@ -11033,8 +11695,8 @@ function initialise(root, start2, home, context, entry) {
11033
11695
  checkConflicts(root.path, home);
11034
11696
  const config = resolvedConfig2(root.path, home, warn3);
11035
11697
  checkGitignore(root.path, config);
11036
- const configExisted = existsSync10(configFilePath(root.path));
11037
- const digestExisted = existsSync10(join25(root.path, TRACKING_DIGEST_FILE));
11698
+ const configExisted = existsSync13(configFilePath(root.path));
11699
+ const digestExisted = existsSync13(join30(root.path, TRACKING_DIGEST_FILE));
11038
11700
  const project = writeProjectScope(root.path);
11039
11701
  const tracking = applyTracking(root.path, config, warn3);
11040
11702
  const writes = [...project, ...trackingWrites(tracking, digestExisted), ...writeUserScope(home)];
@@ -11212,7 +11874,7 @@ function createInitCommand(seams = DEFAULT_INIT_SEAMS) {
11212
11874
  var init_default = createInitCommand();
11213
11875
 
11214
11876
  // src/commands/check-report.ts
11215
- import { resolve as resolve8 } from "path";
11877
+ import { resolve as resolve10 } from "path";
11216
11878
  var DEFAULT_CHECK_SEAMS = Object.freeze({
11217
11879
  cwd: () => process.cwd()
11218
11880
  });
@@ -11228,7 +11890,7 @@ Usage: ${usage2}`);
11228
11890
  throw new CommandExit(1, `\u274C Expected one directory, and read "${extra}" as a second
11229
11891
  Usage: ${usage2}`);
11230
11892
  }
11231
- return resolve8(cwd(), dir);
11893
+ return resolve10(cwd(), dir);
11232
11894
  }
11233
11895
  function readFix(value, usage2) {
11234
11896
  if (value === undefined || value === false || value === "false")
@@ -11245,7 +11907,7 @@ function readProjectRoot(value, usage2, cwd) {
11245
11907
  throw new CommandExit(1, `\u274C --project needs a path, and the line gave it none
11246
11908
  Usage: ${usage2}`);
11247
11909
  }
11248
- return resolve8(cwd(), value);
11910
+ return resolve10(cwd(), value);
11249
11911
  }
11250
11912
  function reportMarker(report2) {
11251
11913
  if (report2.failed)
@@ -11743,7 +12405,7 @@ function createInstinctListCommand(seams = {}) {
11743
12405
  };
11744
12406
  return Object.freeze(command);
11745
12407
  }
11746
- var list_default5 = createInstinctListCommand();
12408
+ var list_default6 = createInstinctListCommand();
11747
12409
 
11748
12410
  // src/commands/instinct/show.ts
11749
12411
  var USAGE15 = "rafa instinct show <id>";
@@ -11881,8 +12543,8 @@ function createInstinctShowCommand() {
11881
12543
  var show_default3 = createInstinctShowCommand();
11882
12544
 
11883
12545
  // src/commands/issue/check.ts
11884
- import { readdirSync as readdirSync2, readFileSync as readFileSync15, statSync as statSync9 } from "fs";
11885
- import { join as join26, resolve as resolve9 } from "path";
12546
+ import { readdirSync as readdirSync3, readFileSync as readFileSync15, statSync as statSync10 } from "fs";
12547
+ import { join as join31, resolve as resolve11 } from "path";
11886
12548
  var CHECK_USAGE = "rafa issue check <n> [--stamp]";
11887
12549
  var STAMP_FLAG = "stamp";
11888
12550
  var ISSUE_NUMBER6 = /^[1-9]\d*$/u;
@@ -11906,7 +12568,7 @@ function missingCopyMessage(issue, specsDir) {
11906
12568
  function copyNames(dir, issue) {
11907
12569
  let names;
11908
12570
  try {
11909
- names = readdirSync2(dir);
12571
+ names = readdirSync3(dir);
11910
12572
  } catch (error) {
11911
12573
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
11912
12574
  return [];
@@ -11914,22 +12576,22 @@ function copyNames(dir, issue) {
11914
12576
  }
11915
12577
  const prefix = `${boardId(issue)}-`;
11916
12578
  const notes = notesFileName(issue);
11917
- return names.filter((name2) => name2.startsWith(prefix) && name2.endsWith(SPEC_EXTENSION) && name2 !== notes).filter((name2) => name2.length > prefix.length + SPEC_EXTENSION.length).filter((name2) => statSync9(join26(dir, name2)).isFile()).sort();
12579
+ return names.filter((name2) => name2.startsWith(prefix) && name2.endsWith(SPEC_EXTENSION) && name2 !== notes).filter((name2) => name2.length > prefix.length + SPEC_EXTENSION.length).filter((name2) => statSync10(join31(dir, name2)).isFile()).sort();
11918
12580
  }
11919
12581
  function findSavedCopy(root, specsDir, issue) {
11920
- const names = copyNames(resolve9(root, specsDir), issue);
12582
+ const names = copyNames(resolve11(root, specsDir), issue);
11921
12583
  const [only] = names;
11922
12584
  if (only === undefined)
11923
12585
  throw new CommandExit(1, missingCopyMessage(issue, specsDir));
11924
12586
  if (names.length > 1) {
11925
12587
  throw new CommandExit(1, [
11926
12588
  `\u274C Issue #${String(issue)} has ${String(names.length)} saved copies under ${specsDir}; remove the stale one:`,
11927
- ...names.map((name2) => ` ${join26(specsDir, name2)}`)
12589
+ ...names.map((name2) => ` ${join31(specsDir, name2)}`)
11928
12590
  ].join(`
11929
12591
  `));
11930
12592
  }
11931
- const path = join26(specsDir, only);
11932
- return { path, absolute: resolve9(root, path) };
12593
+ const path = join31(specsDir, only);
12594
+ return { path, absolute: resolve11(root, path) };
11933
12595
  }
11934
12596
  function liveText(live) {
11935
12597
  return live.kind === "unreadable" ? "unreadable" : fingerprintText(live);
@@ -12198,279 +12860,11 @@ function createIssueCreateCommand(seams = DEFAULT_ISSUE_SEAMS) {
12198
12860
  }
12199
12861
  var create_default = createIssueCreateCommand();
12200
12862
 
12201
- // src/commands/issue/roadmap-check.ts
12202
- var EPIC_CHECK_EXIT = 1;
12203
- function checkHead(count2) {
12204
- return count2 === 1 ? "\u274C 1 epic's stored state disagrees with its computed one:" : `\u274C ${String(count2)} epics' stored states disagree with their computed ones:`;
12205
- }
12206
- function epicCheckFailure(epics) {
12207
- if (epics.unknown !== null)
12208
- return `\u274C Could not check the epics: ${epics.unknown}`;
12209
- const lines2 = epics.epics.flatMap((epic) => epic.disagreement === null ? [] : [` ${epic.disagreement}`]);
12210
- if (lines2.length === 0)
12211
- return null;
12212
- return [checkHead(lines2.length), ...lines2].join(`
12213
- `);
12214
- }
12215
-
12216
- // src/commands/issue/list.ts
12217
- var USAGE18 = "rafa issue list [--roadmap [--all] [--full] [--check]] [--state=<state>] [--type=<type>] [--module=<name>]" + " [--search=<text>] [--limit=<n>]";
12218
- var SWITCH_HINT = `Type it bare: ${USAGE18}`;
12219
- var LIMIT = /^[1-9]\d*$/;
12220
- function readLimit(flags) {
12221
- const value = readTextFlag(flags, "limit", USAGE18);
12222
- if (value === undefined)
12223
- return;
12224
- const limit = Number(value);
12225
- if (LIMIT.test(value) && Number.isSafeInteger(limit))
12226
- return limit;
12227
- throw lineRefusal(`--limit is "${value}", expected a positive whole number`, USAGE18);
12228
- }
12229
- function readIssueQuery(flags) {
12230
- const narrowed = {
12231
- state: readChoiceFlag(flags, "state", ISSUE_STATES, USAGE18),
12232
- type: readChoiceFlag(flags, "type", ISSUE_TYPES, USAGE18),
12233
- module: readNonBlankFlag(flags, "module", USAGE18),
12234
- text: readNonBlankFlag(flags, "search", USAGE18),
12235
- limit: readLimit(flags)
12236
- };
12237
- return Object.fromEntries(Object.entries(narrowed).filter(([, value]) => value !== undefined));
12238
- }
12239
- function readIssueListLine(flags) {
12240
- const roadmap = readSwitch("roadmap", flags["roadmap"], SWITCH_HINT);
12241
- const all = readSwitch("all", flags["all"], SWITCH_HINT);
12242
- const full = readSwitch("full", flags["full"], SWITCH_HINT);
12243
- const check = readSwitch("check", flags["check"], SWITCH_HINT);
12244
- if (roadmap && flags["state"] !== undefined) {
12245
- throw lineRefusal("--state cannot narrow --roadmap: the Roadmap is read off the GitHub board, which holds an issue open or" + " closed and so no state of the tracker", USAGE18);
12246
- }
12247
- if (all && !roadmap)
12248
- throw lineRefusal("--all keeps the ticked Roadmap lines, so it needs --roadmap", USAGE18);
12249
- if (full && !roadmap)
12250
- throw lineRefusal("--full prints the issues of each Roadmap epic, so it needs --roadmap", USAGE18);
12251
- if (check && !roadmap)
12252
- throw lineRefusal("--check weighs the epics the board read finds, so it needs --roadmap", USAGE18);
12253
- return { roadmap, all, full, check };
12254
- }
12255
- function renderIssueList(kind, issues) {
12256
- const head = `Tracker: ${kind}`;
12257
- if (issues.length === 0)
12258
- return [head, "No issues."];
12259
- const widest = (column) => Math.max(...issues.map((issue) => column(issue).length));
12260
- const idWidth = widest((issue) => issue.ref.externalId);
12261
- const stateWidth = widest((issue) => issue.state);
12262
- const typeWidth = widest((issue) => issue.type);
12263
- const rows = issues.map((issue) => [
12264
- issue.ref.externalId.padStart(idWidth),
12265
- issue.state.padEnd(stateWidth),
12266
- issue.type.padEnd(typeWidth),
12267
- issue.title
12268
- ].join(" "));
12269
- return [head, ...rows.map((row2) => ` ${row2}`)];
12270
- }
12271
- function matchesFilter(row2, filter) {
12272
- const { issue } = row2;
12273
- if (filter.type !== undefined && issue?.type !== filter.type)
12274
- return false;
12275
- if (filter.module !== undefined && issue?.module !== filter.module)
12276
- return false;
12277
- if (filter.text === undefined)
12278
- return true;
12279
- const text = issue === null ? row2.line.why : `${issue.title}
12280
- ${issue.body}`;
12281
- return text.toLowerCase().includes(filter.text.toLowerCase());
12282
- }
12283
- function narrowRoadmapRows(rows, filter) {
12284
- const kept = rows.filter((row2) => matchesFilter(row2, filter));
12285
- return Object.freeze(filter.limit === undefined ? kept : kept.slice(0, filter.limit));
12286
- }
12287
- function renderRoadmapList(listed2, width, full = false) {
12288
- if (listed2.epics !== undefined) {
12289
- return renderEpicTable({ ...listed2.epics, roadmap: listed2.roadmap, specs: listed2.rows }, width, full);
12290
- }
12291
- const head = `Roadmap: #${String(listed2.roadmap)}`;
12292
- if (listed2.rows.length === 0)
12293
- return [head, "No issues."];
12294
- return [head, ...renderRoadmapTable(listed2.rows, width)];
12295
- }
12296
- async function listRoadmap(context, seams, all, filter) {
12297
- const project = issueProject(context);
12298
- const warn3 = (message) => {
12299
- context.output.warn(message);
12300
- };
12301
- const config = issueSubjectConfig(project, warn3);
12302
- const gh = seams.gh ?? createGhRunner({ cwd: project.root });
12303
- const plans = plansDirAt(project.root, config.planDir);
12304
- const planNames = (seams.planNames ?? createPlanDirNames)(plans.path);
12305
- const refs2 = async (issues) => roadmapRefsCells(await readDoctorRefs({ root: project.root, specsDir: config.specsDir, gh, env: context.env, issues }, { refsVerifier: seams.refsVerifier }));
12306
- let read;
12307
- try {
12308
- read = await readRoadmapEpicRows({
12309
- configured: config.roadmapIssue,
12310
- listBoards: createGhBoardLister({ gh }),
12311
- search: createGhRoadmapSearch({ gh }),
12312
- issues: createGhSpecIssueReader({ gh }),
12313
- board: createGhBoardListing({ gh }),
12314
- git: seams.git ?? createGitRunner(project.root),
12315
- pullRequests: createGhOpenPullRequests({ gh }),
12316
- planNames,
12317
- refs: refs2,
12318
- all,
12319
- root: project.root
12320
- });
12321
- } catch (error) {
12322
- const code = error instanceof CommandExit ? error.exitCode : ROADMAP_REFUSAL_EXIT;
12323
- throw new CommandExit(code, `\u274C Could not read the roadmap: ${messageOf(error)}`);
12324
- }
12325
- for (const warning of read.warnings)
12326
- warn3(warning);
12327
- const listed2 = {
12328
- roadmap: read.roadmap,
12329
- all,
12330
- filter,
12331
- rows: narrowRoadmapRows(read.specs, filter),
12332
- warnings: read.warnings
12333
- };
12334
- if (!hasEpicLines(read) && read.unknown === null)
12335
- return Object.freeze({ result: Object.freeze(listed2), epics: read.epics });
12336
- const { groups, hidden, unknown, problems } = read;
12337
- const result = Object.freeze({ ...listed2, epics: Object.freeze({ groups, hidden, unknown, problems }) });
12338
- return Object.freeze({ result, epics: read.epics });
12339
- }
12340
- async function listIssues(context, seams) {
12341
- expectNoArgument(context.args, USAGE18);
12342
- const query = readIssueQuery(context.flags);
12343
- const { tracker, data } = await resolveIssueTracker(context, seams);
12344
- const refs2 = await onTracker(tracker, "list issues", () => tracker.find(query));
12345
- const issues = await onTracker(tracker, "read the issues listed", () => Promise.all(refs2.map((ref6) => tracker.get(ref6))));
12346
- return { tracker: data, query, issues };
12347
- }
12348
- async function runIssueList(context, seams) {
12349
- const line = readIssueListLine(context.flags);
12350
- if (!line.roadmap) {
12351
- const listed2 = await listIssues(context, seams);
12352
- if (context.outputMode === "json") {
12353
- context.output.result(listed2);
12354
- return;
12355
- }
12356
- for (const text of renderIssueList(listed2.tracker.kind, listed2.issues))
12357
- context.output.info(text);
12358
- return;
12359
- }
12360
- expectNoArgument(context.args, USAGE18);
12361
- const filter = readIssueQuery(context.flags);
12362
- const { result, epics } = await listRoadmap(context, seams, line.all, filter);
12363
- const failure3 = line.check ? epicCheckFailure(epics) : null;
12364
- if (context.outputMode === "json") {
12365
- if (failure3 !== null)
12366
- throw new CommandExit(EPIC_CHECK_EXIT, failure3);
12367
- context.output.result(result);
12368
- return;
12369
- }
12370
- const width = (seams.terminalWidth ?? (() => process.stdout.columns))();
12371
- for (const text of renderRoadmapList(result, width, line.full))
12372
- context.output.info(text);
12373
- if (failure3 !== null)
12374
- throw new CommandExit(EPIC_CHECK_EXIT, failure3);
12375
- }
12376
- var ISSUE_LIST_FLAGS = Object.freeze([
12377
- {
12378
- name: "roadmap",
12379
- description: "List the unticked lines of the Roadmap issue instead, in its order, with the spec, blocked by," + " has and refs columns, and the epics it names grouped by horizon. Reads the GitHub board whatever" + " tracker the chain lands on.",
12380
- type: "boolean"
12381
- },
12382
- {
12383
- name: "all",
12384
- description: "With --roadmap, keep the ticked lines too, each in its place, and show the epics of every" + " horizon, not only now. Refused without --roadmap.",
12385
- type: "boolean"
12386
- },
12387
- {
12388
- name: "full",
12389
- description: "With --roadmap, print each epic's issues under its row, on two rows each: number, state and" + " title, then labels and blockers. Refused without --roadmap.",
12390
- type: "boolean"
12391
- },
12392
- {
12393
- name: "check",
12394
- description: "With --roadmap, exit 1 when any type:epic issue on the board is open with its work done, or" + " closed with it not done, or when the board could not be read, naming each. Refused without --roadmap.",
12395
- type: "boolean"
12396
- },
12397
- {
12398
- name: "state",
12399
- description: `Only issues in this state: one of ${ISSUE_STATES.join(", ")}. Refused on the github tracker` + " and beside --roadmap.",
12400
- type: "string"
12401
- },
12402
- {
12403
- name: "type",
12404
- description: `Only issues of this type: one of ${ISSUE_TYPES.join(", ")}.`,
12405
- type: "string"
12406
- },
12407
- {
12408
- name: "module",
12409
- description: "Only issues of this module.",
12410
- type: "string"
12411
- },
12412
- {
12413
- name: "search",
12414
- description: "Only issues whose title or body holds this text.",
12415
- type: "string"
12416
- },
12417
- {
12418
- name: "limit",
12419
- description: "At most this many issues, a positive whole number. The limit of the tracker when left out," + " and none under --roadmap.",
12420
- type: "number"
12421
- }
12422
- ]);
12423
- function createIssueListCommand(seams = DEFAULT_ISSUE_SEAMS) {
12424
- const command = {
12425
- name: "issue list",
12426
- subject: "issue",
12427
- action: "list",
12428
- summary: "list the issues on the tracker, or the Roadmap's lines in its order, narrowed by type, module or text",
12429
- description: "Lists the issues on the tracker `tracker.default` names in `.rafa/config.yaml`, or on the first" + " `tracker.fallback` kind whose preflight passes when it fails, each kind passed over warned about." + " Prints the tracker, then one row per issue: its id, state, type and title. Each flag narrows the" + " list, and a flag left out narrows by nothing. The github tracker holds an issue open or closed, so" + " it refuses `--state`, and lists 30 issues unless `--limit` says otherwise. With `--roadmap` it lists" + " the unticked lines of the Roadmap issue (`roadmap.issue`, else the lowest-numbered open type:roadmap" + " board, else the open issue titled Roadmap) in its" + " order instead, read off the GitHub board, as a table adding four columns: spec, whether the body" + " passes the readiness gate; blocked by, each blocker and whether it is open; has, a plan, a branch" + " or a pull request already made for it; and refs, how many references of the issue's saved copy under" + " `specs.dir` read suspect or dangling, `-` with no copy. A line naming an epic prints as an epic row" + " instead, grouped by horizon under a `Roadmap #<n> \xB7 <horizon>` heading with the columns #, state," + " done/total, blocked, title and date, the now horizon only unless `--all`, and the spec rows follow" + " under Specs; `--full` prints each epic's issues under its row, on two rows each: number, state and" + " title, then labels and blockers; `--check` exits 1 when any type:epic issue's stored state" + " disagrees with its computed one. `--type`, `--module`, `--search` and `--limit` then narrow the spec rows, keeping their" + " order, and an unreachable board is warned about with the rows still printed. With `--output=json`" + " the tracker, the query and every issue, or under `--roadmap` the roadmap, the rows, the epics and the" + " warnings, are the data of the terminal result event.",
12430
- args: [],
12431
- flags: [...ISSUE_LIST_FLAGS],
12432
- examples: [
12433
- {
12434
- cmd: "rafa issue list",
12435
- note: "Lists the issues on the tracker the chain lands on, one row each."
12436
- },
12437
- {
12438
- cmd: "rafa issue list --type=bug --search=timeout",
12439
- note: "Lists the bugs whose title or body holds the word timeout."
12440
- },
12441
- {
12442
- cmd: "rafa issue list --limit=5 --output=json",
12443
- note: "Writes a start event, then a result event whose data holds the tracker, the query and five issues at most."
12444
- },
12445
- {
12446
- cmd: "rafa issue list --roadmap",
12447
- note: "Prints the Roadmap's unticked lines in its order, with the spec, blocked by, has and refs of each."
12448
- },
12449
- {
12450
- cmd: "rafa issue list --roadmap --full",
12451
- note: "Prints the Roadmap's now epics with each epic's issues underneath, then its spec lines."
12452
- },
12453
- {
12454
- cmd: "rafa issue list --roadmap --check",
12455
- note: "Prints the Roadmap, then exits 1 when an epic is open with its work done or closed with it not done."
12456
- },
12457
- {
12458
- cmd: "rafa issue list --roadmap --all --type=bug",
12459
- note: "Prints the bugs on the Roadmap, ticked lines included, in the Roadmap's order, beside its epics of every horizon."
12460
- }
12461
- ],
12462
- outputs: ["text", "json"],
12463
- run: (context) => runIssueList(context, seams)
12464
- };
12465
- return Object.freeze(command);
12466
- }
12467
- var list_default6 = createIssueListCommand();
12468
-
12469
12863
  // src/commands/issue/move.ts
12470
- var USAGE19 = "rafa issue move <id> <state>";
12864
+ var USAGE18 = "rafa issue move <id> <state>";
12471
12865
  async function moveIssue(context, seams) {
12472
- const [id2, word] = expectTwoArguments(context.args, USAGE19);
12473
- const state = readChoice(word, "The state", ISSUE_STATES, USAGE19);
12866
+ const [id2, word] = expectTwoArguments(context.args, USAGE18);
12867
+ const state = readChoice(word, "The state", ISSUE_STATES, USAGE18);
12474
12868
  const { tracker, data } = await resolveIssueTracker(context, seams);
12475
12869
  const ref6 = issueRef(tracker, id2);
12476
12870
  const moved = await onTracker(tracker, `move issue ${id2} to ${state}`, () => tracker.transition(ref6, state));
@@ -12529,7 +12923,7 @@ function createIssueMoveCommand(seams = DEFAULT_ISSUE_SEAMS) {
12529
12923
  var move_default2 = createIssueMoveCommand();
12530
12924
 
12531
12925
  // src/commands/issue/show.ts
12532
- var USAGE20 = "rafa issue show <id>";
12926
+ var USAGE19 = "rafa issue show <id>";
12533
12927
  var NO_PRIORITY = "needs-triage";
12534
12928
  function renderIssue(kind, issue) {
12535
12929
  const project = issue.project === null ? [] : [`Project: ${issue.project}`];
@@ -12551,7 +12945,7 @@ function renderIssue(kind, issue) {
12551
12945
  ];
12552
12946
  }
12553
12947
  async function showIssue(context, seams) {
12554
- const id2 = expectOneArgument(context.args, USAGE20);
12948
+ const id2 = expectOneArgument(context.args, USAGE19);
12555
12949
  const { tracker, data } = await resolveIssueTracker(context, seams);
12556
12950
  const issue = await onTracker(tracker, `read issue ${id2}`, () => tracker.get(issueRef(tracker, id2)));
12557
12951
  return { tracker: data, issue };
@@ -12941,7 +13335,7 @@ function createIssueUnblockCommand(seams = DEFAULT_UNBLOCK_SEAMS) {
12941
13335
  var unblock_default = createIssueUnblockCommand();
12942
13336
 
12943
13337
  // src/commands/loop/list.ts
12944
- var USAGE21 = "rafa loop list";
13338
+ var USAGE20 = "rafa loop list";
12945
13339
  function renderSessionList(list) {
12946
13340
  if (list.sessions.length === 0)
12947
13341
  return ["No running sessions."];
@@ -12951,7 +13345,7 @@ function renderSessionList(list) {
12951
13345
  ];
12952
13346
  }
12953
13347
  async function runList5(context, seams) {
12954
- expectNoArgument(context.args, USAGE21);
13348
+ expectNoArgument(context.args, USAGE20);
12955
13349
  const root = projectRoot(context);
12956
13350
  const sessions = readRecords(root, seams).filter(isLive).map((session) => {
12957
13351
  const checklist = readSessionChecklist(root, session);
@@ -12996,7 +13390,7 @@ function createLoopListCommand(seams = {}) {
12996
13390
  var list_default7 = createLoopListCommand();
12997
13391
 
12998
13392
  // src/commands/loop/pause.ts
12999
- var USAGE22 = "rafa loop pause [-s|--session-id=<id>]";
13393
+ var USAGE21 = "rafa loop pause [-s|--session-id=<id>]";
13000
13394
  function renderPause(result) {
13001
13395
  const { session } = result;
13002
13396
  if (!result.changed)
@@ -13005,8 +13399,8 @@ function renderPause(result) {
13005
13399
  return [when, " Nothing is marked. `rafa loop resume` goes on, and `rafa loop stop` ends the run."];
13006
13400
  }
13007
13401
  async function runPause(context, seams) {
13008
- expectNoArgument(context.args, USAGE22);
13009
- const { root, record } = pickSession(context, USAGE22, "live", seams);
13402
+ expectNoArgument(context.args, USAGE21);
13403
+ const { root, record } = pickSession(context, USAGE21, "live", seams);
13010
13404
  if (!isLive(record)) {
13011
13405
  throw refusal(`Session ${record.sessionId} reads ${record.state}: no run is left to pause.`);
13012
13406
  }
@@ -13046,7 +13440,7 @@ function createLoopPauseCommand(seams = {}) {
13046
13440
  var pause_default = createLoopPauseCommand();
13047
13441
 
13048
13442
  // src/commands/loop/resume.ts
13049
- var USAGE23 = "rafa loop resume [-s|--session-id=<id>]";
13443
+ var USAGE22 = "rafa loop resume [-s|--session-id=<id>]";
13050
13444
  function renderResume(result) {
13051
13445
  const { session } = result;
13052
13446
  if (!result.changed)
@@ -13054,8 +13448,8 @@ function renderResume(result) {
13054
13448
  return [session.task === null ? `Resumed session ${session.sessionId}: it goes on with its next task.` : `Resumed session ${session.sessionId}: it goes on past its running task, line ${session.task.line}, ${session.task.text}`];
13055
13449
  }
13056
13450
  async function runResume(context, seams) {
13057
- expectNoArgument(context.args, USAGE23);
13058
- const { root, record } = pickSession(context, USAGE23, "live", seams);
13451
+ expectNoArgument(context.args, USAGE22);
13452
+ const { root, record } = pickSession(context, USAGE22, "live", seams);
13059
13453
  if (!isLive(record)) {
13060
13454
  throw refusal(`Session ${record.sessionId} reads ${record.state}: its run has ended, so there is nothing to resume.`, `\`rafa loop start --plan=${record.plan}\` runs its plan again as a new session, its blocked tasks first.`);
13061
13455
  }
@@ -13100,7 +13494,7 @@ var wrapped = wrapPhaseZeroCommand({
13100
13494
  subject: "loop",
13101
13495
  action: "start",
13102
13496
  summary: "run a plan task by task, then wrap up, open the PR and wait for CI",
13103
- description: "Walks the tracker of a plan one task at a time, each in a Claude Code session whose work" + " is committed once it exits, and retries blocked tasks first. After the last task a wrap-up" + " session promotes findings, syncs with main, pushes and opens or updates the PR, and the loop" + " then waits on its checks, spending repair sessions on a red or conflicting PR. With no" + " `--plan` it runs `PLAN.md` in `plan.dir`, `.rafa/plans` unless the config names another, or" + " `PLAN.md` at the project root when that one does not exist. Before any session it checks the" + " prerequisites the config and the plan's `PREREQUISITES-<stub>.md` name, halting when a required" + " one fails and naming each failed optional one known-missing in every task prompt. Started on" + " `main` or `master` it offers to create the plan's `feat/<stub>` from the latest" + " `origin/<base>` and run there, `--create-branch` answering that without asking, and refuses" + " the run when the offer is not taken. Each run writes its session record to `.rafa/runs/<session-id>.json`:" + " the plan, the branch, the pid, the start, the state and the running task. It refuses a plan whose" + " record names another branch, and a plan a session is still running. `rafa loop stop`, `pause`," + " `resume`, `status` and `list` reach the run through that record. A run holds its terminal: until" + " phase 6 it refuses `--detached`. With `--runtime` the whole run goes on in that installed rafa," + " never in a `src/` directory.",
13497
+ description: "Walks the tracker of a plan one task at a time, each in a Claude Code session whose work" + " is committed once it exits, and retries blocked tasks first. After the last task a wrap-up" + " session promotes findings, syncs with main, pushes and opens or updates the PR, and the loop" + " then waits on its checks, spending repair sessions on a red or conflicting PR. With no" + " `--plan` it runs `PLAN.md` in `plan.dir`, `.rafa/plans` unless the config names another, or" + " `PLAN.md` at the project root when that one does not exist. Before any session it checks the" + " prerequisites the config and the plan's `PREREQUISITES-<stub>.md` name, halting when a required" + " one fails and naming each failed optional one known-missing in every task prompt, and warns once" + " for each migration the effort store holds that this rafa does not know and that is additive. It" + " refuses to start while `RAFA_EFFORT_DIR` is set, since a loop records to the project's own" + " store. Started on" + " `main` or `master` it offers to create the plan's `feat/<stub>` from the latest" + " `origin/<base>` and run there, `--create-branch` answering that without asking, and refuses" + " the run when the offer is not taken. Each run writes its session record to `.rafa/runs/<session-id>.json`:" + " the plan, the branch, the pid, the start, the state and the running task. It refuses a plan whose" + " record names another branch, and a plan a session is still running. `rafa loop stop`, `pause`," + " `resume`, `status` and `list` reach the run through that record. A run holds its terminal: until" + " phase 6 it refuses `--detached`. With `--runtime` the whole run goes on in that installed rafa," + " never in a `src/` directory.",
13104
13498
  args: [],
13105
13499
  flags: [
13106
13500
  {
@@ -13195,7 +13589,7 @@ var loopStart = Object.freeze({
13195
13589
  var start_default = endingWith(loopStart);
13196
13590
 
13197
13591
  // src/commands/loop/status.ts
13198
- var USAGE24 = "rafa loop status [-s|--session-id=<id>]";
13592
+ var USAGE23 = "rafa loop status [-s|--session-id=<id>]";
13199
13593
  var BLOCKED_TASK_LINE = /^- \[BLOCKED\] (.+)/;
13200
13594
  var NO_BLOCKER = "the line trails no blocker comment";
13201
13595
  function blockerAt(checklist, lineNum) {
@@ -13231,8 +13625,8 @@ function renderStatus(status) {
13231
13625
  ];
13232
13626
  }
13233
13627
  async function runStatus(context, seams) {
13234
- expectNoArgument(context.args, USAGE24);
13235
- const { root, record } = pickSession(context, USAGE24, "live-or-newest", seams);
13628
+ expectNoArgument(context.args, USAGE23);
13629
+ const { root, record } = pickSession(context, USAGE23, "live-or-newest", seams);
13236
13630
  const checklist = readSessionChecklist(root, record);
13237
13631
  const tasks = checklist === null ? null : countTasks(checklist.tasks);
13238
13632
  let eta = null;
@@ -13285,7 +13679,7 @@ function createLoopStatusCommand(seams = {}) {
13285
13679
  var status_default2 = createLoopStatusCommand();
13286
13680
 
13287
13681
  // src/commands/loop/stop.ts
13288
- var USAGE25 = "rafa loop stop [-s|--session-id=<id>]";
13682
+ var USAGE24 = "rafa loop stop [-s|--session-id=<id>]";
13289
13683
  function signalRun(record, interrupt) {
13290
13684
  try {
13291
13685
  interrupt(record.pid);
@@ -13338,8 +13732,8 @@ function renderStop(result) {
13338
13732
  ];
13339
13733
  }
13340
13734
  async function runStop(context, seams) {
13341
- expectNoArgument(context.args, USAGE25);
13342
- const { root, record } = pickSession(context, USAGE25, "live", seams);
13735
+ expectNoArgument(context.args, USAGE24);
13736
+ const { root, record } = pickSession(context, USAGE24, "live", seams);
13343
13737
  if (!isLive(record)) {
13344
13738
  throw refusal(`Session ${record.sessionId} reads ${record.state}: no run is left to stop.`);
13345
13739
  }
@@ -13390,7 +13784,7 @@ function createLoopStopCommand(seams = {}) {
13390
13784
  var stop_default = createLoopStopCommand();
13391
13785
 
13392
13786
  // src/commands/module/exec.ts
13393
- var USAGE26 = "rafa module exec <module> <action> [args] [flags]";
13787
+ var USAGE25 = "rafa module exec <module> <action> [args] [flags]";
13394
13788
  var execCommand = {
13395
13789
  name: "module exec",
13396
13790
  subject: "module",
@@ -13419,13 +13813,13 @@ var execCommand = {
13419
13813
  const mounts = context.registry.mounts();
13420
13814
  const mounted = mounts.length === 0 ? "none is mounted; a module is mounted when allowList: names it and modules: gives its path source" : mounts.map((mount) => `${mount.name} (${context.registry.actionsOf(mountKey(mount.name)).map((held) => held.action).join(", ")})`).join(", ");
13421
13815
  throw new CommandExit(1, `\u274C Expected a module and one of its actions; mounted: ${mounted}
13422
- Usage: ${USAGE26}`);
13816
+ Usage: ${USAGE25}`);
13423
13817
  }
13424
13818
  };
13425
13819
  var exec_default = Object.freeze(execCommand);
13426
13820
 
13427
13821
  // src/commands/module/list.ts
13428
- var USAGE27 = "rafa module list";
13822
+ var USAGE26 = "rafa module list";
13429
13823
  function sourceText(source) {
13430
13824
  const ref6 = source.ref === null ? "" : `@${source.ref}`;
13431
13825
  return `${source.kind} ${source.location}${ref6}`;
@@ -13446,10 +13840,10 @@ function renderModuleList(list) {
13446
13840
  return ["Modules:", ...list.modules.flatMap(moduleLines)];
13447
13841
  }
13448
13842
  async function runList6(context, seams) {
13449
- expectNoArgument(context.args, USAGE27);
13843
+ expectNoArgument(context.args, USAGE26);
13450
13844
  const { project } = context;
13451
13845
  if (project === null)
13452
- throw new CommandExit(1, `\u274C ${USAGE27} runs inside a project`);
13846
+ throw new CommandExit(1, `\u274C ${USAGE26} runs inside a project`);
13453
13847
  let loaded;
13454
13848
  try {
13455
13849
  const resolved = loadConfig({ root: project.root, home: project.home });
@@ -13602,7 +13996,7 @@ function fastForwardBase(git, plan2) {
13602
13996
 
13603
13997
  // src/commands/next.ts
13604
13998
  var NEXT_USAGE = "rafa next [--dry-run] [--yes[=<action ids>]]";
13605
- var DRY_RUN_FLAG3 = "dry-run";
13999
+ var DRY_RUN_FLAG4 = "dry-run";
13606
14000
  var STATE_MARK = "\uD83D\uDCCD";
13607
14001
  var PROPOSAL_MARK = "\uD83D\uDC49";
13608
14002
  var STOP_MARK = "\u23F9";
@@ -13623,7 +14017,7 @@ function dryRunLine(dryRun) {
13623
14017
  if (dryRun === "no-terminal") {
13624
14018
  return `${STOP_MARK} There is no terminal to answer on, so nothing ran; run rafa next where you can answer,` + ` or type --${YES_FLAG}=${BARE_YES_ACTIONS.join(",")} to allow those steps unasked.`;
13625
14019
  }
13626
- return `${STOP_MARK} --${DRY_RUN_FLAG3}: nothing ran.`;
14020
+ return `${STOP_MARK} --${DRY_RUN_FLAG4}: nothing ran.`;
13627
14021
  }
13628
14022
  function stopLine(stop, state, ceiling, dryRun) {
13629
14023
  if (stop === "dry-run")
@@ -13719,14 +14113,14 @@ function lineRefusal2(problem2) {
13719
14113
  Usage: ${NEXT_USAGE}`);
13720
14114
  }
13721
14115
  function readDryRun(flags) {
13722
- const value = flags[DRY_RUN_FLAG3];
14116
+ const value = flags[DRY_RUN_FLAG4];
13723
14117
  if (value === undefined)
13724
14118
  return false;
13725
14119
  if (typeof value === "boolean")
13726
14120
  return value;
13727
14121
  if (value === "true" || value === "false")
13728
14122
  return value === "true";
13729
- throw lineRefusal2(`--${DRY_RUN_FLAG3} takes no value, and read "${value}" as one`);
14123
+ throw lineRefusal2(`--${DRY_RUN_FLAG4} takes no value, and read "${value}" as one`);
13730
14124
  }
13731
14125
  function dryRunOf(flag, ceiling, hasTerminal) {
13732
14126
  if (flag)
@@ -13790,7 +14184,7 @@ async function runNext(context, seams) {
13790
14184
  var YES_ID_LIST = YES_ACTIONS.join(", ");
13791
14185
  var NEXT_FLAGS = Object.freeze([
13792
14186
  {
13793
- name: DRY_RUN_FLAG3,
14187
+ name: DRY_RUN_FLAG4,
13794
14188
  description: "Print where the project stands and what to do about it, and stop without asking or running it." + " A run with no terminal to answer on and no --yes does this whether or not the flag is typed.",
13795
14189
  type: "boolean"
13796
14190
  },
@@ -13928,9 +14322,9 @@ var create_default2 = endingWith(wrapped2);
13928
14322
 
13929
14323
  // src/commands/plan/risk.ts
13930
14324
  import { readFileSync as readFileSync16 } from "fs";
13931
- import { resolve as resolve10 } from "path";
14325
+ import { resolve as resolve12 } from "path";
13932
14326
  var COMMAND2 = "rafa plan risk";
13933
- var USAGE28 = "rafa plan risk [<plan>] [--strict]";
14327
+ var USAGE27 = "rafa plan risk [<plan>] [--strict]";
13934
14328
  var STRICT_HINT = `Type the plan first: ${COMMAND2} <plan> --strict`;
13935
14329
  function spawningRunners(root) {
13936
14330
  return { git: createGitRunner(root), gh: createGhRunner({ cwd: root }) };
@@ -13941,9 +14335,9 @@ function riskPlanPath(root, planDir, typed) {
13941
14335
  return path;
13942
14336
  if (typed !== null)
13943
14337
  throw new CommandExit(1, `\u274C Plan file not found: ${path}`);
13944
- const lookedFor = [...new Set([resolve10(root, planDir, DEFAULT_PLAN_FILE), resolve10(root, DEFAULT_PLAN_FILE)])];
14338
+ const lookedFor = [...new Set([resolve12(root, planDir, DEFAULT_PLAN_FILE), resolve12(root, DEFAULT_PLAN_FILE)])];
13945
14339
  throw new CommandExit(1, `\u274C No plan named, and no default plan at ${lookedFor.join(" or ")}
13946
- Usage: ${USAGE28}`);
14340
+ Usage: ${USAGE27}`);
13947
14341
  }
13948
14342
  function riskLine(finding) {
13949
14343
  const line = finding.line === undefined ? "" : `:${String(finding.line)}`;
@@ -14007,7 +14401,7 @@ function createPlanRiskCommand(seams = {}) {
14007
14401
  outputs: ["text", "json"],
14008
14402
  run: async (context) => {
14009
14403
  const strict = readSwitch("strict", context.flags["strict"], STRICT_HINT);
14010
- const typed = expectAtMostOneArgument(context.args, USAGE28);
14404
+ const typed = expectAtMostOneArgument(context.args, USAGE27);
14011
14405
  const project = requireProject(context, COMMAND2);
14012
14406
  const config = resolveProjectConfig(project, COMMAND2, (message) => {
14013
14407
  context.output.warn(message);
@@ -14039,8 +14433,8 @@ var risk_default = createPlanRiskCommand();
14039
14433
 
14040
14434
  // src/commands/plan/show.ts
14041
14435
  import { readFileSync as readFileSync17 } from "fs";
14042
- import { join as join27 } from "path";
14043
- var USAGE29 = "rafa plan show <stub> [--tracker]";
14436
+ import { join as join32 } from "path";
14437
+ var USAGE28 = "rafa plan show <stub> [--tracker]";
14044
14438
  var COMMAND3 = "rafa plan show";
14045
14439
  function readTrackerFlag(value) {
14046
14440
  if (value === undefined || value === false || value === "false")
@@ -14057,9 +14451,9 @@ function showPlan(plans, stub, tracker) {
14057
14451
  throw new CommandExit(1, `\u274C "${stub}" is no plan stub: a stub is letters, digits, ".", "_" and "-"`);
14058
14452
  }
14059
14453
  const name2 = planFileName(stub, tracker);
14060
- const file = join27(plans.path, name2);
14454
+ const file = join32(plans.path, name2);
14061
14455
  if (!isFile(file))
14062
- throw new CommandExit(1, missingText(stub, tracker, join27(plans.label, name2)));
14456
+ throw new CommandExit(1, missingText(stub, tracker, join32(plans.label, name2)));
14063
14457
  const model = parsePlan(readFileSync17(file, "utf8"));
14064
14458
  const { header } = model;
14065
14459
  return {
@@ -14083,7 +14477,7 @@ function taskLine(task) {
14083
14477
  return ` ${checkbox(task.status)} ${task.text}`;
14084
14478
  }
14085
14479
  function renderShownPlan(shown, dirLabel) {
14086
- const relative7 = join27(dirLabel, planFileName(shown.stub, shown.tracker));
14480
+ const relative7 = join32(dirLabel, planFileName(shown.stub, shown.tracker));
14087
14481
  const fileLabel = shown.tracker ? "Tracker" : "File";
14088
14482
  const lines2 = [`Plan: ${shown.stub}`, `${fileLabel}: ${relative7}`];
14089
14483
  if (shown.header.issue !== null)
@@ -14136,7 +14530,7 @@ var planShowCommand = {
14136
14530
  outputs: ["text", "json"],
14137
14531
  run: async (context) => {
14138
14532
  const tracker = readTrackerFlag(context.flags.tracker);
14139
- const stub = expectOneArgument(context.args, USAGE29);
14533
+ const stub = expectOneArgument(context.args, USAGE28);
14140
14534
  const plans = resolvePlansDir(requireProject(context, COMMAND3), COMMAND3, (message) => {
14141
14535
  context.output.warn(message);
14142
14536
  });
@@ -14276,7 +14670,7 @@ Run rafa pr list to see the open pull requests.`, usage2);
14276
14670
  }
14277
14671
 
14278
14672
  // src/commands/pr/current.ts
14279
- var USAGE30 = PR_USAGE.current;
14673
+ var USAGE29 = PR_USAGE.current;
14280
14674
  var SEPARATOR2 = " \u2014 ";
14281
14675
  function trimmedOrNull(value) {
14282
14676
  const trimmed = value.trim();
@@ -14307,9 +14701,9 @@ async function probeChecks(pulls, number) {
14307
14701
  }
14308
14702
  }
14309
14703
  async function readCurrent(context, seams) {
14310
- expectNoArguments(context.args, USAGE30);
14704
+ expectNoArguments(context.args, USAGE29);
14311
14705
  const pr = openPrContext(context, seams);
14312
- const pick = await pickPullRequest(pr, null, USAGE30);
14706
+ const pick = await pickPullRequest(pr, null, USAGE29);
14313
14707
  const summary = pick.summary;
14314
14708
  const probe = await probeChecks(pr.pulls, pick.number);
14315
14709
  const reading = {
@@ -14362,7 +14756,7 @@ function createPrCurrentCommand(seams = DEFAULT_PR_SEAMS) {
14362
14756
  var current_default = createPrCurrentCommand();
14363
14757
 
14364
14758
  // src/commands/pr/list.ts
14365
- var USAGE31 = PR_USAGE.list;
14759
+ var USAGE30 = PR_USAGE.list;
14366
14760
  var UNREADABLE = "?";
14367
14761
  var PROBE_LIMIT = 6;
14368
14762
  var TITLE_WIDTH = 60;
@@ -14463,7 +14857,7 @@ async function rowOf2(pulls, pull) {
14463
14857
  };
14464
14858
  }
14465
14859
  async function readList(context, seams, now) {
14466
- expectNoArguments(context.args, USAGE31);
14860
+ expectNoArguments(context.args, USAGE30);
14467
14861
  const pr = openPrContext(context, seams);
14468
14862
  const open = await onProvider("list the open pull requests", () => pr.pulls.list());
14469
14863
  const rows = await mapWithLimit(open, PROBE_LIMIT, (pull) => rowOf2(pr.pulls, pull));
@@ -14503,24 +14897,24 @@ function createPrListCommand(seams = DEFAULT_PR_SEAMS) {
14503
14897
  var list_default9 = createPrListCommand();
14504
14898
 
14505
14899
  // src/commands/pr/merge.ts
14506
- import { existsSync as existsSync12, readFileSync as readFileSync19 } from "fs";
14507
- import { join as join29 } from "path";
14900
+ import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
14901
+ import { join as join34 } from "path";
14508
14902
 
14509
14903
  // src/runtime/install.ts
14510
14904
  import {
14511
14905
  chmodSync,
14512
- copyFileSync as copyFileSync3,
14513
- existsSync as existsSync11,
14514
- mkdirSync as mkdirSync5,
14515
- readdirSync as readdirSync3,
14906
+ copyFileSync as copyFileSync4,
14907
+ existsSync as existsSync14,
14908
+ mkdirSync as mkdirSync6,
14909
+ readdirSync as readdirSync4,
14516
14910
  readFileSync as readFileSync18,
14517
14911
  realpathSync as realpathSync5,
14518
14912
  renameSync as renameSync2,
14519
- rmSync as rmSync3,
14520
- statSync as statSync10,
14913
+ rmSync as rmSync4,
14914
+ statSync as statSync11,
14521
14915
  symlinkSync
14522
14916
  } from "fs";
14523
- import { basename as basename7, delimiter as delimiter2, dirname as dirname6, join as join28, relative as relative7, resolve as resolve11 } from "path";
14917
+ import { basename as basename6, delimiter as delimiter2, dirname as dirname6, join as join33, relative as relative7, resolve as resolve13 } from "path";
14524
14918
  var LINK_NAME = "rafa";
14525
14919
  var FORCE_FLAG = "--force";
14526
14920
  var EXIT_DONE = 0;
@@ -14581,20 +14975,20 @@ function runtimeRefusalLines(runtimeDir, version2) {
14581
14975
  ];
14582
14976
  }
14583
14977
  function listTrackers(repoRoot, planDir) {
14584
- const dir = resolve11(repoRoot, planDir);
14585
- return entryNames(dir).filter((entry) => TRACKER_NAME.test(entry)).sort((a, b) => a.localeCompare(b)).map((entry) => join28(dir, entry)).filter((path) => statSync10(path).isFile()).map((path) => relative7(repoRoot, path));
14978
+ const dir = resolve13(repoRoot, planDir);
14979
+ return entryNames(dir).filter((entry) => TRACKER_NAME.test(entry)).sort((a, b) => a.localeCompare(b)).map((entry) => join33(dir, entry)).filter((path) => statSync11(path).isFile()).map((path) => relative7(repoRoot, path));
14586
14980
  }
14587
14981
  function openTrackers(repoRoot, planDir) {
14588
14982
  return listTrackers(repoRoot, planDir).flatMap((path) => {
14589
- const task = findNextTask(readFileSync18(join28(repoRoot, path), "utf8"));
14983
+ const task = findNextTask(readFileSync18(join33(repoRoot, path), "utf8"));
14590
14984
  return task ? [{ path, task }] : [];
14591
14985
  });
14592
14986
  }
14593
14987
  function readManifestVersion2(repoRoot) {
14594
- const manifest = JSON.parse(readFileSync18(join28(repoRoot, "package.json"), "utf8"));
14988
+ const manifest = JSON.parse(readFileSync18(join33(repoRoot, "package.json"), "utf8"));
14595
14989
  const record = typeof manifest === "object" && manifest !== null ? manifest : {};
14596
14990
  if (record["name"] !== name) {
14597
- throw new Error(`${join28(repoRoot, "package.json")} names ${JSON.stringify(record["name"])}, not ${name}: this is no rafa checkout`);
14991
+ throw new Error(`${join33(repoRoot, "package.json")} names ${JSON.stringify(record["name"])}, not ${name}: this is no rafa checkout`);
14598
14992
  }
14599
14993
  const version2 = record["version"];
14600
14994
  if (typeof version2 !== "string" || !VERSION_SHAPE.test(version2)) {
@@ -14603,12 +14997,12 @@ function readManifestVersion2(repoRoot) {
14603
14997
  return version2;
14604
14998
  }
14605
14999
  function copyTree(from, to) {
14606
- mkdirSync5(to, { recursive: true });
15000
+ mkdirSync6(to, { recursive: true });
14607
15001
  let copied = 0;
14608
- const entries = readdirSync3(from, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
15002
+ const entries = readdirSync4(from, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
14609
15003
  for (const entry of entries) {
14610
- const source = join28(from, entry.name);
14611
- const target = join28(to, entry.name);
15004
+ const source = join33(from, entry.name);
15005
+ const target = join33(to, entry.name);
14612
15006
  if (entry.isDirectory()) {
14613
15007
  copied += copyTree(source, target);
14614
15008
  } else if (entry.isFile()) {
@@ -14622,23 +15016,23 @@ function copyTree(from, to) {
14622
15016
  }
14623
15017
  function replaceTree(from, to) {
14624
15018
  const staging = temporaryName(to);
14625
- rmSync3(staging, { recursive: true, force: true });
15019
+ rmSync4(staging, { recursive: true, force: true });
14626
15020
  try {
14627
15021
  const copied = copyTree(from, staging);
14628
15022
  swapIn2(staging, to);
14629
15023
  return copied;
14630
15024
  } catch (err) {
14631
- rmSync3(staging, { recursive: true, force: true });
15025
+ rmSync4(staging, { recursive: true, force: true });
14632
15026
  throw err;
14633
15027
  }
14634
15028
  }
14635
15029
  function swapIn2(staging, to) {
14636
- if (!existsSync11(to)) {
15030
+ if (!existsSync14(to)) {
14637
15031
  renameSync2(staging, to);
14638
15032
  return;
14639
15033
  }
14640
15034
  const outgoing = `${temporaryName(to)}.outgoing`;
14641
- rmSync3(outgoing, { recursive: true, force: true });
15035
+ rmSync4(outgoing, { recursive: true, force: true });
14642
15036
  renameSync2(to, outgoing);
14643
15037
  try {
14644
15038
  renameSync2(staging, to);
@@ -14646,17 +15040,17 @@ function swapIn2(staging, to) {
14646
15040
  renameSync2(outgoing, to);
14647
15041
  throw err;
14648
15042
  }
14649
- rmSync3(outgoing, { recursive: true, force: true });
15043
+ rmSync4(outgoing, { recursive: true, force: true });
14650
15044
  }
14651
15045
  function relink(linkPath, target) {
14652
- mkdirSync5(dirname6(linkPath), { recursive: true });
15046
+ mkdirSync6(dirname6(linkPath), { recursive: true });
14653
15047
  const temporary = temporaryName(linkPath);
14654
- rmSync3(temporary, { force: true });
15048
+ rmSync4(temporary, { force: true });
14655
15049
  try {
14656
15050
  symlinkSync(target, temporary);
14657
15051
  renameSync2(temporary, linkPath);
14658
15052
  } catch (err) {
14659
- rmSync3(temporary, { force: true });
15053
+ rmSync4(temporary, { force: true });
14660
15054
  throw err;
14661
15055
  }
14662
15056
  const resolved = realpathSync5(linkPath);
@@ -14703,20 +15097,20 @@ function installRuntime(seams, options = {}) {
14703
15097
  }
14704
15098
  function runInstall(seams, options) {
14705
15099
  const { repoRoot, home, log } = seams;
14706
- const runtimeRoot = join28(home, RUNTIME_SUBDIR);
14707
- const linkPath = join28(home, RAFA_BIN_DIR, LINK_NAME);
15100
+ const runtimeRoot = join33(home, RUNTIME_SUBDIR);
15101
+ const linkPath = join33(home, RAFA_BIN_DIR, LINK_NAME);
14708
15102
  log(`repo root: ${repoRoot}`);
14709
15103
  log(`runtime root: ${runtimeRoot}`);
14710
15104
  log(`link: ${linkPath}`);
14711
15105
  const version2 = inStage("manifest", () => readManifestVersion2(repoRoot));
14712
- const planDir = inStage("config", () => resolve11(repoRoot, loadConfig({ root: repoRoot, home }, {}, seams.warn).config.planDir));
15106
+ const planDir = inStage("config", () => resolve13(repoRoot, loadConfig({ root: repoRoot, home }, {}, seams.warn).config.planDir));
14713
15107
  log(`plan dir: ${planDir}`);
14714
15108
  const open = inStage("trackers", () => openTrackers(repoRoot, planDir));
14715
15109
  if (open.length > 0)
14716
15110
  return { kind: "refused", reason: "trackers", planDir, trackers: open };
14717
- const distDir = join28(repoRoot, "dist");
14718
- const runtimeDir = join28(runtimeRoot, version2);
14719
- const installed = existsSync11(runtimeDir);
15111
+ const distDir = join33(repoRoot, "dist");
15112
+ const runtimeDir = join33(runtimeRoot, version2);
15113
+ const installed = existsSync14(runtimeDir);
14720
15114
  if (installed && options.force !== true) {
14721
15115
  return { kind: "refused", reason: "runtime-exists", version: version2, runtimeDir };
14722
15116
  }
@@ -14724,13 +15118,13 @@ function runInstall(seams, options) {
14724
15118
  const code = inStage("build", () => seams.build(repoRoot));
14725
15119
  if (code !== 0)
14726
15120
  throw new StageFailure("build", `the build exited ${code}`);
14727
- if (!existsSync11(join28(distDir, RUNTIME_ENTRY))) {
14728
- throw new StageFailure("build", `the build exited 0 and left no ${join28(distDir, RUNTIME_ENTRY)}`);
15121
+ if (!existsSync14(join33(distDir, RUNTIME_ENTRY))) {
15122
+ throw new StageFailure("build", `the build exited 0 and left no ${join33(distDir, RUNTIME_ENTRY)}`);
14729
15123
  }
14730
15124
  log(installed ? `copying ${distDir} over ${runtimeDir}, replacing it whole` : `copying ${distDir} into ${runtimeDir}`);
14731
15125
  const copied = inStage("copy", () => replaceTree(distDir, runtimeDir));
14732
15126
  log(`copied ${copied} file(s)`);
14733
- const resolved = inStage("link", () => relink(linkPath, join28(runtimeDir, RUNTIME_ENTRY)));
15127
+ const resolved = inStage("link", () => relink(linkPath, join33(runtimeDir, RUNTIME_ENTRY)));
14734
15128
  log(`${linkPath} resolves to ${resolved}`);
14735
15129
  return { kind: "done", version: version2, runtimeDir, linkPath, resolved, copied };
14736
15130
  }
@@ -14745,7 +15139,7 @@ function inStage(stage, run) {
14745
15139
  }
14746
15140
  function entryNames(dir) {
14747
15141
  try {
14748
- return readdirSync3(dir);
15142
+ return readdirSync4(dir);
14749
15143
  } catch (err) {
14750
15144
  if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT")
14751
15145
  return [];
@@ -14755,16 +15149,16 @@ function entryNames(dir) {
14755
15149
  function replaceFile(source, target) {
14756
15150
  const temporary = temporaryName(target);
14757
15151
  try {
14758
- copyFileSync3(source, temporary);
14759
- chmodSync(temporary, statSync10(source).mode & 4095);
15152
+ copyFileSync4(source, temporary);
15153
+ chmodSync(temporary, statSync11(source).mode & 4095);
14760
15154
  renameSync2(temporary, target);
14761
15155
  } catch (err) {
14762
- rmSync3(temporary, { force: true });
15156
+ rmSync4(temporary, { force: true });
14763
15157
  throw err;
14764
15158
  }
14765
15159
  }
14766
15160
  function temporaryName(path) {
14767
- return join28(dirname6(path), `.${basename7(path)}.snapshot-${process.pid}`);
15161
+ return join33(dirname6(path), `.${basename6(path)}.snapshot-${process.pid}`);
14768
15162
  }
14769
15163
 
14770
15164
  // src/commands/pr/merge-followups.ts
@@ -15070,7 +15464,7 @@ async function postUncheckedComment(pulls, number, unchecked, warn3) {
15070
15464
  }
15071
15465
 
15072
15466
  // src/commands/pr/merge.ts
15073
- var USAGE32 = PR_USAGE.merge;
15467
+ var USAGE31 = PR_USAGE.merge;
15074
15468
  var REMOTE3 = "origin";
15075
15469
  var INDENT4 = " ";
15076
15470
  var YES_ANSWERS4 = ["y", "yes"];
@@ -15149,6 +15543,16 @@ function remoteHoldsBranch(git, branch, warn3) {
15149
15543
  warn3(`${REMOTE3} could not be asked whether it still holds ${branch}, so it is left alone: ${gitSaid(probe2)}`);
15150
15544
  return false;
15151
15545
  }
15546
+ function localHoldsBranch(git, branch, warn3) {
15547
+ const probe2 = git(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
15548
+ if (probe2.ok)
15549
+ return true;
15550
+ const said = gitSaid(probe2);
15551
+ if (said === "")
15552
+ return false;
15553
+ warn3(`could not read whether this checkout holds a local branch ${branch}, so its delete runs: ${said}`);
15554
+ return true;
15555
+ }
15152
15556
  function textOf(path) {
15153
15557
  try {
15154
15558
  return readFileSync19(path, "utf8");
@@ -15157,7 +15561,7 @@ function textOf(path) {
15157
15561
  }
15158
15562
  }
15159
15563
  function followUpsFor(pr, git) {
15160
- const facts = readPackageFacts(textOf(join29(pr.project.root, "package.json")));
15564
+ const facts = readPackageFacts(textOf(join34(pr.project.root, "package.json")));
15161
15565
  if (facts.version === null)
15162
15566
  return [];
15163
15567
  const tags = git(["tag", "--list", versionTag(facts.version)]);
@@ -15165,7 +15569,7 @@ function followUpsFor(pr, git) {
15165
15569
  version: facts.version,
15166
15570
  tagged: tags.ok && tags.stdout.trim() !== "",
15167
15571
  rafaCheckout: facts.rafaCheckout,
15168
- runtimeInstalled: existsSync12(join29(pr.project.home, RUNTIME_SUBDIR, facts.version))
15572
+ runtimeInstalled: existsSync15(join34(pr.project.home, RUNTIME_SUBDIR, facts.version))
15169
15573
  });
15170
15574
  }
15171
15575
  function openGh(pr, seams) {
@@ -15226,25 +15630,34 @@ async function reportTick(context, pr, seams, detail) {
15226
15630
  }
15227
15631
  return tick;
15228
15632
  }
15229
- function readyLine(base, branch, remoteBranchPresent) {
15230
- const remote = remoteBranchPresent ? `and ${branch} is gone locally and on ${REMOTE3}.` : `and ${branch} is gone locally; ${REMOTE3} had already deleted it.`;
15231
- return `${base} is checked out and pulled, ${remote}`;
15633
+ function readyLine(base, branch, present) {
15634
+ const pulled = `${base} is checked out and pulled`;
15635
+ if (present.local) {
15636
+ return present.remote ? `${pulled}, and ${branch} is gone locally and on ${REMOTE3}.` : `${pulled}, and ${branch} is gone locally; ${REMOTE3} had already deleted it.`;
15637
+ }
15638
+ return present.remote ? `${pulled}, and ${branch} is gone on ${REMOTE3}; there was no local branch to delete.` : `${pulled}; ${branch} had no local branch, and ${REMOTE3} had already deleted it.`;
15232
15639
  }
15233
15640
  function quotedLines2(said) {
15234
15641
  return said === "" ? [] : said.split(`
15235
15642
  `).map((line) => `${INDENT4}${line}`);
15236
15643
  }
15237
- function runCleanUp(context, git, detail, remoteBranchPresent) {
15644
+ function runCleanUp(context, git, detail, present) {
15238
15645
  const steps = cleanUpSteps({
15239
15646
  branch: detail.headRefName,
15240
15647
  base: detail.baseRefName,
15241
15648
  remote: REMOTE3,
15242
- remoteBranchPresent
15649
+ localBranchPresent: present.local,
15650
+ remoteBranchPresent: present.remote
15243
15651
  });
15244
15652
  const reports = [];
15245
15653
  for (const step of steps) {
15654
+ if (step.skip !== undefined) {
15655
+ reports.push({ id: step.id, label: step.label, command: commandLine(step), ok: true, skipped: true });
15656
+ context.output.info(`${step.label}: skipped \u2014 ${step.skip}`);
15657
+ continue;
15658
+ }
15246
15659
  const result = git(step.argv.slice(1));
15247
- reports.push({ id: step.id, label: step.label, command: commandLine(step), ok: result.ok });
15660
+ reports.push({ id: step.id, label: step.label, command: commandLine(step), ok: result.ok, skipped: false });
15248
15661
  if (result.ok) {
15249
15662
  context.output.info(`${step.label}: done`);
15250
15663
  continue;
@@ -15287,17 +15700,17 @@ async function commentIfUnchecked(context, pr, number, unchecked) {
15287
15700
  return posted.url;
15288
15701
  }
15289
15702
  async function runMerge(context, seams) {
15290
- const yes = readBooleanFlag(context.flags, "yes", USAGE32);
15291
- const skipChecks = readBooleanFlag(context.flags, "skip-checks", USAGE32);
15292
- const wanted = readMethodFlag(context.flags, USAGE32);
15293
- const asked = readPullArgument(context.args, USAGE32);
15703
+ const yes = readBooleanFlag(context.flags, "yes", USAGE31);
15704
+ const skipChecks = readBooleanFlag(context.flags, "skip-checks", USAGE31);
15705
+ const wanted = readMethodFlag(context.flags, USAGE31);
15706
+ const asked = readPullArgument(context.args, USAGE31);
15294
15707
  const pr = openPrContext(context, seams);
15295
15708
  const git = (seams.git ?? createGitRunner)(pr.project.root);
15296
15709
  const method = wanted ?? pr.mergeMethod;
15297
- const pick = await pickPullRequest(pr, asked, USAGE32);
15710
+ const pick = await pickPullRequest(pr, asked, USAGE31);
15298
15711
  const detail = await onProvider(`read pull request #${pick.number}`, () => pr.pulls.get(pick.number));
15299
15712
  if (detail === null) {
15300
- throw lineRefusal3(`No pull request #${pick.number} at ${pr.project.root}`, USAGE32);
15713
+ throw lineRefusal3(`No pull request #${pick.number} at ${pr.project.root}`, USAGE31);
15301
15714
  }
15302
15715
  const checks = await onProvider(`read the checks of #${pick.number}`, () => pr.pulls.checks(pick.number));
15303
15716
  recordPlanCi({
@@ -15340,11 +15753,15 @@ async function runMerge(context, seams) {
15340
15753
  context.output.info(`Merged #${detail.number} into ${detail.baseRefName} (${method}).`);
15341
15754
  const commentUrl = await commentIfUnchecked(context, pr, detail.number, answer2.unchecked);
15342
15755
  const roadmapTicks = await reportTick(context, pr, seams, detail);
15343
- const remoteBranchPresent = remoteHoldsBranch(git, detail.headRefName, (message) => {
15756
+ const warn3 = (message) => {
15344
15757
  context.output.warn(message);
15345
- });
15346
- const steps = runCleanUp(context, git, detail, remoteBranchPresent);
15347
- context.output.info(readyLine(detail.baseRefName, detail.headRefName, remoteBranchPresent));
15758
+ };
15759
+ const present = {
15760
+ remote: remoteHoldsBranch(git, detail.headRefName, warn3),
15761
+ local: localHoldsBranch(git, detail.headRefName, warn3)
15762
+ };
15763
+ const steps = runCleanUp(context, git, detail, present);
15764
+ context.output.info(readyLine(detail.baseRefName, detail.headRefName, present));
15348
15765
  const followUps = followUpsFor(pr, git);
15349
15766
  if (followUps.length > 0) {
15350
15767
  context.output.info("Follow-ups:");
@@ -15371,7 +15788,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
15371
15788
  subject: "pr",
15372
15789
  action: "merge",
15373
15790
  summary: "merge a pull request and clean up both branches",
15374
- description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow)," + " and after the merge posts one comment on the pull request saying so. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
15791
+ description: "Merges one pull request through the GitHub CLI and then, in code, switches to the base branch," + " pulls it fast-forward only, deletes the head branch locally and on the remote where it is still there," + " and prunes, reporting each step; a head branch this checkout has no local branch for is reported as a" + " skipped local delete, and the rest still run. Refuses before it asks anything on a dirty working tree, on a pull" + " request that is not green or does not merge, and on a head branch checked out in another worktree. It" + " shows the pull request, its branches and the method and asks `Merge? [y/N]`; `--yes` skips the question," + " and without a terminal and without `--yes` it refuses. A pull request that reports no checks at all is" + " refused unless `--skip-checks` is given, which is refused on any pull request that does report checks;" + " with it the command reads how many workflows the repository defines, prints a warning for that case, asks" + " `Merge #<n> with no checks? [y/N]` (which `--yes` answers only where the repository defines no workflow)," + " and after the merge posts one comment on the pull request saying so. A step that fails never undoes the merge: it" + " prints what is left as commands to paste and exits 1. After the merge it ticks the `Closes #<n>` line of" + " every issue the pull request closes on every open board whose checklist lists it, or on the roadmap issue while no issue carries `type:roadmap`, warning rather than failing when that write" + ' does not land. It ends by reading every open issue whose "Blocked by:" line names an issue this pull' + " request closes, asking whether to remove `spec:blocked` from each one whose blockers have all closed;" + " `--yes` does not answer that question, and every failure of that reading is a warning. With" + " `--output=json` the pull request, the method, the steps that ran, the follow-ups, the roadmap tick and" + " the unblock reading are the data of the terminal result event. Refuses with exit code 2 where" + " `pr.provider` is not `gh`.",
15375
15792
  args: [
15376
15793
  {
15377
15794
  name: "n",
@@ -15864,7 +16281,7 @@ function readLastTriage(comments) {
15864
16281
  }
15865
16282
 
15866
16283
  // src/commands/pr/show.ts
15867
- var USAGE33 = PR_USAGE.show;
16284
+ var USAGE32 = PR_USAGE.show;
15868
16285
  var INDENT5 = " ";
15869
16286
  var SHORT_SHA = 7;
15870
16287
  function headLine3(detail) {
@@ -15962,12 +16379,12 @@ async function probe2(call) {
15962
16379
  }
15963
16380
  }
15964
16381
  async function readShow(context, seams) {
15965
- const asked = readPullArgument(context.args, USAGE33);
16382
+ const asked = readPullArgument(context.args, USAGE32);
15966
16383
  const pr = openPrContext(context, seams);
15967
- const pick = await pickPullRequest(pr, asked, USAGE33);
16384
+ const pick = await pickPullRequest(pr, asked, USAGE32);
15968
16385
  const detail = await onProvider(`read pull request #${pick.number}`, () => pr.pulls.get(pick.number));
15969
16386
  if (detail === null) {
15970
- throw lineRefusal3(`No pull request #${pick.number} in the repository at ${pr.project.root}`, USAGE33);
16387
+ throw lineRefusal3(`No pull request #${pick.number} in the repository at ${pr.project.root}`, USAGE32);
15971
16388
  }
15972
16389
  const checks = await probe2(() => pr.pulls.checks(pick.number));
15973
16390
  const comments = await probe2(() => pr.pulls.comments(pick.number));
@@ -16390,8 +16807,8 @@ function describeAge(updatedAt, now) {
16390
16807
  return `${plural4(hours, "hour", "hours")} ago`;
16391
16808
  return `${plural4(Math.floor(age / DAY_MS2), "day", "days")} ago`;
16392
16809
  }
16393
- function triageCommand(number, resolve12 = false) {
16394
- return resolve12 ? `rafa pr triage ${number} --resolve` : `rafa pr triage ${number}`;
16810
+ function triageCommand(number, resolve14 = false) {
16811
+ return resolve14 ? `rafa pr triage ${number} --resolve` : `rafa pr triage ${number}`;
16395
16812
  }
16396
16813
  function joinNumbers(pulls) {
16397
16814
  const names = pulls.map((pull) => `#${pull.number}`);
@@ -16402,14 +16819,14 @@ function joinNumbers(pulls) {
16402
16819
  function byNumber(pulls) {
16403
16820
  return Object.freeze([...pulls].sort((left, right) => left.number - right.number));
16404
16821
  }
16405
- function skipped(pull, now, resolve12) {
16822
+ function skipped(pull, now, resolve14) {
16406
16823
  return Object.freeze({
16407
16824
  number: pull.number,
16408
16825
  title: pull.title,
16409
16826
  updatedAt: pull.updatedAt,
16410
16827
  age: describeAge(pull.updatedAt, now),
16411
16828
  fresh: isFreshCandidate(pull.updatedAt, now),
16412
- command: triageCommand(pull.number, resolve12)
16829
+ command: triageCommand(pull.number, resolve14)
16413
16830
  });
16414
16831
  }
16415
16832
  function skippedLine(one) {
@@ -16453,15 +16870,15 @@ function listReading(rest, count2) {
16453
16870
  return listing("list", Object.freeze([]), rest, headline);
16454
16871
  }
16455
16872
  function selectTriagePullRequests(input) {
16456
- const resolve12 = input.resolve === true;
16873
+ const resolve14 = input.resolve === true;
16457
16874
  const { current, now } = input;
16458
16875
  if (current !== null) {
16459
16876
  return plain("branch", Object.freeze([current]), `assessing #${current.number}, the pull request open on ${current.headRefName}`);
16460
16877
  }
16461
16878
  const candidates = byNumber(input.candidates);
16462
16879
  const count2 = candidates.length;
16463
- const lines2 = candidates.map((pull) => skipped(pull, now, resolve12));
16464
- if (resolve12 && count2 > 1)
16880
+ const lines2 = candidates.map((pull) => skipped(pull, now, resolve14));
16881
+ if (resolve14 && count2 > 1)
16465
16882
  return refusal12(lines2, count2);
16466
16883
  if (count2 === 0) {
16467
16884
  return plain("none", Object.freeze([]), "no red pull request to triage, and no pull request on the current branch");
@@ -16796,8 +17213,8 @@ function withTaskBudget(plan2, usd) {
16796
17213
  }
16797
17214
 
16798
17215
  // src/pr/plans/load.ts
16799
- import { existsSync as existsSync13, readFileSync as readFileSync20 } from "fs";
16800
- import { dirname as dirname7, join as join30 } from "path";
17216
+ import { existsSync as existsSync16, readFileSync as readFileSync20 } from "fs";
17217
+ import { dirname as dirname7, join as join35 } from "path";
16801
17218
  import { fileURLToPath as fileURLToPath4 } from "url";
16802
17219
  var MODULE_DIR4 = dirname7(fileURLToPath4(import.meta.url));
16803
17220
  var PINNED_PLANS_DIRNAME = "plans";
@@ -16814,8 +17231,8 @@ function hasPinnedPlan(triageClass) {
16814
17231
  function pinnedPlanCandidates(moduleDir, triageClass) {
16815
17232
  const name2 = pinnedPlanFileName(triageClass);
16816
17233
  return [
16817
- join30(moduleDir, PINNED_PLANS_DIRNAME, name2),
16818
- join30(moduleDir, name2)
17234
+ join35(moduleDir, PINNED_PLANS_DIRNAME, name2),
17235
+ join35(moduleDir, name2)
16819
17236
  ];
16820
17237
  }
16821
17238
  function readPinnedPlan(triageClass, lookup = {}) {
@@ -16823,7 +17240,7 @@ function readPinnedPlan(triageClass, lookup = {}) {
16823
17240
  throw new Error(`No pinned resolve plan ships for ${triageClass}; ` + `one ships for ${PINNED_PLAN_CLASSES.join(", ")}`);
16824
17241
  }
16825
17242
  const candidates = pinnedPlanCandidates(lookup.moduleDir ?? MODULE_DIR4, triageClass);
16826
- const found = candidates.find((candidate) => existsSync13(candidate));
17243
+ const found = candidates.find((candidate) => existsSync16(candidate));
16827
17244
  if (found === undefined) {
16828
17245
  throw new Error(`The pinned resolve plan for ${triageClass} is missing: ` + `no file at ${candidates.join(" or ")}`);
16829
17246
  }
@@ -16967,20 +17384,20 @@ function readAttemptRepeat(input) {
16967
17384
  }
16968
17385
 
16969
17386
  // src/commands/pr/resolve-loop.ts
16970
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
16971
- import { join as join31 } from "path";
16972
- var RESOLVE_SUBDIR = join31(".rafa", "resolve");
17387
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
17388
+ import { join as join36 } from "path";
17389
+ var RESOLVE_SUBDIR = join36(".rafa", "resolve");
16973
17390
  var RUN_PREFIX = "pr-";
16974
17391
  var ATTEMPT_PREFIX = "attempt-";
16975
17392
  var LOOP_WORDS = Object.freeze(["loop", "start"]);
16976
17393
  var CHILD_OUTPUT = "text";
16977
17394
  function resolveRunDir(home, number, attempt3) {
16978
- return join31(home, RESOLVE_SUBDIR, `${RUN_PREFIX}${number}`, `${ATTEMPT_PREFIX}${attempt3}`);
17395
+ return join36(home, RESOLVE_SUBDIR, `${RUN_PREFIX}${number}`, `${ATTEMPT_PREFIX}${attempt3}`);
16979
17396
  }
16980
17397
  function writeResolvePlan(write) {
16981
17398
  const dir = resolveRunDir(write.home, write.number, write.attempt);
16982
- mkdirSync6(dir, { recursive: true });
16983
- const path = join31(dir, pinnedPlanFileName(write.triageClass));
17399
+ mkdirSync7(dir, { recursive: true });
17400
+ const path = join36(dir, pinnedPlanFileName(write.triageClass));
16984
17401
  writeFileSync5(path, write.plan, "utf8");
16985
17402
  return path;
16986
17403
  }
@@ -17339,7 +17756,7 @@ async function runAttempts(run, detail, first) {
17339
17756
  }
17340
17757
 
17341
17758
  // src/commands/pr/triage.ts
17342
- var USAGE34 = PR_USAGE.triage;
17759
+ var USAGE33 = PR_USAGE.triage;
17343
17760
  var DEFAULT_MAX_ATTEMPTS = 2;
17344
17761
  var DETACHED_HEAD2 = "HEAD";
17345
17762
  var RESOLVED = false;
@@ -17380,22 +17797,22 @@ async function redPullRequests(pr) {
17380
17797
  }
17381
17798
  return red;
17382
17799
  }
17383
- async function selectTargets(pr, warn3, now, resolve12) {
17800
+ async function selectTargets(pr, warn3, now, resolve14) {
17384
17801
  const branch = readBranchOrNone(pr, warn3);
17385
17802
  const current = branch === null ? null : await onProvider(`read the open pull request for the branch "${branch}"`, () => pr.pulls.findOpen(branch));
17386
17803
  if (current !== null)
17387
- return selectTriagePullRequests({ current, candidates: [], now, resolve: resolve12 });
17804
+ return selectTriagePullRequests({ current, candidates: [], now, resolve: resolve14 });
17388
17805
  return selectTriagePullRequests({
17389
17806
  current: null,
17390
17807
  candidates: await redPullRequests(pr),
17391
17808
  now,
17392
- resolve: resolve12
17809
+ resolve: resolve14
17393
17810
  });
17394
17811
  }
17395
17812
  async function detailOf4(pr, number) {
17396
17813
  const detail = await onProvider(`read pull request #${number}`, () => pr.pulls.get(number));
17397
17814
  if (detail === null) {
17398
- throw lineRefusal3(`No pull request #${number} in the repository at ${pr.project.root}`, USAGE34);
17815
+ throw lineRefusal3(`No pull request #${number} in the repository at ${pr.project.root}`, USAGE33);
17399
17816
  }
17400
17817
  return detail;
17401
17818
  }
@@ -17498,14 +17915,14 @@ async function assessOne(options) {
17498
17915
  function refuseResolveWithoutComment(wantsResolve, wantsComment) {
17499
17916
  if (!wantsResolve || wantsComment)
17500
17917
  return;
17501
- throw lineRefusal3("--resolve writes the attempt count into the triage comment, and --no-comment writes none;" + " run one or the other", USAGE34);
17918
+ throw lineRefusal3("--resolve writes the attempt count into the triage comment, and --no-comment writes none;" + " run one or the other", USAGE33);
17502
17919
  }
17503
17920
  async function runTriage(context, seams) {
17504
- const wantsComment = readBooleanFlag(context.flags, "comment", USAGE34, true);
17505
- const wantsResolve = readBooleanFlag(context.flags, "resolve", USAGE34, false);
17506
- const maxAttempts = readMaxAttempts(context.flags, USAGE34);
17921
+ const wantsComment = readBooleanFlag(context.flags, "comment", USAGE33, true);
17922
+ const wantsResolve = readBooleanFlag(context.flags, "resolve", USAGE33, false);
17923
+ const maxAttempts = readMaxAttempts(context.flags, USAGE33);
17507
17924
  refuseResolveWithoutComment(wantsResolve, wantsComment);
17508
- const asked = readPullArgument(context.args, USAGE34);
17925
+ const asked = readPullArgument(context.args, USAGE33);
17509
17926
  const pr = openPrContext(context, seams);
17510
17927
  const git = (seams.git ?? createGitRunner)(pr.project.root);
17511
17928
  const permissions = (seams.permissions ?? ghPermissionsIn)(pr.project.root);
@@ -17518,7 +17935,7 @@ async function runTriage(context, seams) {
17518
17935
  }
17519
17936
  const numbers = asked === null ? (selection?.assess ?? []).map((one) => one.number) : [asked];
17520
17937
  const readings = [];
17521
- let resolve12 = null;
17938
+ let resolve14 = null;
17522
17939
  for (const number of numbers) {
17523
17940
  const assess = () => assessOne({
17524
17941
  pr,
@@ -17537,7 +17954,7 @@ async function runTriage(context, seams) {
17537
17954
  readings.push(reading);
17538
17955
  continue;
17539
17956
  }
17540
- resolve12 = await resolvePullRequest({
17957
+ resolve14 = await resolvePullRequest({
17541
17958
  trust: {
17542
17959
  permissions,
17543
17960
  trustedAuthors: pr.trustedAuthors,
@@ -17558,16 +17975,16 @@ async function runTriage(context, seams) {
17558
17975
  ...seams.clock === undefined ? {} : { clock: seams.clock },
17559
17976
  ...seams.sleep === undefined ? {} : { sleep: seams.sleep }
17560
17977
  });
17561
- readings.push(resolve12.reading);
17978
+ readings.push(resolve14.reading);
17562
17979
  }
17563
17980
  const text = [
17564
17981
  ...selection === null ? [] : [selection.message],
17565
17982
  ...readings.length === 0 ? [] : [renderTriages(readings)],
17566
- ...resolve12 === null ? [] : resolve12.lines
17983
+ ...resolve14 === null ? [] : resolve14.lines
17567
17984
  ].join(`
17568
17985
 
17569
17986
  `);
17570
- return { selection, readings, resolve: resolve12, text, exitCode: resolve12?.exitCode ?? 0 };
17987
+ return { selection, readings, resolve: resolve14, text, exitCode: resolve14?.exitCode ?? 0 };
17571
17988
  }
17572
17989
  function createPrTriageCommand(seams = DEFAULT_TRIAGE_SEAMS) {
17573
17990
  const command = {
@@ -17641,7 +18058,7 @@ function createPrTriageCommand(seams = DEFAULT_TRIAGE_SEAMS) {
17641
18058
  var triage_default = createPrTriageCommand();
17642
18059
 
17643
18060
  // src/commands/pr/view.ts
17644
- var USAGE35 = PR_USAGE.view;
18061
+ var USAGE34 = PR_USAGE.view;
17645
18062
  function trimmedOrNull3(value) {
17646
18063
  const trimmed = value.trim();
17647
18064
  return trimmed === "" ? null : trimmed;
@@ -17651,9 +18068,9 @@ function renderView(number, url) {
17651
18068
  return url === null ? opened : `${opened}${SEPARATOR2}${url}`;
17652
18069
  }
17653
18070
  async function runView2(context, seams) {
17654
- const asked = readPullArgument(context.args, USAGE35);
18071
+ const asked = readPullArgument(context.args, USAGE34);
17655
18072
  const pr = openPrContext(context, seams);
17656
- const pick = await pickPullRequest(pr, asked, USAGE35);
18073
+ const pick = await pickPullRequest(pr, asked, USAGE34);
17657
18074
  await onProvider(`open pull request #${pick.number} in the browser`, () => pr.pulls.browse(pick.number));
17658
18075
  const url = trimmedOrNull3(pick.summary?.url ?? "");
17659
18076
  return {
@@ -17708,7 +18125,7 @@ function createPrViewCommand(seams = DEFAULT_PR_SEAMS) {
17708
18125
  var view_default = createPrViewCommand();
17709
18126
 
17710
18127
  // src/commands/pr/wait.ts
17711
- var USAGE36 = PR_USAGE.wait;
18128
+ var USAGE35 = PR_USAGE.wait;
17712
18129
  var TIMEOUT_FLAG = "timeout";
17713
18130
  var DEFAULT_WAIT_TIMEOUT_MIN = DEFAULT_CI_TIMEOUT_MIN;
17714
18131
  var MS_PER_MIN = 60000;
@@ -17774,10 +18191,10 @@ function renderWait(reading) {
17774
18191
  `);
17775
18192
  }
17776
18193
  async function runWait(context, seams) {
17777
- const minutes = readTimeoutMinutes(context.flags, USAGE36);
17778
- const asked = readPullArgument(context.args, USAGE36);
18194
+ const minutes = readTimeoutMinutes(context.flags, USAGE35);
18195
+ const asked = readPullArgument(context.args, USAGE35);
17779
18196
  const pr = openPrContext(context, seams);
17780
- const pick = await pickPullRequest(pr, asked, USAGE36);
18197
+ const pick = await pickPullRequest(pr, asked, USAGE35);
17781
18198
  const timeoutMs = minutes * MS_PER_MIN;
17782
18199
  context.output.info(`Waiting for the checks of #${pick.number}, up to ${minutes} min.`);
17783
18200
  const waited = await waitForChecks({
@@ -17857,12 +18274,51 @@ var wait_default = createPrWaitCommand();
17857
18274
 
17858
18275
  // src/commands/release/tag.ts
17859
18276
  import { readFileSync as readFileSync21 } from "fs";
17860
- import { join as join32 } from "path";
18277
+ import { join as join37 } from "path";
18278
+
18279
+ // src/commands/release/release-commit.ts
18280
+ function unread2(reason, message) {
18281
+ return { commit: null, ahead: 0, problem: { reason, message } };
18282
+ }
18283
+ function versionAt(git, commit, path) {
18284
+ const shown = git(["show", `${commit}:./${path}`]);
18285
+ return shown.ok ? readManifestVersion(shown.stdout) : null;
18286
+ }
18287
+ function readReleaseCommit(git, path, version2) {
18288
+ const log = git(["log", "--first-parent", "--format=%H", "HEAD", "--", `./${path}`]);
18289
+ if (!log.ok)
18290
+ return unread2("git", `the history of ${path} could not be read: ${gitSaid(log)}`);
18291
+ const changed = log.stdout.split(`
18292
+ `).map((line) => line.trim()).filter((line) => line !== "");
18293
+ if (changed.length === 0) {
18294
+ return unread2("version", `no commit on this branch holds ${path}; commit it before tagging`);
18295
+ }
18296
+ let release = null;
18297
+ let newest;
18298
+ for (const commit of changed) {
18299
+ const declared = versionAt(git, commit, path);
18300
+ if (newest === undefined)
18301
+ newest = declared;
18302
+ if (declared !== version2)
18303
+ break;
18304
+ release = commit;
18305
+ }
18306
+ if (release === null) {
18307
+ return unread2("version", `${path} says ${version2}, and the last commit on this branch that changed it` + ` says ${newest ?? "no version"}; commit the version before tagging it`);
18308
+ }
18309
+ const count2 = git(["rev-list", "--count", "--first-parent", `${release}..HEAD`]);
18310
+ if (!count2.ok)
18311
+ return unread2("git", `the commits past ${release} could not be counted: ${gitSaid(count2)}`);
18312
+ return { commit: release, ahead: Number.parseInt(count2.stdout.trim(), 10), problem: null };
18313
+ }
18314
+
18315
+ // src/commands/release/tag.ts
17861
18316
  var RELEASE_TAG_USAGE = "rafa release tag";
17862
18317
  var DEFAULT_RELEASE_BRANCH = "main";
17863
18318
  var DEFAULT_REGISTRY = "https://registry.npmjs.org";
17864
18319
  var RELEASE_REMOTE = "origin";
17865
18320
  var INDENT8 = " ";
18321
+ var SHORT_COMMIT = 7;
17866
18322
  function readBranch2(git) {
17867
18323
  const result = git(["rev-parse", "--abbrev-ref", "HEAD"]);
17868
18324
  if (!result.ok) {
@@ -17882,7 +18338,7 @@ function textOf2(path) {
17882
18338
  }
17883
18339
  }
17884
18340
  function readVersion(root, configured) {
17885
- const resolved = join32(root, configured);
18341
+ const resolved = join37(root, configured);
17886
18342
  const text = textOf2(resolved);
17887
18343
  if (text === null) {
17888
18344
  return {
@@ -17896,7 +18352,7 @@ function readVersion(root, configured) {
17896
18352
  return version2 === null ? { path: configured, text, version: null, problem: `${resolved} declares no version this can read` } : { path: configured, text, version: version2, problem: null };
17897
18353
  }
17898
18354
  function readNewestRelease(root, configured) {
17899
- const resolved = join32(root, configured);
18355
+ const resolved = join37(root, configured);
17900
18356
  const text = textOf2(resolved);
17901
18357
  if (text === null) {
17902
18358
  return { path: configured, version: null, problem: `the changelog could not be read at ${resolved}` };
@@ -17939,18 +18395,22 @@ function readPublishTarget(text) {
17939
18395
  isPrivate: fields["private"] === true
17940
18396
  };
17941
18397
  }
17942
- function followUpsFor2(tag, version2, target) {
18398
+ var ON_HEAD = Object.freeze({ ahead: 0, branch: DEFAULT_RELEASE_BRANCH });
18399
+ function followUpsFor2(tag, version2, target, place2 = ON_HEAD) {
17943
18400
  const push = {
17944
18401
  command: `git push ${RELEASE_REMOTE} ${tag}`,
17945
18402
  why: `the tag is local until ${RELEASE_REMOTE} has it`
17946
18403
  };
17947
18404
  if (target.name === null || target.isPrivate)
17948
18405
  return [push];
18406
+ const publishes = `publishes ${target.name}@${version2} to ${target.registry}`;
18407
+ if (place2.ahead === 0)
18408
+ return [push, { command: `${target.manager} publish`, why: publishes }];
17949
18409
  return [
17950
18410
  push,
17951
18411
  {
17952
- command: `${target.manager} publish`,
17953
- why: `publishes ${target.name}@${version2} to ${target.registry}`
18412
+ command: `git switch --detach ${tag} && ${target.manager} publish && git switch ${place2.branch}`,
18413
+ why: `${publishes} from the tagged commit, not HEAD; skip it if ${target.registry} has that version already`
17954
18414
  }
17955
18415
  ];
17956
18416
  }
@@ -17990,7 +18450,14 @@ function decideTag(inputs) {
17990
18450
  const disagrees = changelogRefusal(inputs, version2);
17991
18451
  if (disagrees !== null)
17992
18452
  return disagrees;
17993
- return { kind: "ready", version: version2, tag: versionTag(version2) };
18453
+ const release = inputs.release;
18454
+ if (release === null)
18455
+ return refuse2("version", `${inputs.version.path} was not looked up in the history`);
18456
+ if (release.problem !== null)
18457
+ return refuse2(release.problem.reason, release.problem.message);
18458
+ if (release.commit === null)
18459
+ return refuse2("git", `no commit could be named for ${version2}`);
18460
+ return { kind: "ready", version: version2, tag: versionTag(version2), commit: release.commit, ahead: release.ahead };
17994
18461
  }
17995
18462
  function projectOf9(context) {
17996
18463
  if (context.project === null)
@@ -18013,23 +18480,34 @@ function tagConfig(project, warn3) {
18013
18480
  }
18014
18481
  }
18015
18482
  function readTagInputs(root, git, config) {
18483
+ const branch = readBranch2(git);
18484
+ const version2 = readVersion(root, config.versionFile);
18485
+ const walkable = version2.version !== null && branch.branch === config.releaseBranch;
18016
18486
  return {
18017
18487
  releaseBranch: config.releaseBranch,
18018
- branch: readBranch2(git),
18019
- version: readVersion(root, config.versionFile),
18488
+ branch,
18489
+ version: version2,
18020
18490
  changelog: readNewestRelease(root, config.changelog),
18021
- tags: readTags(git)
18491
+ tags: readTags(git),
18492
+ release: walkable && version2.version !== null ? readReleaseCommit(git, config.versionFile, version2.version) : null
18022
18493
  };
18023
18494
  }
18024
- function writeTag(git, tag) {
18025
- const result = git(["tag", tag, "HEAD"]);
18495
+ function writeTag(git, tag, commit) {
18496
+ const result = git(["tag", tag, commit]);
18026
18497
  if (result.ok)
18027
18498
  return;
18028
18499
  throw new CommandExit(1, `\u274C ${tag} could not be written: ${gitSaid(result)}`);
18029
18500
  }
18501
+ function pastReleaseWarning(written, branch) {
18502
+ if (written.ahead === 0)
18503
+ return null;
18504
+ const commits = written.ahead === 1 ? "1 commit" : `${String(written.ahead)} commits`;
18505
+ return `HEAD of ${branch} is ${commits} past ${written.commit.slice(0, SHORT_COMMIT)}, where ${written.version}` + ` was set; they are not in ${written.tag}.`;
18506
+ }
18030
18507
  function renderTagged(written, branch, followUps) {
18508
+ const where = written.ahead === 0 ? `the HEAD of ${branch}` : `${written.commit.slice(0, SHORT_COMMIT)}, the commit of ${branch} that set ${written.version}`;
18031
18509
  return [
18032
- `\u2705 Tagged ${written.tag} at the HEAD of ${branch}.`,
18510
+ `\u2705 Tagged ${written.tag} at ${where}.`,
18033
18511
  "Next:",
18034
18512
  ...followUps.map((followUp2) => `${INDENT8}${followUp2.command} \u2014 ${followUp2.why}`)
18035
18513
  ];
@@ -18047,8 +18525,9 @@ function runTag(context, seams = DEFAULT_RELEASE_SEAMS) {
18047
18525
  throw new CommandExit(1, `\u274C ${decision.message}
18048
18526
  Usage: ${RELEASE_TAG_USAGE}`);
18049
18527
  }
18050
- writeTag(git, decision.tag);
18051
- const followUps = followUpsFor2(decision.tag, decision.version, readPublishTarget(inputs.version.text));
18528
+ writeTag(git, decision.tag, decision.commit);
18529
+ const place2 = { ahead: decision.ahead, branch: config.releaseBranch };
18530
+ const followUps = followUpsFor2(decision.tag, decision.version, readPublishTarget(inputs.version.text), place2);
18052
18531
  return { inputs, written: decision, followUps };
18053
18532
  }
18054
18533
  function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
@@ -18056,8 +18535,8 @@ function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
18056
18535
  name: "release tag",
18057
18536
  subject: "release",
18058
18537
  action: "tag",
18059
- summary: "tag the release branch's HEAD with the version both release files agree on",
18060
- description: "Writes `v<version>` on the HEAD of the release branch \u2014 `pr.base`, or `main` \u2014 when the" + " version `release.versionFile` declares is the one the newest section of `release.changelog` names." + " It refuses, and writes nothing, when another branch is checked out, when a tag already names that" + " version, when the two files disagree, and when either the version file or the tag list could not" + " be read. After the tag it prints what to run next: the push that puts the tag on the remote, and" + " the publish line for the registry the version file configures, which it does not run. With" + " `--output=json` the readings, the decision and the follow-ups are the data of the terminal result" + " event.",
18538
+ summary: "tag the commit that set the version both release files agree on",
18539
+ description: "Writes `v<version>` on the release branch \u2014 `pr.base`, or `main` \u2014 when the" + " version `release.versionFile` declares is the one the newest section of `release.changelog` names." + " The tag goes on the commit that set that version: HEAD, or, where later commits landed on the branch," + " the earlier commit, with a warning naming how many commits HEAD is past it." + " It refuses, and writes nothing, when another branch is checked out, when a tag already names that" + " version, when the two files disagree, when no commit holds the version yet, and when the version" + " file, the tag list or the history could not be read. After the tag it prints what to run next: the" + " push that puts the tag on the remote, and the publish line for the registry the version file" + " configures, spelled to publish from the tag when HEAD is past it, which it does not run. With" + " `--output=json` the readings, the decision and the follow-ups are the data of the terminal result" + " event.",
18061
18540
  args: [],
18062
18541
  flags: [],
18063
18542
  examples: [
@@ -18073,6 +18552,9 @@ function createReleaseTagCommand(seams = DEFAULT_RELEASE_SEAMS) {
18073
18552
  outputs: ["text", "json"],
18074
18553
  run: async (context) => {
18075
18554
  const result = runTag(context, seams);
18555
+ const past = pastReleaseWarning(result.written, result.inputs.releaseBranch);
18556
+ if (past !== null)
18557
+ context.output.warn(past);
18076
18558
  if (context.outputMode === "json") {
18077
18559
  context.output.result(result);
18078
18560
  return;
@@ -18095,7 +18577,7 @@ function createRoadmapCommand(seams = DEFAULT_ISSUE_SEAMS) {
18095
18577
  subject: "roadmap",
18096
18578
  action: "roadmap",
18097
18579
  summary: "list the Roadmap's lines in its order, with spec, blocked by, has and refs",
18098
- description: "Runs `rafa issue list --roadmap` with the flags typed: the unticked lines of the Roadmap issue" + " (the current place's board, the one `rafa switch` moved to; with no position file `roadmap.issue`," + " else the lowest-numbered open type:roadmap board, else the open issue titled Roadmap) in its order," + " read off the GitHub board, as a" + " table with four columns the plain list has not got: spec, whether the body passes the readiness" + " gate; blocked by, each blocker and whether it is open; has, a plan, a branch or a pull request" + " already made for it; and refs, how many references of the issue's saved copy under `specs.dir` read" + " suspect or dangling, `-` with no copy. A line naming an epic prints as an epic row instead, grouped" + " by horizon under a `Roadmap #<n> \xB7 <horizon>` heading with the columns #, state, done/total, blocked," + " title and date, and the spec rows follow under Specs. `--full` prints each epic's issues under its" + " row, on two rows each: number, state and title, then labels and blockers. `--check` exits 1 when any" + " type:epic issue's stored state disagrees with its computed one, or the board could not be read, so CI" + " can run it. `--all` keeps the ticked lines and shows the" + " epics of every horizon, not only now, and `--type`, `--module`, `--search` and `--limit` narrow the" + " spec rows, keeping their order. An unreachable board is warned about with the rows still printed." + " With `--output=json` the roadmap, the rows, the epics and the warnings are the data of the terminal" + " result event.",
18580
+ description: "Runs `rafa issue list --roadmap` with the flags typed: the unticked lines of the Roadmap issue" + " (the current place's board, the one `rafa switch` moved to; with no position file `roadmap.issue`," + " else the lowest-numbered open type:roadmap board, else the open issue titled Roadmap) in its order," + " read off the GitHub board, as a" + " table with four columns the plain list has not got: spec, whether the issue can be planned (every" + " section of the spec template filled, and the spec:ready label on it), as a symbol; blocked by, the" + " blockers grouped by whether they are still open; has, a plan, a branch" + " or a pull request already made for it; and refs, how many references of the issue's saved copy under" + " `specs.dir` read suspect or dangling, `-` with no copy. A legend under the table names each symbol" + " in plain words, one line per column," + " and `--texts` (`-t`) spells both columns in words instead. Labels are no column: `--labels` prints" + " them on a row under each issue. A Roadmap naming epics prints them alone, grouped by horizon under a" + " `Roadmap #<n> \xB7 <horizon>` heading with the columns #, state, done/total, blocked, date and title," + " and one line counts the lines naming no epic. `--full` prints each epic's issues under its row, a" + " second row led by \u2514\u2192 for a member's blockers, and the lines naming no epic as the issue table. The" + " board listing is kept under .rafa/cache/ and read for what changed since; `--refresh` reads the whole" + " board again. `--check` exits 1 when any" + " type:epic issue's stored state disagrees with its computed one, or the board could not be read, so CI" + " can run it. `--all` keeps the ticked lines and shows the" + " epics of every horizon, not only now, and `--type`, `--module`, `--search` and `--limit` narrow the" + " spec rows, keeping their order. An unreachable board is warned about with the rows still printed." + " With `--output=json` the roadmap, the rows, the epics and the warnings are the data of the terminal" + " result event.",
18099
18581
  args: [],
18100
18582
  flags: [...ROADMAP_FLAGS],
18101
18583
  examples: [
@@ -18111,6 +18593,14 @@ function createRoadmapCommand(seams = DEFAULT_ISSUE_SEAMS) {
18111
18593
  cmd: "rafa roadmap --full",
18112
18594
  note: "Prints the now epics with each epic's issues underneath, then the spec lines."
18113
18595
  },
18596
+ {
18597
+ cmd: "rafa roadmap --labels --texts",
18598
+ note: "Prints each line's labels on a row under it, and the spec and blocked by columns in words."
18599
+ },
18600
+ {
18601
+ cmd: "rafa roadmap --refresh",
18602
+ note: "Reads the whole board again instead of only what changed since the kept listing."
18603
+ },
18114
18604
  {
18115
18605
  cmd: "rafa roadmap --check",
18116
18606
  note: "Prints the Roadmap, then exits 1 when an epic is open with its work done or closed with it not done."
@@ -18129,14 +18619,14 @@ var roadmap_default = createRoadmapCommand();
18129
18619
 
18130
18620
  // src/commands/self-update.ts
18131
18621
  var DEFAULT_SELF_UPDATE_SEAMS = Object.freeze({ build: runBuild });
18132
- var USAGE37 = "rafa self-update [--force]";
18622
+ var USAGE36 = "rafa self-update [--force]";
18133
18623
  function readForceFlag(value) {
18134
18624
  if (value === undefined || value === false || value === "false")
18135
18625
  return false;
18136
18626
  if (value === true || value === "true")
18137
18627
  return true;
18138
18628
  throw new CommandExit(1, `\u274C --force takes no value, and read "${value}" as one
18139
- Usage: ${USAGE37}`);
18629
+ Usage: ${USAGE36}`);
18140
18630
  }
18141
18631
  function projectOf10(context) {
18142
18632
  if (context.project === null)
@@ -18144,7 +18634,7 @@ function projectOf10(context) {
18144
18634
  return context.project;
18145
18635
  }
18146
18636
  async function runSelfUpdate(context, seams) {
18147
- expectNoArgument(context.args, USAGE37);
18637
+ expectNoArgument(context.args, USAGE36);
18148
18638
  const force = readForceFlag(context.flags["force"]);
18149
18639
  const project = projectOf10(context);
18150
18640
  const info2 = (line) => context.output.info(line);
@@ -18202,17 +18692,17 @@ function createSelfUpdateCommand(seams = DEFAULT_SELF_UPDATE_SEAMS) {
18202
18692
  var self_update_default = createSelfUpdateCommand();
18203
18693
 
18204
18694
  // src/commands/skill/backfill.ts
18205
- import { statSync as statSync12 } from "fs";
18206
- import { join as join37, resolve as resolve14 } from "path";
18695
+ import { statSync as statSync13 } from "fs";
18696
+ import { join as join42, resolve as resolve16 } from "path";
18207
18697
 
18208
18698
  // src/backfill/backup.ts
18209
- import { copyFileSync as copyFileSync4, existsSync as existsSync14, mkdirSync as mkdirSync7 } from "fs";
18210
- import { dirname as dirname8, isAbsolute as isAbsolute8, join as join34, relative as relative8, resolve as resolve12, sep as sep5 } from "path";
18699
+ import { copyFileSync as copyFileSync5, existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
18700
+ import { dirname as dirname8, isAbsolute as isAbsolute8, join as join39, relative as relative8, resolve as resolve14, sep as sep5 } from "path";
18211
18701
 
18212
18702
  // src/backfill/proposal-file.ts
18213
- import { readdirSync as readdirSync4, readFileSync as readFileSync22 } from "fs";
18214
- import { join as join33 } from "path";
18215
- var BACKFILL_PATH = join33(".rafa", "backfill");
18703
+ import { readdirSync as readdirSync5, readFileSync as readFileSync22 } from "fs";
18704
+ import { join as join38 } from "path";
18705
+ var BACKFILL_PATH = join38(".rafa", "backfill");
18216
18706
  var PROPOSAL_FILE_PREFIX = "proposals-";
18217
18707
  var PROPOSAL_FILE_EXTENSION = ".yaml";
18218
18708
  var SHA256_PATTERN = /^[0-9a-f]{64}$/;
@@ -18238,7 +18728,7 @@ var PROPOSAL_FILE_HEADER = [
18238
18728
  ].join(`
18239
18729
  `);
18240
18730
  function proposalPath(dir, batch) {
18241
- return join33(dir, `${PROPOSAL_FILE_PREFIX}${String(batch).padStart(2, "0")}${PROPOSAL_FILE_EXTENSION}`);
18731
+ return join38(dir, `${PROPOSAL_FILE_PREFIX}${String(batch).padStart(2, "0")}${PROPOSAL_FILE_EXTENSION}`);
18242
18732
  }
18243
18733
  function isProposalFileName(name2) {
18244
18734
  return name2.startsWith(PROPOSAL_FILE_PREFIX) && name2.endsWith(PROPOSAL_FILE_EXTENSION);
@@ -18325,7 +18815,7 @@ function parseRow(value, index) {
18325
18815
  note: filledStringAt(value, "note")
18326
18816
  };
18327
18817
  }
18328
- function unreadable(problem2) {
18818
+ function unreadable2(problem2) {
18329
18819
  return { file: null, problem: problem2 };
18330
18820
  }
18331
18821
  function parseHeader(data) {
@@ -18344,15 +18834,15 @@ function numberAt(data, key) {
18344
18834
  function parseProposalFile(text) {
18345
18835
  const parsed = readYamlMapping(text);
18346
18836
  if (parsed === null)
18347
- return unreadable("the file holds no YAML mapping");
18837
+ return unreadable2("the file holds no YAML mapping");
18348
18838
  const headerProblem = parseHeader(parsed);
18349
18839
  if (headerProblem !== null)
18350
- return unreadable(headerProblem);
18840
+ return unreadable2(headerProblem);
18351
18841
  const rows = [];
18352
18842
  for (const [index, value] of parsed["rows"].entries()) {
18353
18843
  const row2 = parseRow(value, index);
18354
18844
  if (typeof row2 === "string")
18355
- return unreadable(row2);
18845
+ return unreadable2(row2);
18356
18846
  rows.push(row2);
18357
18847
  }
18358
18848
  return {
@@ -18369,34 +18859,34 @@ function parseProposalFile(text) {
18369
18859
  function readProposalFiles(dir) {
18370
18860
  let names;
18371
18861
  try {
18372
- names = readdirSync4(dir);
18862
+ names = readdirSync5(dir);
18373
18863
  } catch {
18374
18864
  return [];
18375
18865
  }
18376
18866
  return names.filter((name2) => isProposalFileName(name2)).sort((left, right) => left.localeCompare(right)).map((name2) => {
18377
- const path = join33(dir, name2);
18867
+ const path = join38(dir, name2);
18378
18868
  try {
18379
18869
  return { path, result: parseProposalFile(readFileSync22(path, "utf8")) };
18380
18870
  } catch (error) {
18381
- return { path, result: unreadable(`the file cannot be read (${error instanceof Error ? error.message : String(error)})`) };
18871
+ return { path, result: unreadable2(`the file cannot be read (${error instanceof Error ? error.message : String(error)})`) };
18382
18872
  }
18383
18873
  });
18384
18874
  }
18385
18875
 
18386
18876
  // src/backfill/backup.ts
18387
- var BACKUP_PATH = join34(BACKFILL_PATH, "backup");
18877
+ var BACKUP_PATH = join39(BACKFILL_PATH, "backup");
18388
18878
  var BACKUP_KINDS = ["copied", "kept", "tracked", "outside", "missing"];
18389
18879
  function backupDirectory(base) {
18390
- return join34(base, BACKUP_PATH);
18880
+ return join39(base, BACKUP_PATH);
18391
18881
  }
18392
18882
  function isUnder(root, path) {
18393
- const step = relative8(resolve12(root), resolve12(path));
18883
+ const step = relative8(resolve14(root), resolve14(path));
18394
18884
  return step === "" || !step.startsWith(`..${sep5}`) && step !== ".." && !isAbsolute8(step);
18395
18885
  }
18396
18886
  function backupPathFor(path, root, backupDir) {
18397
18887
  if (!isUnder(root, path))
18398
18888
  return null;
18399
- return join34(backupDir, relative8(resolve12(root), resolve12(path)));
18889
+ return join39(backupDir, relative8(resolve14(root), resolve14(path)));
18400
18890
  }
18401
18891
  function backupFile(path, options) {
18402
18892
  if (options.repoRoot !== null && isUnder(options.repoRoot, path)) {
@@ -18405,12 +18895,12 @@ function backupFile(path, options) {
18405
18895
  const backup = backupPathFor(path, options.root, options.backupDir);
18406
18896
  if (backup === null)
18407
18897
  return { path, kind: "outside", backup: null };
18408
- if (!existsSync14(path))
18898
+ if (!existsSync17(path))
18409
18899
  return { path, kind: "missing", backup: null };
18410
- if (existsSync14(backup))
18900
+ if (existsSync17(backup))
18411
18901
  return { path, kind: "kept", backup };
18412
- mkdirSync7(dirname8(backup), { recursive: true });
18413
- copyFileSync4(path, backup);
18902
+ mkdirSync8(dirname8(backup), { recursive: true });
18903
+ copyFileSync5(path, backup);
18414
18904
  return { path, kind: "copied", backup };
18415
18905
  }
18416
18906
  function backupFiles(paths, options) {
@@ -18433,7 +18923,7 @@ function countBackups(results) {
18433
18923
 
18434
18924
  // src/backfill/derive.ts
18435
18925
  import { readFileSync as readFileSync23, writeFileSync as writeFileSync6 } from "fs";
18436
- import { basename as basename8 } from "path";
18926
+ import { basename as basename7 } from "path";
18437
18927
  var DERIVED_FIELDS = ["stack", "paths", "when_to_use"];
18438
18928
  function renderWhenToUse(trigger, prevents) {
18439
18929
  return `${sentenceEnded(trigger)} Prevents: ${prevents}`;
@@ -18550,8 +19040,8 @@ function fileChanges(name2, data, body, options) {
18550
19040
  return { changes, notes };
18551
19041
  }
18552
19042
  function planFile(entry, options) {
18553
- if (basename8(entry.path) !== SKILL_FILE) {
18554
- return inert(entry, "skipped", `${basename8(entry.path)} is no ${SKILL_FILE}, so it registers no skill`);
19043
+ if (basename7(entry.path) !== SKILL_FILE) {
19044
+ return inert(entry, "skipped", `${basename7(entry.path)} is no ${SKILL_FILE}, so it registers no skill`);
18555
19045
  }
18556
19046
  if (entry.name === null) {
18557
19047
  return inert(entry, "skipped", "its place implies no skill name, so no stack can be derived from it");
@@ -19038,8 +19528,8 @@ function parseSessionAnswer(stdout) {
19038
19528
  }
19039
19529
 
19040
19530
  // src/backfill/propose.ts
19041
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync25, writeFileSync as writeFileSync7 } from "fs";
19042
- import { join as join35, resolve as resolve13 } from "path";
19531
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync25, writeFileSync as writeFileSync7 } from "fs";
19532
+ import { join as join40, resolve as resolve15 } from "path";
19043
19533
  var PROPOSAL_ACTION_KINDS = [
19044
19534
  "applied",
19045
19535
  "unchanged",
@@ -19118,7 +19608,7 @@ async function runProposalPass(options) {
19118
19608
  const batches = batchCandidates(candidates);
19119
19609
  const written = [];
19120
19610
  if (batches.length > 0)
19121
- mkdirSync8(options.backfillDir, { recursive: true });
19611
+ mkdirSync9(options.backfillDir, { recursive: true });
19122
19612
  for (const [index, batch] of batches.entries()) {
19123
19613
  const number = index + 1;
19124
19614
  const file = await runProposalBatch(number, batch, options);
@@ -19188,7 +19678,7 @@ function writeRow(path, row2, original, options) {
19188
19678
  };
19189
19679
  }
19190
19680
  function applyRow(root, row2, options) {
19191
- const path = join35(root, ...row2.path.split("/"));
19681
+ const path = join40(root, ...row2.path.split("/"));
19192
19682
  if (row2.status === UNANSWERED_ROW) {
19193
19683
  return inert2(path, row2, "unanswered", row2.note ?? "the row carries no answer");
19194
19684
  }
@@ -19211,7 +19701,7 @@ function fileRefusal(file, root) {
19211
19701
  if (file.status !== REVIEWED_FILE) {
19212
19702
  return `the proposal file is still marked ${file.status}: review it and mark it ${REVIEWED_FILE}`;
19213
19703
  }
19214
- return resolve13(file.skills) === resolve13(root) ? null : `the proposal file names the skills directory ${file.skills}, not ${root}`;
19704
+ return resolve15(file.skills) === resolve15(root) ? null : `the proposal file names the skills directory ${file.skills}, not ${root}`;
19215
19705
  }
19216
19706
  function countProposalActions(actions) {
19217
19707
  const counts = Object.fromEntries(PROPOSAL_ACTION_KINDS.map((kind) => [kind, 0]));
@@ -19225,7 +19715,7 @@ function applyProposals(files, root, options) {
19225
19715
  for (const file of files) {
19226
19716
  const refusal13 = fileRefusal(file, root);
19227
19717
  for (const row2 of file.rows) {
19228
- const action = refusal13 === null ? applyRow(root, row2, options) : inert2(join35(root, ...row2.path.split("/")), row2, "refused", refusal13);
19718
+ const action = refusal13 === null ? applyRow(root, row2, options) : inert2(join40(root, ...row2.path.split("/")), row2, "refused", refusal13);
19229
19719
  actions.push(action);
19230
19720
  if (row2.trigger !== null && action.kind !== "refused" && action.kind !== "unanswered") {
19231
19721
  triggers[row2.name] = row2.trigger;
@@ -19236,13 +19726,13 @@ function applyProposals(files, root, options) {
19236
19726
  }
19237
19727
 
19238
19728
  // src/demote/select.ts
19239
- import { readdirSync as readdirSync5, readFileSync as readFileSync26, realpathSync as realpathSync6, statSync as statSync11 } from "fs";
19240
- import { basename as basename9, dirname as dirname9, join as join36 } from "path";
19729
+ import { readdirSync as readdirSync6, readFileSync as readFileSync26, realpathSync as realpathSync6, statSync as statSync12 } from "fs";
19730
+ import { basename as basename8, dirname as dirname9, join as join41 } from "path";
19241
19731
  var SKILLS_DIRECTORY = "skills";
19242
19732
  var CLAUDE_DIRECTORY = ".claude";
19243
19733
  var AUTO_EXTRACTED_ORIGIN = "auto-extracted";
19244
- var DEMOTED_PATH = join36(".rafa", "demoted");
19245
- var INSTINCTS_PATH = join36(".rafa", "instincts");
19734
+ var DEMOTED_PATH = join41(".rafa", "demoted");
19735
+ var INSTINCTS_PATH = join41(".rafa", "instincts");
19246
19736
  var REPORT_FILE = "report.md";
19247
19737
  function realOrSelf(path) {
19248
19738
  try {
@@ -19259,25 +19749,25 @@ function samePath(left, right) {
19259
19749
  return resolved === other || resolved.toLowerCase() === other.toLowerCase();
19260
19750
  }
19261
19751
  function resolveDemotionScope(dir, home) {
19262
- if (basename9(dir) !== SKILLS_DIRECTORY)
19752
+ if (basename8(dir) !== SKILLS_DIRECTORY)
19263
19753
  return null;
19264
19754
  const claude = dirname9(dir);
19265
- if (basename9(claude) !== CLAUDE_DIRECTORY)
19755
+ if (basename8(claude) !== CLAUDE_DIRECTORY)
19266
19756
  return null;
19267
19757
  const base = dirname9(claude);
19268
- const demotedDir = join36(base, DEMOTED_PATH);
19758
+ const demotedDir = join41(base, DEMOTED_PATH);
19269
19759
  return {
19270
19760
  scope: samePath(base, home) ? "user" : "project",
19271
19761
  dir,
19272
19762
  base,
19273
19763
  demotedDir,
19274
- instinctsDir: join36(base, INSTINCTS_PATH),
19275
- reportPath: join36(demotedDir, REPORT_FILE)
19764
+ instinctsDir: join41(base, INSTINCTS_PATH),
19765
+ reportPath: join41(demotedDir, REPORT_FILE)
19276
19766
  };
19277
19767
  }
19278
19768
  function entryKind(path) {
19279
19769
  try {
19280
- const stats = statSync11(path);
19770
+ const stats = statSync12(path);
19281
19771
  if (stats.isFile())
19282
19772
  return "file";
19283
19773
  if (stats.isDirectory())
@@ -19289,7 +19779,7 @@ function entryKind(path) {
19289
19779
  }
19290
19780
  function visibleNames(dir) {
19291
19781
  try {
19292
- return readdirSync5(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
19782
+ return readdirSync6(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
19293
19783
  } catch {
19294
19784
  return [];
19295
19785
  }
@@ -19299,7 +19789,7 @@ function readSelected(absolute, path, entries) {
19299
19789
  let mtime;
19300
19790
  try {
19301
19791
  text = readFileSync26(absolute, "utf8");
19302
- mtime = statSync11(absolute).mtime.toISOString();
19792
+ mtime = statSync12(absolute).mtime.toISOString();
19303
19793
  } catch {
19304
19794
  return null;
19305
19795
  }
@@ -19314,14 +19804,14 @@ function selectsLearned(text, scope) {
19314
19804
  return origin === AUTO_EXTRACTED_ORIGIN;
19315
19805
  }
19316
19806
  function selectLearned(scope) {
19317
- const dir = join36(scope.dir, LEARNED_DIRECTORY);
19807
+ const dir = join41(scope.dir, LEARNED_DIRECTORY);
19318
19808
  if (entryKind(dir) !== "directory")
19319
19809
  return [];
19320
19810
  const selected = [];
19321
19811
  for (const name2 of visibleNames(dir)) {
19322
19812
  if (!name2.endsWith(MARKDOWN_EXTENSION) || name2 === MARKDOWN_EXTENSION)
19323
19813
  continue;
19324
- const absolute = join36(dir, name2);
19814
+ const absolute = join41(dir, name2);
19325
19815
  if (entryKind(absolute) !== "file")
19326
19816
  continue;
19327
19817
  const file = readSelected(absolute, `${LEARNED_DIRECTORY}/${name2}`, undefined);
@@ -19335,10 +19825,10 @@ function selectSkillFiles(scope) {
19335
19825
  for (const name2 of visibleNames(scope.dir)) {
19336
19826
  if (name2 === LEARNED_DIRECTORY)
19337
19827
  continue;
19338
- const skillDir = join36(scope.dir, name2);
19828
+ const skillDir = join41(scope.dir, name2);
19339
19829
  if (entryKind(skillDir) !== "directory")
19340
19830
  continue;
19341
- const absolute = join36(skillDir, SKILL_FILE);
19831
+ const absolute = join41(skillDir, SKILL_FILE);
19342
19832
  if (entryKind(absolute) !== "file")
19343
19833
  continue;
19344
19834
  const file = readSelected(absolute, `${name2}/${SKILL_FILE}`, visibleNames(skillDir));
@@ -19386,7 +19876,7 @@ Usage: ${BACKFILL_USAGE}`);
19386
19876
  throw new CommandExit(1, `\u274C Expected one directory, and read "${extra}" as a second
19387
19877
  Usage: ${BACKFILL_USAGE}`);
19388
19878
  }
19389
- return resolve14(cwd(), dir);
19879
+ return resolve16(cwd(), dir);
19390
19880
  }
19391
19881
  function readConsumerRoot(value, cwd) {
19392
19882
  if (value === undefined || value === false)
@@ -19395,11 +19885,11 @@ function readConsumerRoot(value, cwd) {
19395
19885
  throw new CommandExit(1, `\u274C --project needs a path, and the line gave it none
19396
19886
  Usage: ${BACKFILL_USAGE}`);
19397
19887
  }
19398
- return resolve14(cwd(), value);
19888
+ return resolve16(cwd(), value);
19399
19889
  }
19400
19890
  function isDirectory3(path) {
19401
19891
  try {
19402
- return statSync12(path).isDirectory();
19892
+ return statSync13(path).isDirectory();
19403
19893
  } catch {
19404
19894
  return false;
19405
19895
  }
@@ -19523,7 +20013,7 @@ function reviewedProposals(dir) {
19523
20013
  return read2.flatMap((entry) => entry.result.file === null ? [] : [entry.result.file]);
19524
20014
  }
19525
20015
  function rowPaths(files, root) {
19526
- return files.flatMap((file) => file.status !== REVIEWED_FILE ? [] : file.rows.filter((row2) => row2.status === ANSWERED_ROW).map((row2) => join37(root, ...row2.path.split("/"))));
20016
+ return files.flatMap((file) => file.status !== REVIEWED_FILE ? [] : file.rows.filter((row2) => row2.status === ANSWERED_ROW).map((row2) => join42(root, ...row2.path.split("/"))));
19527
20017
  }
19528
20018
  function runApply(context, scope, backfillDir, projectRoot3) {
19529
20019
  const options = checkOptions(context, projectRoot3);
@@ -19569,7 +20059,7 @@ async function runBackfill(context, seams) {
19569
20059
  const dir = readBackfillDirectory(context.args, seams.cwd);
19570
20060
  const projectRoot3 = readConsumerRoot(context.flags["project"], seams.cwd);
19571
20061
  const scope = backfillScopeOf(dir, projectOf11(context).home);
19572
- const backfillDir = join37(scope.base, BACKFILL_PATH);
20062
+ const backfillDir = join42(scope.base, BACKFILL_PATH);
19573
20063
  if (mode === "propose") {
19574
20064
  await runPropose(context, scope, backfillDir, seams);
19575
20065
  return;
@@ -19683,13 +20173,13 @@ function createSkillCheckCommand(seams = DEFAULT_CHECK_SEAMS) {
19683
20173
  var check_default3 = createSkillCheckCommand();
19684
20174
 
19685
20175
  // src/commands/skill/demote.ts
19686
- import { existsSync as existsSync16, mkdirSync as mkdirSync10, readFileSync as readFileSync28, statSync as statSync14, writeFileSync as writeFileSync9 } from "fs";
19687
- import { dirname as dirname11, resolve as resolve15 } from "path";
20176
+ import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync28, statSync as statSync15, writeFileSync as writeFileSync9 } from "fs";
20177
+ import { dirname as dirname11, resolve as resolve17 } from "path";
19688
20178
 
19689
20179
  // src/demote/apply.ts
19690
- import { copyFileSync as copyFileSync5, existsSync as existsSync15, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readdirSync as readdirSync6, readFileSync as readFileSync27, renameSync as renameSync3, rmSync as rmSync4, statSync as statSync13, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
20180
+ import { copyFileSync as copyFileSync6, existsSync as existsSync18, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, readdirSync as readdirSync7, readFileSync as readFileSync27, renameSync as renameSync3, rmSync as rmSync5, statSync as statSync14, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
19691
20181
  import { tmpdir as tmpdir2 } from "os";
19692
- import { dirname as dirname10, join as join38 } from "path";
20182
+ import { dirname as dirname10, join as join43 } from "path";
19693
20183
  var DEMOTION_ACTION_KINDS = [
19694
20184
  "demoted",
19695
20185
  "kept",
@@ -19710,18 +20200,18 @@ function learnedSkillName(path) {
19710
20200
  return base.slice(0, -MARKDOWN_EXTENSION.length);
19711
20201
  }
19712
20202
  function sourcePath(scope, path) {
19713
- return join38(scope.dir, ...path.split("/"));
20203
+ return join43(scope.dir, ...path.split("/"));
19714
20204
  }
19715
20205
  function demotedPath(scope, path) {
19716
- return join38(scope.demotedDir, ...path.split("/"));
20206
+ return join43(scope.demotedDir, ...path.split("/"));
19717
20207
  }
19718
20208
  function promotedPath(scope, path) {
19719
- return join38(scope.dir, learnedSkillName(path), SKILL_FILE);
20209
+ return join43(scope.dir, learnedSkillName(path), SKILL_FILE);
19720
20210
  }
19721
20211
  function takenInstinctIds(dir) {
19722
20212
  let names;
19723
20213
  try {
19724
- names = readdirSync6(dir);
20214
+ names = readdirSync7(dir);
19725
20215
  } catch {
19726
20216
  return [];
19727
20217
  }
@@ -19729,7 +20219,7 @@ function takenInstinctIds(dir) {
19729
20219
  }
19730
20220
  function readText4(path) {
19731
20221
  try {
19732
- return statSync13(path).isFile() ? readFileSync27(path, "utf8") : null;
20222
+ return statSync14(path).isFile() ? readFileSync27(path, "utf8") : null;
19733
20223
  } catch {
19734
20224
  return null;
19735
20225
  }
@@ -19757,7 +20247,7 @@ function planKept(scope, row2, file) {
19757
20247
  return inert3(row2.path, "procedure", "kept", "procedure-shaped: left where it is, for the backfill");
19758
20248
  }
19759
20249
  const to = promotedPath(scope, row2.path);
19760
- if (existsSync15(to) || existsSync15(dirname10(to))) {
20250
+ if (existsSync18(to) || existsSync18(dirname10(to))) {
19761
20251
  return inert3(row2.path, "procedure", "refused", `kept as a skill, but ${to} is already there`);
19762
20252
  }
19763
20253
  return {
@@ -19771,7 +20261,7 @@ function planKept(scope, row2, file) {
19771
20261
  };
19772
20262
  }
19773
20263
  function checkCandidate(dir, id2, text, pathDirs) {
19774
- const path = join38(dir, `${id2}${MARKDOWN_EXTENSION}`);
20264
+ const path = join43(dir, `${id2}${MARKDOWN_EXTENSION}`);
19775
20265
  writeFileSync8(path, text, "utf8");
19776
20266
  const report3 = checkFile(path, "instinct", { projectRoot: null, pathDirs });
19777
20267
  return failureLines2(report3.issues);
@@ -19792,11 +20282,11 @@ function planDemoted(scope, row2, file, context) {
19792
20282
  if (failures.length > 0) {
19793
20283
  return inert3(row2.path, "observation", "refused", `the record ${id2} fails the checker \u2014 ${failures.join("; ")}`);
19794
20284
  }
19795
- const destination = join38(scope.instinctsDir, `${id2}${MARKDOWN_EXTENSION}`);
20285
+ const destination = join43(scope.instinctsDir, `${id2}${MARKDOWN_EXTENSION}`);
19796
20286
  const kept = demotedPath(scope, row2.path);
19797
- if (existsSync15(destination))
20287
+ if (existsSync18(destination))
19798
20288
  return inert3(row2.path, "observation", "refused", `${destination} is already there`);
19799
- if (existsSync15(kept))
20289
+ if (existsSync18(kept))
19800
20290
  return inert3(row2.path, "observation", "refused", `${kept} is already there`);
19801
20291
  context.taken.push(id2);
19802
20292
  return {
@@ -19828,7 +20318,7 @@ function planRow(scope, row2, context) {
19828
20318
  entries: undefined,
19829
20319
  absolute,
19830
20320
  hash,
19831
- mtime: statSync13(absolute).mtime.toISOString()
20321
+ mtime: statSync14(absolute).mtime.toISOString()
19832
20322
  };
19833
20323
  return verdict === "procedure" ? planKept(scope, row2, file) : planDemoted(scope, row2, file, context);
19834
20324
  }
@@ -19839,28 +20329,28 @@ function countActions(actions) {
19839
20329
  return counts;
19840
20330
  }
19841
20331
  function planDemotion(report3, scope, seams = DEFAULT_APPLY_SEAMS) {
19842
- const temp = mkdtempSync2(join38(tmpdir2(), "rafa-demote-"));
20332
+ const temp = mkdtempSync2(join43(tmpdir2(), "rafa-demote-"));
19843
20333
  const taken = [...takenInstinctIds(scope.instinctsDir)];
19844
20334
  try {
19845
20335
  const actions = report3.rows.map((row2) => planRow(scope, row2, { temp, taken, seams }));
19846
20336
  return { scope, actions, counts: countActions(actions) };
19847
20337
  } finally {
19848
- rmSync4(temp, { recursive: true, force: true });
20338
+ rmSync5(temp, { recursive: true, force: true });
19849
20339
  }
19850
20340
  }
19851
20341
  function moveFile(move3) {
19852
- mkdirSync9(dirname10(move3.to), { recursive: true });
20342
+ mkdirSync10(dirname10(move3.to), { recursive: true });
19853
20343
  try {
19854
20344
  renameSync3(move3.from, move3.to);
19855
20345
  } catch {
19856
- copyFileSync5(move3.from, move3.to);
20346
+ copyFileSync6(move3.from, move3.to);
19857
20347
  unlinkSync(move3.from);
19858
20348
  }
19859
20349
  }
19860
20350
  function applyDemotion(plan2) {
19861
20351
  for (const action of plan2.actions) {
19862
20352
  if (action.write !== null) {
19863
- mkdirSync9(dirname10(action.write.path), { recursive: true });
20353
+ mkdirSync10(dirname10(action.write.path), { recursive: true });
19864
20354
  writeFileSync8(action.write.path, action.write.text, "utf8");
19865
20355
  }
19866
20356
  if (action.move !== null)
@@ -19926,11 +20416,11 @@ Usage: ${DEMOTE_USAGE}`);
19926
20416
  throw new CommandExit(1, `\u274C Expected one directory, and read "${extra}" as a second
19927
20417
  Usage: ${DEMOTE_USAGE}`);
19928
20418
  }
19929
- return resolve15(cwd(), dir);
20419
+ return resolve17(cwd(), dir);
19930
20420
  }
19931
20421
  function isDirectory4(path) {
19932
20422
  try {
19933
- return statSync14(path).isDirectory();
20423
+ return statSync15(path).isDirectory();
19934
20424
  } catch {
19935
20425
  return false;
19936
20426
  }
@@ -19952,13 +20442,13 @@ function projectOf12(context) {
19952
20442
  throw new Error("rafa skill demote runs inside a project, and was handed none");
19953
20443
  return context.project;
19954
20444
  }
19955
- function verdictLine(report3) {
20445
+ function verdictLine2(report3) {
19956
20446
  const counts = countVerdicts(report3.rows);
19957
20447
  const parts = DEMOTION_VERDICTS.map((verdict) => `${String(counts[verdict])} ${verdict}`);
19958
20448
  return `${String(report3.rows.length)} file(s): ${parts.join(", ")}`;
19959
20449
  }
19960
20450
  function previousReport(path, context) {
19961
- if (!existsSync16(path))
20451
+ if (!existsSync19(path))
19962
20452
  return null;
19963
20453
  const parsed = parseDemotionReport(readFileSync28(path, "utf8"));
19964
20454
  if (parsed.report !== null)
@@ -19969,7 +20459,7 @@ function previousReport(path, context) {
19969
20459
  function runWrite(context, scope) {
19970
20460
  const files = selectSources(scope);
19971
20461
  const report3 = buildDemotionReport(files, previousReport(scope.reportPath, context));
19972
- mkdirSync10(dirname11(scope.reportPath), { recursive: true });
20462
+ mkdirSync11(dirname11(scope.reportPath), { recursive: true });
19973
20463
  writeFileSync9(scope.reportPath, writeDemotionReport(report3), "utf8");
19974
20464
  const result = {
19975
20465
  applied: false,
@@ -19986,14 +20476,14 @@ function runWrite(context, scope) {
19986
20476
  }
19987
20477
  for (const line of [
19988
20478
  `${scope.scope} scope: ${scope.dir}`,
19989
- verdictLine(report3),
20479
+ verdictLine2(report3),
19990
20480
  `Report written to ${scope.reportPath} (status: ${report3.status}); nothing was moved`,
19991
20481
  `Review each row, then mark the report ${REVIEWED_STATUS} and re-run with --apply`
19992
20482
  ])
19993
20483
  context.output.info(line);
19994
20484
  }
19995
20485
  function reviewedReport(scope) {
19996
- if (!existsSync16(scope.reportPath)) {
20486
+ if (!existsSync19(scope.reportPath)) {
19997
20487
  throw new CommandExit(1, `\u274C No report at ${scope.reportPath}; run "rafa skill demote ${scope.dir}" first, then review it`);
19998
20488
  }
19999
20489
  const parsed = parseDemotionReport(readFileSync28(scope.reportPath, "utf8"));
@@ -20102,8 +20592,8 @@ var demote_default = createSkillDemoteCommand();
20102
20592
 
20103
20593
  // src/commands/skill/show.ts
20104
20594
  var DEFAULT_SKILL_SHOW_SEAMS = DEFAULT_SKILL_LIST_SEAMS;
20105
- var COMMAND_NAME6 = "rafa skill show";
20106
- var USAGE38 = "rafa skill show <name> [--full]";
20595
+ var COMMAND_NAME9 = "rafa skill show";
20596
+ var USAGE37 = "rafa skill show <name> [--full]";
20107
20597
  function unknownSkillMessage(name2, inventory) {
20108
20598
  const agent = findShown(inventory.records, "agent", name2);
20109
20599
  const hint = agent === null ? [] : [`An agent is named "${name2}" (${agent.source}): rafa agent show ${name2}`];
@@ -20111,7 +20601,7 @@ function unknownSkillMessage(name2, inventory) {
20111
20601
  `\u274C No skill is named "${name2}".`,
20112
20602
  ...hint,
20113
20603
  "Run `rafa skill list` to see every skill and where it comes from.",
20114
- `Usage: ${USAGE38}`
20604
+ `Usage: ${USAGE37}`
20115
20605
  ].join(`
20116
20606
  `);
20117
20607
  }
@@ -20127,10 +20617,10 @@ function projectOf13(context) {
20127
20617
  return context.project;
20128
20618
  }
20129
20619
  async function runShow3(context, seams) {
20130
- const full = readSwitch("full", context.flags["full"], `Type the name ahead of it: ${USAGE38}`);
20131
- const name2 = expectOneArgument(context.args, USAGE38);
20620
+ const full = readSwitch("full", context.flags["full"], `Type the name ahead of it: ${USAGE37}`);
20621
+ const name2 = expectOneArgument(context.args, USAGE37);
20132
20622
  const project = projectOf13(context);
20133
- const { inventory, settingSources } = await projectInventory(project, context, seams, COMMAND_NAME6);
20623
+ const { inventory, settingSources } = await projectInventory(project, context, seams, COMMAND_NAME9);
20134
20624
  const warnings = warningLines(inventory);
20135
20625
  for (const warning of warnings)
20136
20626
  context.output.warn(warning);
@@ -20323,12 +20813,12 @@ function statusData(sections) {
20323
20813
  }
20324
20814
 
20325
20815
  // src/status/sections.ts
20326
- import { existsSync as existsSync17 } from "fs";
20816
+ import { existsSync as existsSync20 } from "fs";
20327
20817
  var STATUS_NETWORK_TIMEOUT_MS = 5000;
20328
20818
  function isIdleWorktree(row2) {
20329
20819
  return !row2.blockers.some((blocker) => blocker.kind === "recent");
20330
20820
  }
20331
- function unread2(problem2) {
20821
+ function unread3(problem2) {
20332
20822
  return { read: false, problem: problem2 };
20333
20823
  }
20334
20824
  function read2(reading) {
@@ -20339,7 +20829,7 @@ function localSection(work) {
20339
20829
  const reading = work();
20340
20830
  return isUnread(reading) ? reading : read2(reading);
20341
20831
  } catch (error) {
20342
- return unread2(messageOf(error));
20832
+ return unread3(messageOf(error));
20343
20833
  }
20344
20834
  }
20345
20835
  function isUnread(value) {
@@ -20370,15 +20860,15 @@ function boundedRunner(open, root, deadline) {
20370
20860
  }
20371
20861
  async function networkSection(what, deadline, work) {
20372
20862
  let timer;
20373
- const expired = new Promise((resolve16) => {
20863
+ const expired = new Promise((resolve18) => {
20374
20864
  timer = setTimeout(() => {
20375
- resolve16(unread2(`the ${what} was not read within the ${String(deadline.ms)}ms network deadline`));
20865
+ resolve18(unread3(`the ${what} was not read within the ${String(deadline.ms)}ms network deadline`));
20376
20866
  }, Math.max(0, deadline.left()));
20377
20867
  });
20378
20868
  try {
20379
20869
  return await Promise.race([work().then((reading) => read2(reading)), expired]);
20380
20870
  } catch (error) {
20381
- return unread2(messageOf(error));
20871
+ return unread3(messageOf(error));
20382
20872
  } finally {
20383
20873
  clearTimeout(timer);
20384
20874
  }
@@ -20394,7 +20884,7 @@ function readBranchSection(sources) {
20394
20884
  };
20395
20885
  const branch = readBranch(sources, note);
20396
20886
  if (branch === "") {
20397
- return unread2(notes.join("; ") || "the branch at the project root could not be read");
20887
+ return unread3(notes.join("; ") || "the branch at the project root could not be read");
20398
20888
  }
20399
20889
  const plans = readPlans(sources, note);
20400
20890
  return { branch, plan: readBranchPlan(branch, plans), notes };
@@ -20421,9 +20911,9 @@ function readLoopsSection(root, sources) {
20421
20911
  }
20422
20912
  function pullSection(sources, branch, provider, deadline) {
20423
20913
  if (provider.provider !== "gh")
20424
- return Promise.resolve(unread2(notGhProblem("pull request", provider)));
20914
+ return Promise.resolve(unread3(notGhProblem("pull request", provider)));
20425
20915
  if (!branch.read) {
20426
- return Promise.resolve(unread2(`no pull request was looked for, as the branch was not read: ${branch.problem}`));
20916
+ return Promise.resolve(unread3(`no pull request was looked for, as the branch was not read: ${branch.problem}`));
20427
20917
  }
20428
20918
  return networkSection("pull request", deadline, () => readPull(sources, branch.branch));
20429
20919
  }
@@ -20462,7 +20952,7 @@ function nextOfPlace(current, epic, walk) {
20462
20952
  return epic?.members.some((member) => member.number === line.issue) === true ? line.issue : null;
20463
20953
  }
20464
20954
  function hasBoards(root, listing2) {
20465
- if (existsSync17(positionFilePath(root)))
20955
+ if (existsSync20(positionFilePath(root)))
20466
20956
  return true;
20467
20957
  return listing2.some((issue2) => issue2.state === "OPEN" && issue2.labels.includes(ROADMAP_LABEL));
20468
20958
  }
@@ -20507,11 +20997,11 @@ async function readHousekeeping(input, cleanup, now) {
20507
20997
  const settings = doctorCleanupSettings({ root: input.root, home: input.home, config: input.config, gh: null }, now);
20508
20998
  const reading = await readCleanup(cleanup, settings);
20509
20999
  if (!reading.ok)
20510
- return unread2(reading.detail);
21000
+ return unread3(reading.detail);
20511
21001
  const idle = reading.worktrees.filter((row2) => isIdleWorktree(row2));
20512
21002
  return read2({ counts: cleanupCounts(reading), idleWorktrees: idle.length, notes: reading.notes });
20513
21003
  } catch (error) {
20514
- return unread2(messageOf(error));
21004
+ return unread3(messageOf(error));
20515
21005
  }
20516
21006
  }
20517
21007
  async function readStatusSections(input, seams = {}) {
@@ -20540,7 +21030,7 @@ async function readStatusSections(input, seams = {}) {
20540
21030
  const blockedIssues = seams.blockedIssues ?? ((runner) => readBlockedIssues({ gh: runner }));
20541
21031
  deadline.start();
20542
21032
  const pull = pullSection(sources, branch, provider, deadline);
20543
- const boardSection = isGh ? networkSection("board", deadline, () => readBoard(root, board, listing2, blockedIssues(gh))) : Promise.resolve(unread2(notGhProblem("board", provider)));
21033
+ const boardSection = isGh ? networkSection("board", deadline, () => readBoard(root, board, listing2, blockedIssues(gh))) : Promise.resolve(unread3(notGhProblem("board", provider)));
20544
21034
  const housekeeping = readHousekeeping(input, cleanup, now);
20545
21035
  return {
20546
21036
  branch,
@@ -20552,7 +21042,7 @@ async function readStatusSections(input, seams = {}) {
20552
21042
  }
20553
21043
 
20554
21044
  // src/commands/status.ts
20555
- var COMMAND_NAME7 = "rafa status";
21045
+ var COMMAND_NAME10 = "rafa status";
20556
21046
  var STATUS_USAGE = "rafa status";
20557
21047
  var STATUS_ARGUMENT_EXIT = 2;
20558
21048
  var DEFAULT_STATUS_SEAMS = Object.freeze({});
@@ -20564,13 +21054,13 @@ function projectOf14(context) {
20564
21054
  function expectNoWord(args) {
20565
21055
  if (args.length === 0)
20566
21056
  return;
20567
- throw new CommandExit(STATUS_ARGUMENT_EXIT, `\u274C ${COMMAND_NAME7}: expected no argument, got ${String(args.length)}: ${args.join(" ")}
21057
+ throw new CommandExit(STATUS_ARGUMENT_EXIT, `\u274C ${COMMAND_NAME10}: expected no argument, got ${String(args.length)}: ${args.join(" ")}
20568
21058
  Usage: ${STATUS_USAGE}`);
20569
21059
  }
20570
21060
  async function runStatus2(context, seams) {
20571
21061
  expectNoWord(context.args);
20572
21062
  const project = projectOf14(context);
20573
- const config = resolveProjectConfig(project, COMMAND_NAME7, (message) => {
21063
+ const config = resolveProjectConfig(project, COMMAND_NAME10, (message) => {
20574
21064
  context.output.warn(message);
20575
21065
  });
20576
21066
  const read3 = seams.read ?? ((input) => readStatusSections(input));
@@ -20636,18 +21126,18 @@ var CORE_SUBJECTS = Object.freeze([
20636
21126
  { name: "loop", summary: "start a plan; stop, pause, resume, show and list its sessions" },
20637
21127
  { name: "issue", summary: "the tracker: list, show, create, comment on and move issues; mark one ready, unblock it and check its references" },
20638
21128
  { name: "pr", summary: "the pull request of a branch: one line, in full or in the browser; list, wait on, merge and triage them" },
20639
- { name: "effort", summary: "collect session and commit rows; report per plan; repair a store a newer rafa migrated" },
21129
+ { name: "effort", summary: "collect session and commit rows; report per plan; read and repair the store: its schema, a copy for testing, migrations, and fixes" },
20640
21130
  { name: "module", summary: "list the configured modules; run an action a module provides" },
20641
21131
  { name: "agent", summary: "copy an agent definition into the project; list what a session sees" },
20642
21132
  { name: "skill", summary: "check a skills directory; list each tier; demote and backfill it" },
20643
21133
  { name: "instinct", summary: "check an instincts directory; list, show, flag and promote its records" },
20644
- { name: "release", summary: "read the release state of the project; tag the release branch's HEAD" },
21134
+ { name: "release", summary: "read the release state of the project; tag the commit that set the version" },
20645
21135
  { name: "board", summary: "list the boards with their owner, epic count, and which is current and home" },
20646
21136
  { name: "epic", summary: "show one epic's issues as the Roadmap table; create an epic; defer or promote it; move an issue to it; close it through the gate or cancel it" }
20647
21137
  ]);
20648
21138
  var CORE_COMMANDS = Object.freeze([
20649
21139
  create_default2,
20650
- list_default,
21140
+ list_default2,
20651
21141
  show_default5,
20652
21142
  validate_default,
20653
21143
  risk_default,
@@ -20658,7 +21148,7 @@ var CORE_COMMANDS = Object.freeze([
20658
21148
  resume_default,
20659
21149
  status_default2,
20660
21150
  list_default7,
20661
- list_default6,
21151
+ list_default,
20662
21152
  show_default4,
20663
21153
  create_default,
20664
21154
  comment_default,
@@ -20676,26 +21166,29 @@ var CORE_COMMANDS = Object.freeze([
20676
21166
  collect_default,
20677
21167
  report_default,
20678
21168
  fix_schema_default,
21169
+ copy_default,
21170
+ schema_default,
21171
+ migrate_default,
20679
21172
  list_default8,
20680
21173
  exec_default,
20681
21174
  vendor_default,
20682
- list_default3,
21175
+ list_default4,
20683
21176
  show_default2,
20684
21177
  search_default2,
20685
21178
  check_default3,
20686
- list_default2,
21179
+ list_default3,
20687
21180
  show_default7,
20688
21181
  search_default,
20689
21182
  demote_default,
20690
21183
  backfill_default,
20691
21184
  check_default,
20692
- list_default5,
21185
+ list_default6,
20693
21186
  show_default3,
20694
21187
  flag_default,
20695
21188
  promote_default2,
20696
21189
  status_default,
20697
21190
  tag_default,
20698
- list_default4,
21191
+ list_default5,
20699
21192
  show_default,
20700
21193
  new_default,
20701
21194
  defer_default,