@open-tomato/rafa 0.26.0 → 0.28.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.
@@ -25,7 +25,6 @@ import {
25
25
  NEXT_COMMAND_ACTIONS,
26
26
  NO_NEXT_STEP,
27
27
  ORIGIN_FIELD,
28
- POSITION_FILE,
29
28
  PREVIOUS_COPY_WARN_ABOVE,
30
29
  PROVENANCE_FIELD,
31
30
  REASON_FLAG,
@@ -61,7 +60,7 @@ import {
61
60
  blockedLineSentence,
62
61
  boardChanged,
63
62
  boardId,
64
- bodyLines,
63
+ boardOfEpic,
65
64
  bodySections,
66
65
  branchClaims,
67
66
  bundledAgentsDirectory,
@@ -92,8 +91,8 @@ import {
92
91
  createGhPullRequests,
93
92
  createGhRoadmapSearch,
94
93
  createGhSpecIssueReader,
95
- createGitRunner,
96
94
  createLinePrompter,
95
+ createOwnerResolver,
97
96
  createPlanDirNames,
98
97
  createPlanRefsVerifier,
99
98
  developmentApplied,
@@ -109,7 +108,6 @@ import {
109
108
  expectOneArgument,
110
109
  expectTwoArguments,
111
110
  failingRows,
112
- findField,
113
111
  fingerprintText,
114
112
  firstNowEpic,
115
113
  firstSentence,
@@ -124,12 +122,10 @@ import {
124
122
  ghPreflightItems,
125
123
  ghPullRequestsIn,
126
124
  ghUnauthenticatedMessage,
127
- gitSaid,
128
125
  groupByEpicLabel,
129
126
  hasDiverged,
130
127
  hasPlanFor,
131
128
  hasSpecBlockedLabel,
132
- hop,
133
129
  horizonOf,
134
130
  isFile,
135
131
  isFirstDispatch,
@@ -153,9 +149,11 @@ import {
153
149
  missingBoardLabels,
154
150
  missingPlanAgents,
155
151
  namedSecrets,
152
+ nextOwnerGate,
156
153
  nextQuestion,
157
154
  notesFileName,
158
155
  onTracker,
156
+ openBoards,
159
157
  openNextSources,
160
158
  parseBaseStanding,
161
159
  parseRoadmapBody,
@@ -168,7 +166,6 @@ import {
168
166
  planLabel,
169
167
  plansDirAt,
170
168
  plural,
171
- positionFilePath,
172
169
  previousDir,
173
170
  projectRoot,
174
171
  provenanceBlock,
@@ -176,6 +173,7 @@ import {
176
173
  readAgentDirectory,
177
174
  readAuthorTrust,
178
175
  readBlockedBy,
176
+ readBoardBody,
179
177
  readBranch,
180
178
  readBranchPlan,
181
179
  readChoice,
@@ -186,13 +184,13 @@ import {
186
184
  readEpicDependents,
187
185
  readEpicProblems,
188
186
  readEpics,
187
+ readHomeAfterLoop,
189
188
  readManifestVersion,
190
189
  readMergeRefusal,
191
190
  readNextState,
192
191
  readNonBlankFlag,
193
192
  readOpenPull,
194
193
  readPlans,
195
- readPositionFile,
196
194
  readRankCandidate,
197
195
  readReason,
198
196
  readRecords,
@@ -210,7 +208,6 @@ import {
210
208
  recordPlanCi,
211
209
  refusal,
212
210
  refuseMisplacedRuntime,
213
- rehome,
214
211
  remainingFrom,
215
212
  renderCancelComment,
216
213
  renderCloseComment,
@@ -271,10 +268,9 @@ import {
271
268
  verdictOf,
272
269
  waitForChecks,
273
270
  watchDryEpic,
274
- writePositionFile,
275
271
  writeSession,
276
272
  writeShippedFile
277
- } from "./index-59x3zsh4.js";
273
+ } from "./index-6jkj0dcx.js";
278
274
  import {
279
275
  PROBE_TIMEOUT_MS,
280
276
  findNextTask,
@@ -297,16 +293,20 @@ import {
297
293
  DISK_FILE_SYSTEM,
298
294
  DevelopmentBuildRefusedError,
299
295
  EFFORT_DIR_VARIABLE,
296
+ EFFORT_KEY_PROJECTIONS,
300
297
  EFFORT_STORE_DIR,
298
+ HOP_FILE,
301
299
  ISSUE_PRIORITIES,
302
300
  ISSUE_STATES,
303
301
  ISSUE_TYPES,
304
302
  MARKDOWN_EXTENSION,
305
303
  MIGRATION_LOG_TABLE,
304
+ POSITION_FILE,
306
305
  SCOPE_DIR,
307
306
  SKILL_FILE,
308
307
  SQLITE_MIGRATIONS,
309
308
  SQLITE_STORE_FILE_NAME,
309
+ SchemaRefusedError,
310
310
  SessionRecordError,
311
311
  activeStoreSettings,
312
312
  allRecords,
@@ -314,26 +314,37 @@ import {
314
314
  bringForward,
315
315
  claudeArgs,
316
316
  createGhRunner,
317
+ createGitRunner,
317
318
  effortStoreDir,
318
319
  effortStoreFileName,
319
320
  errorCode,
320
321
  findRecords,
321
322
  gitRemoteUrl,
323
+ gitSaid,
322
324
  guardTestProcess,
325
+ hop,
323
326
  instinctProject,
324
327
  isPidAlive,
325
328
  mergeFrontmatter,
326
329
  name,
327
330
  normalizeRemote,
331
+ openNdjsonStore,
332
+ openSqliteStore,
328
333
  planSchema,
334
+ positionFilePath,
329
335
  readFrontmatter,
330
336
  readFrontmatterDocument,
337
+ readHopRecord,
338
+ readPositionFile,
339
+ readProjectIdentity,
331
340
  readRuntimeIdentity,
332
341
  readScopes,
333
342
  readSession,
334
343
  readSessions,
344
+ readStoreMeta,
335
345
  readStoreSchema,
336
346
  refuseUnownedDevelopmentWrite,
347
+ rehome,
337
348
  renderFrontmatter,
338
349
  runClaudeCaptured,
339
350
  runsDir,
@@ -345,13 +356,15 @@ import {
345
356
  spawnClaudeCaptured,
346
357
  sqliteCatalogue,
347
358
  sqliteStorePath,
359
+ staleAgainst,
348
360
  storeProjectRoot,
349
361
  toHeldRecords,
350
362
  version,
351
363
  versionLine,
352
364
  writeFrontmatter,
353
- writeNames
354
- } from "./index-azptzvp3.js";
365
+ writeNames,
366
+ writePositionFile
367
+ } from "./index-4rwvr6t4.js";
355
368
  import {
356
369
  CONFIG_DEFAULTS,
357
370
  CONFIG_FILE,
@@ -2545,7 +2558,7 @@ function headingOf(search, question, projectRoot2) {
2545
2558
  const how = search.status === "ranked" ? "; keyword ranking, no session" : "";
2546
2559
  return `${plural2} for "${question}" (project: ${projectRoot2}${how}):`;
2547
2560
  }
2548
- function bodyLines2(search) {
2561
+ function bodyLines(search) {
2549
2562
  switch (search.status) {
2550
2563
  case "no-candidates":
2551
2564
  return [` (no ${search.kind} ranks for these words)`];
@@ -2570,7 +2583,7 @@ function searchWarnings(search) {
2570
2583
  return [...notice, ...issues, ...effort];
2571
2584
  }
2572
2585
  function renderKindSearch(search, question, projectRoot2) {
2573
- return [headingOf(search, question, projectRoot2), ...bodyLines2(search)];
2586
+ return [headingOf(search, question, projectRoot2), ...bodyLines(search)];
2574
2587
  }
2575
2588
  function projectOf3(context, kind) {
2576
2589
  if (context.project === null)
@@ -2945,96 +2958,6 @@ function createAgentVendorCommand(now = () => new Date, entry = () => Bun.main)
2945
2958
  }
2946
2959
  var vendor_default = createAgentVendorCommand();
2947
2960
 
2948
- // src/board/board-body.ts
2949
- var OWNER_FIELD = "Owner";
2950
- var BOARD_OWNS_FIELD = "Owns";
2951
- var LOGIN = "[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}";
2952
- var TEAM = "[A-Za-z0-9][A-Za-z0-9_.-]*";
2953
- var HANDLE = new RegExp(`^@${LOGIN}(?:/${TEAM})?$`, "u");
2954
- function isOwnerHandle(text) {
2955
- return HANDLE.test(text);
2956
- }
2957
- var OWNS_SEPARATOR = /[\s,]+/u;
2958
- function undressed(text) {
2959
- return text.replaceAll("`", " ").trim();
2960
- }
2961
- function normaliseFolder(folder) {
2962
- let path = folder;
2963
- while (path.startsWith("./"))
2964
- path = path.slice(2);
2965
- while (path.endsWith("/"))
2966
- path = path.slice(0, -1);
2967
- return path;
2968
- }
2969
- function isRepoRelative(path) {
2970
- if (path === "" || path === "." || path.startsWith("/"))
2971
- return false;
2972
- return !path.split("/").includes("..");
2973
- }
2974
- function ownedFolders(text) {
2975
- const named = undressed(text).split(OWNS_SEPARATOR).filter((folder) => folder !== "");
2976
- const owns = named.map(normaliseFolder).filter(isRepoRelative);
2977
- const rejected = named.filter((folder) => !isRepoRelative(normaliseFolder(folder)));
2978
- return { owns: [...new Set(owns)], rejected };
2979
- }
2980
- function readBoardBody(body) {
2981
- const lines = bodyLines(body);
2982
- const ownerLine = findField(lines, OWNER_FIELD);
2983
- const ownsLine = findField(lines, BOARD_OWNS_FIELD);
2984
- const handle = ownerLine === null ? null : undressed(ownerLine.text);
2985
- const isOwned = handle !== null && isOwnerHandle(handle);
2986
- const folders = ownsLine === null ? { owns: [], rejected: [] } : ownedFolders(ownsLine.text);
2987
- const problems = [
2988
- ownerLine === null || isOwned ? null : { kind: "malformed-owner", line: ownerLine.number, text: ownerLine.text, rejected: Object.freeze([]) },
2989
- ownsLine === null || folders.rejected.length === 0 ? null : { kind: "malformed-owns", line: ownsLine.number, text: ownsLine.text, rejected: Object.freeze(folders.rejected) }
2990
- ];
2991
- return Object.freeze({
2992
- lines: parseRoadmapBody(body),
2993
- owner: isOwned ? handle : null,
2994
- owns: Object.freeze(folders.owns),
2995
- problems: Object.freeze(problems.filter((problem) => problem !== null))
2996
- });
2997
- }
2998
-
2999
- // src/board/owner-resolve.ts
3000
- var NOT_FOUND = "(HTTP 404)";
3001
- function ownerLookupArgs(handle) {
3002
- if (!isOwnerHandle(handle)) {
3003
- return null;
3004
- }
3005
- const [account = "", slug] = handle.slice(1).split("/");
3006
- return slug === undefined ? ["api", `users/${account}`] : ["api", `orgs/${account}/teams/${slug}`];
3007
- }
3008
- function failureReason(result, command) {
3009
- const written = result.stderr.trim() || result.stdout.trim();
3010
- return written === "" ? `${command} failed and wrote nothing` : `${command} failed: ${written}`;
3011
- }
3012
- async function lookUp(gh, handle) {
3013
- const args = ownerLookupArgs(handle);
3014
- if (args === null) {
3015
- return { handle, state: "unknown", reason: `${JSON.stringify(handle)} is not an @login or @org/team handle, so nothing was asked` };
3016
- }
3017
- const result = await gh(args);
3018
- if (result.ok) {
3019
- return { handle, state: "resolved" };
3020
- }
3021
- return result.stderr.includes(NOT_FOUND) ? { handle, state: "unresolved" } : { handle, state: "unknown", reason: failureReason(result, `gh ${args.join(" ")}`) };
3022
- }
3023
- function createOwnerResolver(options) {
3024
- const { gh } = options;
3025
- const answers = new Map;
3026
- return (handle) => {
3027
- const key = handle.toLowerCase();
3028
- const known = answers.get(key);
3029
- if (known !== undefined) {
3030
- return known.then((answer) => ({ ...answer, handle }));
3031
- }
3032
- const pending = lookUp(gh, handle);
3033
- answers.set(key, pending);
3034
- return pending;
3035
- };
3036
- }
3037
-
3038
2961
  // src/commands/switch.ts
3039
2962
  var USAGE5 = "rafa switch <n | -> [--no-rehome]";
3040
2963
  var SWITCH_REFUSAL_EXIT = 2;
@@ -3068,9 +2991,6 @@ function readRehome(flags) {
3068
2991
  return value;
3069
2992
  throw lineRefusal(`--${REHOME_FLAG} takes no value, and read "${value}" as one; type the target before the flags`, USAGE5);
3070
2993
  }
3071
- function openBoards(listing) {
3072
- return listing.filter((issue) => issue.state === "OPEN" && issue.labels.includes(ROADMAP_LABEL)).sort((a, b) => a.number - b.number);
3073
- }
3074
2994
  async function kindOf(number, board) {
3075
2995
  const row = board.rows.get(number);
3076
2996
  if (row === undefined) {
@@ -3087,20 +3007,6 @@ async function kindOf(number, board) {
3087
3007
  return { why: `${id(number)} is neither a board nor an epic: it carries neither ${ROADMAP_LABEL} nor type:epic` };
3088
3008
  return row.state === "CLOSED" ? { why: `${id(number)} is a closed ${kind}` } : { kind };
3089
3009
  }
3090
- function lists(number, epic, board) {
3091
- const row = board.rows.get(number);
3092
- if (row?.state !== "OPEN")
3093
- return false;
3094
- return parseRoadmapBody(row.body).some((line) => line.issue === epic);
3095
- }
3096
- async function boardOfEpic(epic, current, board) {
3097
- if (lists(current.board, epic, board))
3098
- return current.board;
3099
- const fallback = await board.defaultBoard();
3100
- if (lists(fallback, epic, board))
3101
- return fallback;
3102
- return openBoards(board.listing).find((row) => lists(row.number, epic, board))?.number ?? fallback;
3103
- }
3104
3010
  function boardPlace(number, board, epics) {
3105
3011
  const row = board.rows.get(number);
3106
3012
  const epic = row === undefined ? null : firstNowEpic(parseRoadmapBody(row.body), board.listing, epics)?.number ?? null;
@@ -7559,12 +7465,14 @@ function refuseInFlight(path) {
7559
7465
  }
7560
7466
  }
7561
7467
  }
7562
- function checkedCounts(db, tables, original) {
7468
+ function checkedCounts(db, tables, original, added = {}) {
7563
7469
  return tables.map((table) => {
7564
7470
  const rows = count(db, `SELECT count(*) AS n FROM main.${quoted(table)}`);
7565
7471
  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)}`);
7472
+ const addedRows = added[table] ?? 0;
7473
+ if (rows !== originalRows + addedRows) {
7474
+ const plus = addedRows === 0 ? "" : ` plus ${String(addedRows)} added`;
7475
+ throw new RebuildRefusal(`the rebuilt ${table} holds ${String(rows)} rows, the store ${String(originalRows)}${plus}`);
7568
7476
  }
7569
7477
  return { table, rows };
7570
7478
  });
@@ -7978,8 +7886,291 @@ function createCopyCommand(seams = {}) {
7978
7886
  }
7979
7887
  var copy_default = createCopyCommand();
7980
7888
 
7981
- // src/commands/effort/migrate.ts
7982
- import { join as join20 } from "path";
7889
+ // src/commands/effort/merge.ts
7890
+ import { join as join21, resolve as resolve8 } from "path";
7891
+
7892
+ // src/effort/store/merge-store.ts
7893
+ import { randomUUID as randomUUID2 } from "crypto";
7894
+ import { existsSync as existsSync9, mkdtempSync as mkdtempSync2, rmSync as rmSync4 } from "fs";
7895
+ import { tmpdir as tmpdir2 } from "os";
7896
+ import { dirname as dirname3, join as join20 } from "path";
7897
+ import { Database as Database4, SQLiteError as SQLiteError2 } from "bun:sqlite";
7898
+
7899
+ // src/effort/store/merge-commit-gaps.ts
7900
+ var GAP_FIELD = "minutesSincePrevious";
7901
+ var UPDATE_GAP = "UPDATE commits SET row_json = ? WHERE seq = ?";
7902
+ function parsedObject(text) {
7903
+ try {
7904
+ const value = JSON.parse(text);
7905
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
7906
+ } catch {
7907
+ return null;
7908
+ }
7909
+ }
7910
+ function withoutGap(rowJson) {
7911
+ const row2 = parsedObject(rowJson);
7912
+ if (row2 === null)
7913
+ return rowJson;
7914
+ const rest = Object.fromEntries(Object.entries(row2).filter(([key]) => key !== GAP_FIELD));
7915
+ return JSON.stringify(rest);
7916
+ }
7917
+ function readCommits(db) {
7918
+ return db.query("SELECT seq, sha, row_json FROM commits").all().map(({ seq, sha, row_json: rowJson }) => {
7919
+ const row2 = parsedObject(rowJson);
7920
+ const timestamp = typeof row2?.timestamp === "string" ? row2.timestamp : null;
7921
+ return { seq, sha, row: row2, timestamp, epoch: timestamp === null ? Number.NaN : Date.parse(timestamp) };
7922
+ });
7923
+ }
7924
+ function timeOrder(entries) {
7925
+ return entries.filter((entry) => !Number.isNaN(entry.epoch)).sort((left, right) => left.epoch - right.epoch || compareText(left.sha, right.sha));
7926
+ }
7927
+ function compareText(left, right) {
7928
+ if (left === right)
7929
+ return 0;
7930
+ return left < right ? -1 : 1;
7931
+ }
7932
+ function storedGap(entry) {
7933
+ const value = entry.row?.[GAP_FIELD];
7934
+ return typeof value === "number" ? value : null;
7935
+ }
7936
+ function namedRows(inserted, positions, order) {
7937
+ const position = positions.get(inserted.seq);
7938
+ if (position === undefined)
7939
+ return [inserted.seq];
7940
+ const next = order[position + 1];
7941
+ return next === undefined ? [inserted.seq] : [inserted.seq, next.seq];
7942
+ }
7943
+ function recomputedGap(entry, positions, order) {
7944
+ const position = positions.get(entry.seq);
7945
+ if (position === undefined || position === 0)
7946
+ return null;
7947
+ const previous = order[position - 1];
7948
+ if (previous?.timestamp == null || entry.timestamp === null)
7949
+ return null;
7950
+ return minutesBetween(previous.timestamp, entry.timestamp);
7951
+ }
7952
+ function recomputeCommitGaps(db, inserted) {
7953
+ return db.transaction(() => {
7954
+ const entries = readCommits(db);
7955
+ const bySeq = new Map(entries.map((entry) => [entry.seq, entry]));
7956
+ const order = timeOrder(entries);
7957
+ const positions = new Map(order.map((entry, position) => [entry.seq, position]));
7958
+ const named = new Set(inserted.flatMap((seq) => {
7959
+ const entry = bySeq.get(seq);
7960
+ return entry === undefined ? [] : namedRows(entry, positions, order);
7961
+ }));
7962
+ const targets = [...named].map((seq) => bySeq.get(seq)).filter((entry) => entry?.row != null).sort((left, right) => (positions.get(left.seq) ?? -1) - (positions.get(right.seq) ?? -1));
7963
+ return targets.flatMap((entry) => {
7964
+ const before = storedGap(entry);
7965
+ const after = recomputedGap(entry, positions, order);
7966
+ if (before === after && entry.row !== null && Object.hasOwn(entry.row, GAP_FIELD))
7967
+ return [];
7968
+ db.query(UPDATE_GAP).run(JSON.stringify({ ...entry.row, [GAP_FIELD]: after }), entry.seq);
7969
+ return [{ seq: entry.seq, sha: entry.sha, before, after }];
7970
+ });
7971
+ })();
7972
+ }
7973
+
7974
+ // src/effort/store/merge-rules.ts
7975
+ var SET_ONCE = "set-once";
7976
+ var RECOMPUTED = "recomputed";
7977
+ var APPEND_ONLY = {};
7978
+ function appendOnly(...identity) {
7979
+ return { scope: "merged", identity, edited: APPEND_ONLY };
7980
+ }
7981
+ var MERGE_RULES = {
7982
+ sessions: appendOnly("session_id"),
7983
+ commits: { scope: "merged", identity: ["sha"], edited: { row_json: RECOMPUTED } },
7984
+ findings: { scope: "merged", identity: ["id"], edited: { tracker_ref: SET_ONCE } },
7985
+ blockers: appendOnly("id"),
7986
+ out_of_scope_bugs: appendOnly("id"),
7987
+ report_absences: appendOnly("id"),
7988
+ task_reports: appendOnly("id"),
7989
+ preflight: appendOnly("run_id", "position"),
7990
+ dispatches: appendOnly("session_id"),
7991
+ changes: appendOnly("id"),
7992
+ skill_invocations: appendOnly("session_id", "name", "sidechain"),
7993
+ plan_ci: appendOnly("pr", "head_sha", "read_at"),
7994
+ schema_migrations: { scope: "local" },
7995
+ store_meta: { scope: "local" },
7996
+ merges: { scope: "local" },
7997
+ merge_conflicts: { scope: "local" }
7998
+ };
7999
+
8000
+ // src/effort/store/merge-union.ts
8001
+ import { SQLiteError } from "bun:sqlite";
8002
+ class UnionSchemaMismatch extends Error {
8003
+ constructor(message) {
8004
+ super(message);
8005
+ this.name = "UnionSchemaMismatch";
8006
+ }
8007
+ }
8008
+ var UNIQUE_REFUSAL = "SQLITE_CONSTRAINT_UNIQUE";
8009
+ function mergedTables() {
8010
+ return Object.entries(MERGE_RULES).flatMap(([table, rule]) => rule.scope === "merged" ? [{ table, identity: rule.identity }] : []);
8011
+ }
8012
+ function carriedColumns(db, table) {
8013
+ return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name: name2 }) => name2).filter((name2) => name2 !== "seq");
8014
+ }
8015
+ function refuseColumnMismatch(local, other, tables) {
8016
+ const faults = tables.flatMap(({ table }) => {
8017
+ const here = new Set(carriedColumns(local, table));
8018
+ const there = new Set(carriedColumns(other, table));
8019
+ const onlyHere = [...here].filter((column) => !there.has(column)).map((column) => `${table}.${column} only here`);
8020
+ const onlyThere = [...there].filter((column) => !here.has(column)).map((column) => `${table}.${column} only there`);
8021
+ return [...onlyHere, ...onlyThere];
8022
+ });
8023
+ if (faults.length > 0) {
8024
+ throw new UnionSchemaMismatch(`the two stores hold different columns (${faults.join(", ")}); bring both to the same migrations first`);
8025
+ }
8026
+ }
8027
+ function valueOf(row2, column) {
8028
+ return row2[column] ?? null;
8029
+ }
8030
+ function byOrigin(local, table, row2) {
8031
+ const store = valueOf(row2, "origin_store");
8032
+ const seq = valueOf(row2, "origin_seq");
8033
+ if (store === null || seq === null)
8034
+ return null;
8035
+ return local.query(`SELECT seq FROM ${quoted(table)} WHERE origin_store = ? AND origin_seq = ?`).get(store, seq)?.seq ?? null;
8036
+ }
8037
+ function byIdentity(local, { table, identity }, row2) {
8038
+ const where = identity.map((column) => `${quoted(column)} IS ?`).join(" AND ");
8039
+ return local.query(`SELECT seq FROM ${quoted(table)} WHERE ${where} ORDER BY seq LIMIT 1`).get(...identity.map((column) => valueOf(row2, column)))?.seq ?? null;
8040
+ }
8041
+ function insertRow(local, table, columns2, row2, incomingSeq) {
8042
+ const columnList = columns2.map(quoted).join(", ");
8043
+ const placeholders = columns2.map(() => "?").join(", ");
8044
+ const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${quoted(table)})`;
8045
+ try {
8046
+ const inserted = local.query(`INSERT INTO ${quoted(table)} (seq, ${columnList}) VALUES (${nextSeq}, ${placeholders}) RETURNING seq`).get(...columns2.map((column) => valueOf(row2, column)));
8047
+ if (inserted === null)
8048
+ throw new Error(`the insert into ${table} returned no seq`);
8049
+ return { kind: "added", localSeq: inserted.seq };
8050
+ } catch (error) {
8051
+ if (!(error instanceof SQLiteError) || error.code !== UNIQUE_REFUSAL)
8052
+ throw error;
8053
+ return { kind: "collided", collision: { incomingSeq, reason: error.message } };
8054
+ }
8055
+ }
8056
+ function placeRow(local, merged2, columns2, row2) {
8057
+ const incomingSeq = Number(valueOf(row2, "seq"));
8058
+ const origin = byOrigin(local, merged2.table, row2);
8059
+ if (origin !== null)
8060
+ return { kind: "matched", match: { localSeq: origin, incomingSeq, by: "origin" } };
8061
+ const identity = byIdentity(local, merged2, row2);
8062
+ if (identity !== null)
8063
+ return { kind: "matched", match: { localSeq: identity, incomingSeq, by: "identity" } };
8064
+ return insertRow(local, merged2.table, columns2, row2, incomingSeq);
8065
+ }
8066
+ function unionTable(local, other, merged2) {
8067
+ const columns2 = carriedColumns(local, merged2.table);
8068
+ const incoming = other.query(`SELECT seq, ${columns2.map(quoted).join(", ")} FROM ${quoted(merged2.table)} ORDER BY seq`).all();
8069
+ const outcomes = incoming.map((row2) => placeRow(local, merged2, columns2, row2));
8070
+ return {
8071
+ table: merged2.table,
8072
+ added: outcomes.flatMap((outcome) => outcome.kind === "added" ? [outcome.localSeq] : []),
8073
+ matched: outcomes.flatMap((outcome) => outcome.kind === "matched" ? [outcome.match] : []),
8074
+ collided: outcomes.flatMap((outcome) => outcome.kind === "collided" ? [outcome.collision] : [])
8075
+ };
8076
+ }
8077
+ function unionStores(local, other) {
8078
+ const tables = mergedTables();
8079
+ refuseColumnMismatch(local, other, tables);
8080
+ return local.transaction(() => tables.map((merged2) => unionTable(local, other, merged2)))();
8081
+ }
8082
+
8083
+ // src/effort/store/merge-conflicts.ts
8084
+ var SET_ONCE_FILLS = {
8085
+ "findings.tracker_ref": "UPDATE findings SET tracker_ref = ? WHERE seq = ? AND tracker_ref IS NULL"
8086
+ };
8087
+ var RECOMPUTED_COMPARISONS = {
8088
+ "commits.row_json": withoutGap
8089
+ };
8090
+ var NOT_CONTENT = new Set(["seq", "origin_store", "origin_seq"]);
8091
+ var INSERT_CONFLICT = `INSERT INTO merge_conflicts (merge_id, table_name, local_seq, field, incoming, recorded_at)
8092
+ VALUES (?, ?, ?, ?, ?, ?)`;
8093
+ function rowAt(db, table, columns2, seq, side) {
8094
+ const row2 = db.query(`SELECT seq, ${columns2.map(quoted).join(", ")} FROM ${quoted(table)} WHERE seq = ?`).get(seq);
8095
+ if (row2 === null)
8096
+ throw new Error(`the union matched ${table} row ${seq} ${side}, which that store does not hold`);
8097
+ return row2;
8098
+ }
8099
+ function sameValue(left, right) {
8100
+ if (left instanceof Uint8Array && right instanceof Uint8Array)
8101
+ return Buffer.from(left).equals(right);
8102
+ return (left ?? null) === (right ?? null);
8103
+ }
8104
+ function fillStatement(table, field) {
8105
+ const statement = SET_ONCE_FILLS[`${table}.${field}`];
8106
+ if (statement === undefined) {
8107
+ throw new Error(`${table}.${field} is set-once in MERGE_RULES and has no statement in SET_ONCE_FILLS`);
8108
+ }
8109
+ return statement;
8110
+ }
8111
+ function settleSetOnce(local, table, field, here, there) {
8112
+ const incoming = there[field] ?? null;
8113
+ if (incoming === null)
8114
+ return { skipped: true, filled: [], conflictFields: [] };
8115
+ if ((here[field] ?? null) !== null)
8116
+ return { skipped: false, filled: [], conflictFields: [field] };
8117
+ local.query(fillStatement(table, field)).run(incoming, Number(here.seq));
8118
+ return { skipped: true, filled: [field], conflictFields: [] };
8119
+ }
8120
+ function settleRecomputed(table, field, here, there) {
8121
+ const comparable = RECOMPUTED_COMPARISONS[`${table}.${field}`];
8122
+ if (comparable === undefined) {
8123
+ throw new Error(`${table}.${field} is recomputed in MERGE_RULES and has no entry in RECOMPUTED_COMPARISONS`);
8124
+ }
8125
+ const hereValue = here[field];
8126
+ const thereValue = there[field];
8127
+ const same = typeof hereValue === "string" && typeof thereValue === "string" && comparable(hereValue) === comparable(thereValue);
8128
+ return same ? { skipped: true, filled: [], conflictFields: [] } : { skipped: false, filled: [], conflictFields: [null] };
8129
+ }
8130
+ function settlePair(local, table, edited, here, there) {
8131
+ const differing = Object.keys(here).filter((column) => !NOT_CONTENT.has(column) && !sameValue(here[column], there[column]));
8132
+ if (differing.length === 0)
8133
+ return { skipped: true, filled: [], conflictFields: [] };
8134
+ if (differing.some((column) => !Object.hasOwn(edited, column)))
8135
+ return { skipped: false, filled: [], conflictFields: [null] };
8136
+ const outcomes = differing.map((field) => {
8137
+ if (edited[field] === SET_ONCE)
8138
+ return settleSetOnce(local, table, field, here, there);
8139
+ if (edited[field] === RECOMPUTED)
8140
+ return settleRecomputed(table, field, here, there);
8141
+ throw new Error(`${table}.${field} has the rule ${edited[field]}, which the merge does not apply`);
8142
+ });
8143
+ return {
8144
+ skipped: outcomes.every((outcome) => outcome.skipped),
8145
+ filled: outcomes.flatMap((outcome) => outcome.filled),
8146
+ conflictFields: outcomes.flatMap((outcome) => outcome.conflictFields)
8147
+ };
8148
+ }
8149
+ function settleTable(local, other, union, trail) {
8150
+ const rule = MERGE_RULES[union.table];
8151
+ if (rule?.scope !== "merged")
8152
+ throw new Error(`${union.table} is not a merged table in MERGE_RULES`);
8153
+ const columns2 = carriedColumns(local, union.table);
8154
+ const settled = union.matched.map((match) => {
8155
+ const here = rowAt(local, union.table, columns2, match.localSeq, "here");
8156
+ const there = rowAt(other, union.table, columns2, match.incomingSeq, "in the other store");
8157
+ const outcome = settlePair(local, union.table, rule.edited, here, there);
8158
+ const conflicts = outcome.conflictFields.map((field) => {
8159
+ local.query(INSERT_CONFLICT).run(trail.mergeId, union.table, match.localSeq, field, JSON.stringify(there), trail.recordedAt);
8160
+ return { localSeq: match.localSeq, incomingSeq: match.incomingSeq, field };
8161
+ });
8162
+ return { match, outcome, conflicts };
8163
+ });
8164
+ return {
8165
+ table: union.table,
8166
+ skipped: settled.flatMap(({ match, outcome }) => outcome.skipped ? [match.localSeq] : []),
8167
+ filled: settled.flatMap(({ match, outcome }) => outcome.filled.map((field) => ({ localSeq: match.localSeq, field }))),
8168
+ conflicts: settled.flatMap(({ conflicts }) => conflicts)
8169
+ };
8170
+ }
8171
+ function settleMatches(local, other, unions, trail) {
8172
+ return local.transaction(() => unions.map((union) => settleTable(local, other, union, trail)))();
8173
+ }
7983
8174
 
7984
8175
  // src/effort/store/migrate.ts
7985
8176
  import { existsSync as existsSync8 } from "fs";
@@ -7988,7 +8179,7 @@ var LIVE2 = "live";
7988
8179
  function asidePaths2(path, first, stamp) {
7989
8180
  return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
7990
8181
  }
7991
- function liveLoop(path, options) {
8182
+ function liveLoop(path, options, nothingDone = "Nothing was migrated.") {
7992
8183
  const root = storeProjectRoot(path);
7993
8184
  if (root === null)
7994
8185
  return;
@@ -7996,7 +8187,7 @@ function liveLoop(path, options) {
7996
8187
  try {
7997
8188
  records = readSessions(root, { isAlive: options.isAlive ?? isPidAlive });
7998
8189
  } 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.`);
8190
+ throw new RebuildRefusal(`the loop's run records cannot be read, so a live loop on ${path} cannot be ruled out` + ` (${messageOf(error)}). ${nothingDone}`);
8000
8191
  }
8001
8192
  return records.find((record) => record.state === "running" || record.state === "paused");
8002
8193
  }
@@ -8065,109 +8256,629 @@ function migrateStore(options) {
8065
8256
  const status = dryRun ? "would-migrate" : "migrated";
8066
8257
  return { ...found, ...planned, ...built, status, backupPath };
8067
8258
  }
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.`;
8259
+
8260
+ // src/effort/store/merge-store.ts
8261
+ var MOVE_TO_SQLITE = "rafa effort move --to=sqlite";
8262
+
8263
+ class MergeRefusal extends RebuildRefusal {
8264
+ reason;
8265
+ constructor(reason, message) {
8266
+ super(message);
8267
+ this.name = "MergeRefusal";
8268
+ this.reason = reason;
8269
+ }
8270
+ }
8271
+ var LIVE3 = "live";
8272
+ var MERGES_TABLE = "merges";
8273
+ var INSERT_MERGE = `INSERT INTO merges (id, other_store, merged_at, rows_added, rows_skipped, rows_in_conflict)
8274
+ VALUES (?, ?, ?, ?, ?, ?)`;
8275
+ var NOTHING_MERGED = "Nothing was merged.";
8276
+ function refuseNdjson(backend) {
8277
+ if (backend !== "ndjson")
8278
+ return;
8279
+ throw new MergeRefusal("ndjson", `REFUSED \u2014 this project keeps its sessions and commits as NDJSON (store: ndjson), and a merge needs the SQLite store. ${NOTHING_MERGED}` + ` Next safe step: ${MOVE_TO_SQLITE}`);
8280
+ }
8281
+ function refuseMissing(path, which) {
8282
+ if (existsSync9(path))
8283
+ return;
8284
+ throw new MergeRefusal("missing", `REFUSED \u2014 ${which} ${path} is not there. ${NOTHING_MERGED}`);
8285
+ }
8286
+ function refuseDamaged(path) {
8287
+ let answers;
8288
+ try {
8289
+ const db = new Database4(path, { readonly: true });
8290
+ try {
8291
+ answers = db.query("PRAGMA integrity_check").all().map((row2) => row2.integrity_check);
8292
+ } finally {
8293
+ db.close();
8294
+ }
8295
+ } catch (error) {
8296
+ if (!(error instanceof SQLiteError2))
8297
+ throw error;
8298
+ throw new MergeRefusal("damaged", `REFUSED \u2014 ${path} cannot be read as a SQLite store (${error.message}). ${NOTHING_MERGED}` + " Take a consistent copy on its device with rafa effort copy, and merge that.");
8299
+ }
8300
+ if (answers.length === 1 && answers[0] === "ok")
8301
+ return;
8302
+ throw new MergeRefusal("damaged", `REFUSED \u2014 ${path} failed integrity_check: ${answers.join("; ")}. ${NOTHING_MERGED}` + " Take a consistent copy on its device with rafa effort copy, and merge that.");
8303
+ }
8304
+ function writablePlan(path, migrations) {
8305
+ const plan2 = planSchema(readOnlySchema(path), sqliteCatalogue(migrations), "write", "open");
8306
+ if (plan2.verdict === "refuse")
8307
+ throw new MergeRefusal("schema", `REFUSED \u2014 ${path}: ${plan2.message}`);
8308
+ return plan2;
8309
+ }
8310
+ function recordedMeta(path) {
8311
+ const db = new Database4(path, { readonly: true });
8312
+ try {
8313
+ const hasTable = db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
8314
+ return hasTable ? readStoreMeta(db) : null;
8315
+ } finally {
8316
+ db.close();
8317
+ }
8318
+ }
8319
+ function refuseOtherProject(options) {
8320
+ const other = recordedMeta(options.otherPath);
8321
+ const here = recordedMeta(options.path)?.projectRootCommit ?? (options.readProject ?? readProjectIdentity)(dirname3(options.path)).rootCommit;
8322
+ const there = other?.projectRootCommit ?? null;
8323
+ if (here !== null && there !== null && here !== there) {
8324
+ throw new MergeRefusal("other-project", `REFUSED \u2014 ${options.otherPath} is the effort store of another project (root commit ${there});` + ` this store's project has root commit ${here}. ${NOTHING_MERGED}`);
8325
+ }
8326
+ return other?.storeId ?? null;
8327
+ }
8328
+ function refuseLiveLoop3(path, options) {
8329
+ const loop = liveLoop(path, options, NOTHING_MERGED);
8330
+ if (loop === undefined)
8331
+ return;
8332
+ throw new MergeRefusal("live-loop", `REFUSED \u2014 loop ${loop.sessionId} (pid ${String(loop.pid)}, plan ${loop.planStub ?? loop.plan}) is running on this store,` + ` and swapping the merged store in under it would lose what it writes. ${NOTHING_MERGED}` + " Finish or stop that loop, then run it again.");
8333
+ }
8334
+ function openOther(otherPath, plan2, stamp) {
8335
+ const needs = writeNames(plan2);
8336
+ if (needs.length === 0) {
8337
+ const db = new Database4(otherPath, { readonly: true });
8338
+ return { db, broughtForward: [], release: () => db.close() };
8339
+ }
8340
+ const dir = mkdtempSync2(join20(tmpdir2(), "rafa-merge-other-"));
8341
+ const release = (db) => {
8342
+ db?.close();
8343
+ rmSync4(dir, { recursive: true, force: true });
8344
+ };
8345
+ try {
8346
+ const copyPath = join20(dir, "other.sqlite");
8347
+ vacuumInto(otherPath, copyPath);
8348
+ const db = new Database4(copyPath, { readwrite: true });
8349
+ try {
8350
+ bringForward(db, copyPath, "write", "open", { ...stamp, builtAside: true });
8351
+ } catch (error) {
8352
+ release(db);
8353
+ throw error;
8354
+ }
8355
+ return { db, broughtForward: needs, release: () => release(db) };
8356
+ } catch (error) {
8357
+ release();
8358
+ throw error;
8359
+ }
8360
+ }
8361
+ function mergeInto(db, other, mergeId, otherStore, mergedAt) {
8362
+ return db.transaction(() => {
8363
+ const unions = unionStores(db, other);
8364
+ const settlements = settleMatches(db, other, unions, { mergeId, recordedAt: mergedAt });
8365
+ const gaps = recomputeCommitGaps(db, unions.find(({ table }) => table === "commits")?.added ?? []);
8366
+ const tables = unions.map((union, index) => {
8367
+ const settled = settlements[index];
8368
+ return {
8369
+ table: union.table,
8370
+ added: union.added.length,
8371
+ skipped: settled?.skipped.length ?? 0,
8372
+ inConflict: settled?.conflicts.length ?? 0,
8373
+ filled: settled?.filled.length ?? 0,
8374
+ collided: union.collided.length
8375
+ };
8376
+ });
8377
+ const sum = (field) => tables.reduce((total, entry) => total + entry[field], 0);
8378
+ const totals = { rowsAdded: sum("added"), rowsSkipped: sum("skipped"), rowsInConflict: sum("inConflict") };
8379
+ db.query(INSERT_MERGE).run(mergeId, otherStore, mergedAt, totals.rowsAdded, totals.rowsSkipped, totals.rowsInConflict);
8380
+ return { tables, ...totals, gapsRewritten: gaps.length };
8381
+ })();
8382
+ }
8383
+ function addedRows(merged2) {
8384
+ return {
8385
+ ...Object.fromEntries(merged2.tables.map(({ table, added }) => [table, added])),
8386
+ [MERGES_TABLE]: 1,
8387
+ merge_conflicts: merged2.rowsInConflict
8388
+ };
8389
+ }
8390
+ function buildMerged(path, parallelPath, other, merge) {
8391
+ vacuumInto(path, parallelPath);
8392
+ const db = new Database4(parallelPath, { readwrite: true });
8393
+ try {
8394
+ bringForward(db, parallelPath, "write", "open", { ...merge.stamp, builtAside: true });
8395
+ const merged2 = mergeInto(db, other, merge.id, merge.otherStore, merge.stamp.now().toISOString());
8396
+ db.run(`ATTACH DATABASE ? AS ${LIVE3}`, [path]);
8397
+ const live = new Set(tableNames(db, LIVE3));
8398
+ checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE3, addedRows(merged2));
8399
+ db.run(`DETACH DATABASE ${LIVE3}`);
8400
+ refuseCorrupt(db);
8401
+ refuseUnusable(db, parallelPath, merge.stamp.migrations);
8402
+ return merged2;
8403
+ } finally {
8404
+ db.close();
8405
+ }
8406
+ }
8407
+ function refuseUnmergeable(options, migrations) {
8408
+ const { path, otherPath } = options;
8409
+ refuseNdjson(options.backend);
8410
+ refuseMissing(path, "this store");
8411
+ refuseMissing(otherPath, "the other store");
8412
+ refuseInFlight(path);
8413
+ refuseInFlight(otherPath);
8414
+ refuseDamaged(otherPath);
8415
+ const otherPlan = writablePlan(otherPath, migrations);
8416
+ const localPlan = writablePlan(path, migrations);
8417
+ const otherStore = refuseOtherProject(options);
8418
+ if (!options.dryRun) {
8419
+ refuseUnownedDevelopmentWrite(path, [...writeNames(localPlan), MERGES_TABLE], options);
8420
+ refuseLiveLoop3(path, options);
8421
+ }
8422
+ return { otherPlan, otherStore };
8423
+ }
8424
+ function mergeStore(options) {
8425
+ const { path, otherPath, dryRun, stamp } = options;
8426
+ guardTestProcess(path);
8427
+ guardTestProcess(otherPath);
8428
+ const migrations = options.migrations ?? SQLITE_MIGRATIONS;
8429
+ const { otherPlan, otherStore } = refuseUnmergeable(options, migrations);
8430
+ const mergeStamp = {
8431
+ migrations,
8432
+ appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
8433
+ now: options.now ?? (() => new Date)
8434
+ };
8435
+ const mergeId = (options.newMergeId ?? randomUUID2)();
8436
+ const other = openOther(otherPath, otherPlan, mergeStamp);
8437
+ try {
8438
+ const { built, backupPath } = rebuildAside({
8439
+ path,
8440
+ parallelPath: `${path}.merge-${stamp}`,
8441
+ backupPath: `${path}.before-merge-${stamp}.bak`,
8442
+ dryRun,
8443
+ build: (parallelPath) => buildMerged(path, parallelPath, other.db, { id: mergeId, otherStore, stamp: mergeStamp })
8444
+ });
8445
+ const status = dryRun ? "would-merge" : "merged";
8446
+ return { path, otherPath, status, mergeId, otherStore, otherBroughtForward: other.broughtForward, ...built, backupPath };
8447
+ } finally {
8448
+ other.release();
8449
+ }
8450
+ }
8451
+
8452
+ // src/commands/effort/merge.ts
8453
+ var COMMAND_NAME7 = "rafa effort merge";
8454
+ var MERGE_USAGE = "rafa effort merge <file> [--dry-run]";
8455
+ var DRY_RUN_FLAG3 = "dry-run";
8456
+ var REFUSED_EXIT2 = 1;
8457
+ var UNMERGEABLE_EXIT = 2;
8458
+ var UNMERGEABLE_REASONS = new Set(["other-project", "ndjson"]);
8459
+ function tableLine(entry) {
8460
+ const extra = [
8461
+ ...entry.filled === 0 ? [] : [`${String(entry.filled)} filled`],
8462
+ ...entry.collided === 0 ? [] : [`${String(entry.collided)} refused by another unique key, kept in the other file only`]
8463
+ ];
8464
+ const counts = [
8465
+ `${String(entry.added)} added`,
8466
+ `${String(entry.skipped)} skipped`,
8467
+ `${String(entry.inConflict)} in conflict`,
8468
+ ...extra
8469
+ ];
8470
+ return ` ${entry.table}: ${counts.join(", ")}`;
8471
+ }
8472
+ function closingLine(result) {
8473
+ if (result.status === "would-merge") {
8474
+ return "\uD83D\uDD0D Dry run: the merged store was built beside it, checked (row counts, integrity_check, this rafa's" + ` schema plan) and deleted. Run \`${COMMAND_NAME7} ${result.otherPath}\` to swap it in; the original would be` + ` kept whole at ${result.path}.before-merge-<stamp>.bak.`;
8475
+ }
8476
+ return `\u2705 Merged. The original is kept whole at ${String(result.backupPath)}; rename it back to undo.`;
8477
+ }
8478
+ function renderMerge(result) {
8479
+ const other = result.otherStore === null ? "a store that names no origin" : `store ${result.otherStore}`;
8480
+ const forward = result.otherBroughtForward.length === 0 ? [] : [`A copy of the other store was brought forward through ${result.otherBroughtForward.join(", ")}; the file itself was only read.`];
8481
+ return [
8482
+ `Merges ${result.otherPath} (${other}) into ${result.path}, as merge ${result.mergeId}.`,
8483
+ ...forward,
8484
+ ...result.tables.map(tableLine),
8485
+ `Total: ${String(result.rowsAdded)} added, ${String(result.rowsSkipped)} skipped,` + ` ${String(result.rowsInConflict)} in conflict; ${String(result.gapsRewritten)} commit gaps recomputed.`,
8486
+ closingLine(result)
8487
+ ];
8488
+ }
8489
+ function storePath(context, root) {
8490
+ try {
8491
+ return join21(effortStoreDir(root, context.env), SQLITE_STORE_FILE_NAME);
8492
+ } catch (error) {
8493
+ throw new CommandExit(REFUSED_EXIT2, `\u274C ${COMMAND_NAME7}: ${messageOf(error)}`);
8494
+ }
8495
+ }
8496
+ function refusalExit(error) {
8497
+ if (error instanceof MergeRefusal) {
8498
+ const code = UNMERGEABLE_REASONS.has(error.reason) ? UNMERGEABLE_EXIT : REFUSED_EXIT2;
8499
+ return new CommandExit(code, `\u274C ${COMMAND_NAME7}: ${error.message}`);
8500
+ }
8501
+ const known = error instanceof RebuildRefusal || error instanceof DevelopmentBuildRefusedError || error instanceof UnionSchemaMismatch;
8502
+ return known ? new CommandExit(REFUSED_EXIT2, `\u274C ${COMMAND_NAME7}: ${error.message}`) : null;
8503
+ }
8504
+ function probeSeams(seams) {
8505
+ return {
8506
+ ...seams.isAlive === undefined ? {} : { isAlive: seams.isAlive },
8507
+ ...seams.identity === undefined ? {} : { identity: seams.identity },
8508
+ ...seams.migrations === undefined ? {} : { migrations: seams.migrations },
8509
+ ...seams.newMergeId === undefined ? {} : { newMergeId: seams.newMergeId },
8510
+ ...seams.readProject === undefined ? {} : { readProject: seams.readProject }
8511
+ };
8512
+ }
8513
+ function runMerge(context, seams) {
8514
+ const file = expectOneArgument(context.args, MERGE_USAGE);
8515
+ const dryRun = readSwitch(DRY_RUN_FLAG3, context.flags[DRY_RUN_FLAG3], `Usage: ${MERGE_USAGE}`);
8516
+ const project = requireProject(context, COMMAND_NAME7);
8517
+ const path = storePath(context, project.root);
8518
+ const backend = resolveProjectConfig(project, COMMAND_NAME7, (message) => context.output.warn(message)).store;
8519
+ const now = seams.now ?? (() => new Date);
8520
+ const otherPath = resolve8((seams.cwd ?? (() => process.cwd()))(), file);
8521
+ let result;
8522
+ try {
8523
+ result = mergeStore({ path, otherPath, backend, dryRun, stamp: fileStamp(now()), now, env: context.env, ...probeSeams(seams) });
8524
+ } catch (error) {
8525
+ throw refusalExit(error) ?? error;
8526
+ }
8527
+ if (context.outputMode === "json") {
8528
+ context.output.result(result);
8529
+ return;
8530
+ }
8531
+ for (const line of renderMerge(result))
8532
+ context.output.info(line);
8533
+ }
8534
+ function createMergeCommand(seams = {}) {
8535
+ const command = {
8536
+ name: "effort merge",
8537
+ subject: "effort",
8538
+ action: "merge",
8539
+ summary: "merge another device's effort store file into this one, keeping every row and the original",
8540
+ description: "Adds to `.rafa/effort/effort.sqlite`, or the store under `RAFA_EFFORT_DIR`, every row another" + " device's store file holds that this one lacks. A row matches on its origin pair, or on its table's" + " identity columns when either side has no origin; an equal pair is skipped, a set-once field NULL here" + " is filled, and any other difference is kept on both sides and recorded in `merge_conflicts`. The gap" + " of each commit brought in, and of the commit after it, is recomputed. The other file is only read." + " The merged store is built beside this one, checked (each table's count plus the rows added," + " integrity_check, the schema plan), and swapped in with the original kept whole as" + " `effort.sqlite.before-merge-<stamp>.bak`. It prints the rows added, skipped and in conflict per table" + " and the backup's name. Exit code 2 for another project's store, and for a `store: ndjson` project," + " naming `rafa effort move --to=sqlite`; exit code 1, changing nothing, for a missing or damaged file," + " a write in flight, a loop session running or paused, and a development build over a store it does not" + " own. With `--output=json` the outcome is the data of the terminal result event. Starts no session.",
8541
+ args: [
8542
+ {
8543
+ name: "file",
8544
+ description: "The other device's `effort.sqlite`, a consistent copy such as `rafa effort copy` makes;" + " a relative path is read from the directory the command runs in.",
8545
+ type: "string",
8546
+ required: true
8547
+ }
8548
+ ],
8549
+ flags: [
8550
+ {
8551
+ name: DRY_RUN_FLAG3,
8552
+ description: "Build and check the merged store, print what it would add, then delete it. This store is" + " only read, so this runs beside a live loop and from a development build, and can be repeated.",
8553
+ type: "boolean"
8554
+ }
8555
+ ],
8556
+ examples: [
8557
+ {
8558
+ cmd: "rafa effort merge ../laptop-effort/effort.sqlite --dry-run",
8559
+ note: "Prints what the merge would add, skip and record as conflicts, changing nothing."
8560
+ },
8561
+ {
8562
+ cmd: "rafa effort merge ../laptop-effort/effort.sqlite",
8563
+ note: "Swaps the merged store in and prints where the original was kept."
8564
+ }
8565
+ ],
8566
+ outputs: ["text", "json"],
8567
+ run: async (context) => {
8568
+ runMerge(context, seams);
8569
+ await Promise.resolve();
8570
+ }
8571
+ };
8572
+ return Object.freeze(command);
8573
+ }
8574
+ var merge_default = createMergeCommand();
8575
+
8576
+ // src/commands/effort/migrate.ts
8577
+ import { join as join22 } from "path";
8578
+ var COMMAND_NAME8 = "rafa effort migrate";
8579
+ var MIGRATE_USAGE = "rafa effort migrate [--dry-run]";
8580
+ var DRY_RUN_FLAG4 = "dry-run";
8581
+ var REFUSED_EXIT3 = 1;
8582
+ function builtLines(result) {
8583
+ const rows = result.counted.reduce((sum, table) => sum + table.rows, 0);
8584
+ 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.`;
8585
+ return [
8586
+ `Applies ${result.applying.join(", ")} to ${result.path}, logged as applied by ${result.appliedBy}. ${breaking}`,
8587
+ `Row counts match in ${String(result.counted.length)} tables, ${String(rows)} rows; integrity_check ok;` + ` schema version ${String(result.userVersion)} after.`
8588
+ ];
8589
+ }
8590
+ function renderMigrate(result) {
8591
+ switch (result.status) {
8592
+ case "missing":
8593
+ return [`No effort store at ${result.path}: nothing to migrate.`];
8594
+ case "current":
8595
+ return [`\u2705 ${result.path} is current: no migration is pending. Nothing to migrate.`];
8596
+ case "would-migrate":
8597
+ return [
8598
+ ...builtLines(result),
8599
+ "\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."
8600
+ ];
8601
+ case "migrated":
8602
+ return [
8603
+ ...builtLines(result),
8604
+ `\u2705 Migrated. The original is kept whole at ${String(result.backupPath)}; rename it back to undo.`
8605
+ ];
8606
+ }
8607
+ }
8608
+ function storePath2(context, root) {
8609
+ try {
8610
+ return join22(effortStoreDir(root, context.env), SQLITE_STORE_FILE_NAME);
8611
+ } catch (error) {
8612
+ throw new CommandExit(REFUSED_EXIT3, `\u274C ${COMMAND_NAME8}: ${messageOf(error)}`);
8613
+ }
8614
+ }
8615
+ function runMigrate(context, seams) {
8616
+ expectNoArgument(context.args, MIGRATE_USAGE);
8617
+ const dryRun = readSwitch(DRY_RUN_FLAG4, context.flags[DRY_RUN_FLAG4], `Usage: ${MIGRATE_USAGE}`);
8618
+ const path = storePath2(context, requireProject(context, COMMAND_NAME8).root);
8619
+ const now = seams.now ?? (() => new Date);
8620
+ let result;
8621
+ try {
8622
+ result = migrateStore({
8623
+ path,
8624
+ dryRun,
8625
+ stamp: fileStamp(now()),
8626
+ now,
8627
+ env: context.env,
8628
+ ...seams.identity === undefined ? {} : { identity: seams.identity },
8629
+ ...seams.isAlive === undefined ? {} : { isAlive: seams.isAlive },
8630
+ ...seams.migrations === undefined ? {} : { migrations: seams.migrations }
8631
+ });
8632
+ } catch (error) {
8633
+ if (error instanceof RebuildRefusal || error instanceof DevelopmentBuildRefusedError) {
8634
+ throw new CommandExit(REFUSED_EXIT3, `\u274C ${COMMAND_NAME8}: ${error.message}`);
8635
+ }
8636
+ throw error;
8637
+ }
8638
+ if (context.outputMode === "json") {
8639
+ context.output.result(result);
8640
+ return;
8641
+ }
8642
+ for (const line of renderMigrate(result))
8643
+ context.output.info(line);
8644
+ }
8645
+ function createMigrateCommand(seams = {}) {
8646
+ const command = {
8647
+ name: "effort migrate",
8648
+ subject: "effort",
8649
+ action: "migrate",
8650
+ summary: "apply the effort store's pending migrations, one that breaks older runtimes included, keeping the original",
8651
+ 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.",
8652
+ args: [],
8653
+ flags: [
8654
+ {
8655
+ name: DRY_RUN_FLAG4,
8656
+ 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.",
8657
+ type: "boolean"
8658
+ }
8659
+ ],
8660
+ examples: [
8661
+ {
8662
+ cmd: "rafa effort migrate --dry-run",
8663
+ note: "Prints what the migration applies and how it checked, changing nothing."
8664
+ },
8665
+ {
8666
+ cmd: "rafa effort migrate",
8667
+ note: "Swaps the migrated store in and prints where the original was kept."
8668
+ }
8669
+ ],
8670
+ outputs: ["text", "json"],
8671
+ run: async (context) => {
8672
+ runMigrate(context, seams);
8673
+ await Promise.resolve();
8674
+ }
8675
+ };
8676
+ return Object.freeze(command);
8677
+ }
8678
+ var migrate_default = createMigrateCommand();
8679
+
8680
+ // src/effort/store/move.ts
8681
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
8682
+ var MOVED_KINDS = Object.freeze(["sessions", "commits"]);
8683
+ var MOVE_TARGET = "sqlite";
8684
+ var STORE_LINE = `store: ${MOVE_TARGET}`;
8685
+ var STORE_KEY = /^store\s*:/u;
8686
+ var COMMENTED_STORE_KEY = /^#\s*store\s*:/u;
8687
+
8688
+ class MoveRefusal extends Error {
8689
+ reason;
8690
+ constructor(reason, message) {
8691
+ super(message);
8692
+ this.name = "MoveRefusal";
8693
+ this.reason = reason;
8694
+ }
8695
+ }
8696
+ function namesStore(text) {
8697
+ let document;
8698
+ try {
8699
+ document = Bun.YAML.parse(text);
8700
+ } catch {
8701
+ return false;
8702
+ }
8703
+ return isMapping(document) && Object.hasOwn(document, "store");
8704
+ }
8705
+ function withSqliteStoreSetting(text) {
8706
+ const lines2 = text.split(`
8707
+ `);
8708
+ const at = lines2.findIndex((line) => STORE_KEY.test(line));
8709
+ const commented = lines2.findIndex((line) => COMMENTED_STORE_KEY.test(line));
8710
+ const target = at >= 0 ? at : commented;
8711
+ if (target >= 0)
8712
+ return [...lines2.slice(0, target), STORE_LINE, ...lines2.slice(target + 1)].join(`
8713
+ `);
8714
+ if (namesStore(text))
8715
+ return null;
8716
+ const body = lines2.at(-1) === "" ? lines2.slice(0, -1) : lines2;
8717
+ return [...body, STORE_LINE, ""].join(`
8718
+ `);
8719
+ }
8720
+ function plannedConfig(path) {
8721
+ let text;
8722
+ try {
8723
+ text = readFileSync8(path, "utf8");
8724
+ } catch (error) {
8725
+ throw new MoveRefusal("config", `${path} could not be read: ${messageOf(error)}. Nothing was moved.`);
8726
+ }
8727
+ const refuse = (why) => new MoveRefusal("config", `${path} ${why}, so this move cannot set ${STORE_LINE}. Nothing was moved. Set it by hand, then run the move again.`);
8728
+ try {
8729
+ if (parseConfigText(text, path).values.store === MOVE_TARGET)
8730
+ return null;
8731
+ } catch (error) {
8732
+ throw refuse(`cannot be read as a config (${messageOf(error)})`);
8733
+ }
8734
+ const written = withSqliteStoreSetting(text);
8735
+ if (written === null)
8736
+ throw refuse("spells store in a shape this command does not edit");
8737
+ let read;
8738
+ try {
8739
+ read = parseConfigText(written, path).values.store;
8740
+ } catch (error) {
8741
+ throw refuse(`would not parse once edited (${messageOf(error)})`);
8742
+ }
8743
+ if (read !== MOVE_TARGET)
8744
+ throw refuse(`would read store as ${String(read)} once edited`);
8745
+ return written;
8746
+ }
8747
+ function keyedRows(kind, rows) {
8748
+ const project = EFFORT_KEY_PROJECTIONS[kind];
8749
+ const keyed = rows.filter((row2) => project(row2) !== null);
8750
+ return { keyed, unkeyed: rows.length - keyed.length };
8751
+ }
8752
+ function checkCounts(kind, before, added, read, after) {
8753
+ const missing = read.filter((key) => !after.has(key));
8754
+ if (after.size === before + added && missing.length === 0)
8755
+ return;
8756
+ throw new MoveRefusal("count", `the ${kind} check failed: the SQLite store held ${String(before)}, ${String(added)} were appended, and it now holds` + ` ${String(after.size)}, missing ${String(missing.length)} of the keys read. The config was not changed, so the` + " project still reads its NDJSON files.");
8757
+ }
8758
+ function moveKind(kind, source, target) {
8759
+ const reading = source.readRows(kind);
8760
+ const { keyed, unkeyed } = keyedRows(kind, reading.rows);
8761
+ const project = EFFORT_KEY_PROJECTIONS[kind];
8762
+ const before = target.keys(kind).size;
8763
+ const appended = target.append(kind, keyed);
8764
+ const keys = keyed.map((row2) => project(row2) ?? "");
8765
+ checkCounts(kind, before, appended.appended, keys, target.keys(kind));
8766
+ return {
8767
+ kind,
8768
+ from: source.path(kind),
8769
+ read: keyed.length,
8770
+ added: appended.appended,
8771
+ skipped: appended.skipped,
8772
+ unkeyed,
8773
+ unparsed: reading.unparsedLineCount
8774
+ };
8775
+ }
8776
+ function writeConfig(path, text) {
8777
+ try {
8778
+ writeFileSync2(path, text, "utf8");
8779
+ } catch (error) {
8780
+ throw new MoveRefusal("config", `${path} could not be written (${messageOf(error)}). The rows were moved and checked; set ${STORE_LINE}` + " there by hand, or run the move again, which adds nothing.");
8781
+ }
8782
+ }
8783
+ function moveToSqlite(options) {
8784
+ const source = options.source ?? openNdjsonStore(options.root);
8785
+ const target = options.target ?? openSqliteStore(options.root);
8786
+ const configPath = configFilePath(options.root);
8787
+ const config = plannedConfig(configPath);
8788
+ const kinds = MOVED_KINDS.map((kind) => moveKind(kind, source, target));
8789
+ if (config !== null)
8790
+ writeConfig(configPath, config);
8791
+ return { to: target.path("sessions"), kinds, configPath, configChanged: config !== null };
8792
+ }
8793
+
8794
+ // src/commands/effort/move.ts
8795
+ var COMMAND_NAME9 = "rafa effort move";
8796
+ var MOVE_USAGE = MOVE_TO_SQLITE;
8797
+ var TO_FLAG2 = "to";
8798
+ var REFUSED_EXIT4 = 1;
8799
+ function readTarget2(context) {
8800
+ const value = context.flags[TO_FLAG2];
8801
+ if (value === MOVE_TARGET)
8802
+ return;
8803
+ const found = value === undefined ? "no --to" : `--${TO_FLAG2}=${String(value)}`;
8804
+ throw new CommandExit(REFUSED_EXIT4, `\u274C ${COMMAND_NAME9}: ${found}; a move writes to ${MOVE_TARGET} and nothing else. Nothing was moved.
8805
+ Usage: ${MOVE_USAGE}`);
8806
+ }
8807
+ function kindLine(move3) {
8808
+ const extra = [
8809
+ ...move3.unkeyed === 0 ? [] : [`${String(move3.unkeyed)} without a key, not moved`],
8810
+ ...move3.unparsed === 0 ? [] : [`${String(move3.unparsed)} unparsed lines, not moved`]
8811
+ ];
8812
+ const counts = [`${String(move3.read)} read`, `${String(move3.added)} added`, `${String(move3.skipped)} already held`, ...extra];
8813
+ return ` ${move3.kind}: ${counts.join(", ")} (from ${move3.from})`;
8814
+ }
8815
+ function renderMove(result) {
8816
+ const config = result.configChanged ? `\u2705 Set store: ${MOVE_TARGET} in ${result.configPath}.` : `\u2705 ${result.configPath} already reads store: ${MOVE_TARGET}; left as it was.`;
8077
8817
  return [
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.`
8818
+ `Moves the NDJSON sessions and commits into ${result.to}.`,
8819
+ ...result.kinds.map(kindLine),
8820
+ "Checked: the SQLite store holds every key read, and its count is the old count plus the rows added.",
8821
+ config,
8822
+ "The NDJSON files are left where they are; running the move again adds nothing."
8080
8823
  ];
8081
8824
  }
8082
- function renderMigrate(result) {
8083
- switch (result.status) {
8084
- case "missing":
8085
- return [`No effort store at ${result.path}: nothing to migrate.`];
8086
- case "current":
8087
- return [`\u2705 ${result.path} is current: no migration is pending. Nothing to migrate.`];
8088
- case "would-migrate":
8089
- return [
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."
8092
- ];
8093
- case "migrated":
8094
- return [
8095
- ...builtLines(result),
8096
- `\u2705 Migrated. The original is kept whole at ${String(result.backupPath)}; rename it back to undo.`
8097
- ];
8098
- }
8825
+ function refusalExit2(error) {
8826
+ const known = error instanceof MoveRefusal || error instanceof SchemaRefusedError || error instanceof DevelopmentBuildRefusedError;
8827
+ return known ? new CommandExit(REFUSED_EXIT4, `\u274C ${COMMAND_NAME9}: ${error.message}`) : null;
8099
8828
  }
8100
- function storePath(context, root) {
8829
+ function runMove(context) {
8830
+ expectNoArgument(context.args, MOVE_USAGE);
8831
+ readTarget2(context);
8832
+ const project = requireProject(context, COMMAND_NAME9);
8833
+ resolveProjectConfig(project, COMMAND_NAME9, (message) => context.output.warn(message));
8101
8834
  try {
8102
- return join20(effortStoreDir(root, context.env), SQLITE_STORE_FILE_NAME);
8835
+ effortStoreDir(project.root, context.env);
8103
8836
  } catch (error) {
8104
- throw new CommandExit(REFUSED_EXIT2, `\u274C ${COMMAND_NAME7}: ${messageOf(error)}`);
8837
+ throw new CommandExit(REFUSED_EXIT4, `\u274C ${COMMAND_NAME9}: ${messageOf(error)}`);
8105
8838
  }
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);
8112
8839
  let result;
8113
8840
  try {
8114
- result = migrateStore({
8115
- path,
8116
- dryRun,
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 }
8123
- });
8841
+ result = moveToSqlite({ root: project.root });
8124
8842
  } catch (error) {
8125
- if (error instanceof RebuildRefusal || error instanceof DevelopmentBuildRefusedError) {
8126
- throw new CommandExit(REFUSED_EXIT2, `\u274C ${COMMAND_NAME7}: ${error.message}`);
8127
- }
8128
- throw error;
8843
+ throw refusalExit2(error) ?? error;
8129
8844
  }
8130
8845
  if (context.outputMode === "json") {
8131
8846
  context.output.result(result);
8132
8847
  return;
8133
8848
  }
8134
- for (const line of renderMigrate(result))
8849
+ for (const line of renderMove(result))
8135
8850
  context.output.info(line);
8136
8851
  }
8137
- function createMigrateCommand(seams = {}) {
8852
+ function createMoveCommand() {
8138
8853
  const command = {
8139
- name: "effort migrate",
8854
+ name: "effort move",
8140
8855
  subject: "effort",
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.",
8856
+ action: "move",
8857
+ summary: "move a store: ndjson project's sessions and commits into the SQLite store and switch to it",
8858
+ description: "Appends every session and commit row the NDJSON files under `.rafa/effort/`, or under" + " `RAFA_EFFORT_DIR`, hold to the SQLite store beside them, through the store port, so each row is" + " deduplicated by its key as `rafa effort collect` would. It then checks that the SQLite store holds" + " every key read and that its count is the old count plus the rows added, and only then sets" + " `store: sqlite` in `.rafa/config.yaml`, keeping every other line. The NDJSON files are left where" + " they are, and a move run again adds nothing, so it can be repeated once a loop started before it" + " has ended. Exit code 1, with nothing moved, for a `--to` other than `sqlite`, a config it cannot" + " edit, and a development build over a store it does not own; a failed count check is exit code 1" + " with the config left as it was. With `--output=json` the outcome is the data of the terminal" + " result event. Starts no session.",
8144
8859
  args: [],
8145
8860
  flags: [
8146
8861
  {
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.",
8149
- type: "boolean"
8862
+ name: TO_FLAG2,
8863
+ description: "The store to move to; `sqlite` is the one value it takes, and it is required.",
8864
+ type: "string"
8150
8865
  }
8151
8866
  ],
8152
8867
  examples: [
8153
8868
  {
8154
- cmd: "rafa effort migrate --dry-run",
8155
- note: "Prints what the migration applies and how it checked, changing nothing."
8156
- },
8157
- {
8158
- cmd: "rafa effort migrate",
8159
- note: "Swaps the migrated store in and prints where the original was kept."
8869
+ cmd: MOVE_USAGE,
8870
+ note: "Moves the NDJSON rows into the SQLite store, checks the counts and sets store: sqlite."
8160
8871
  }
8161
8872
  ],
8162
8873
  outputs: ["text", "json"],
8163
8874
  run: async (context) => {
8164
- runMigrate(context, seams);
8875
+ runMove(context);
8165
8876
  await Promise.resolve();
8166
8877
  }
8167
8878
  };
8168
8879
  return Object.freeze(command);
8169
8880
  }
8170
- var migrate_default = createMigrateCommand();
8881
+ var move_default = createMoveCommand();
8171
8882
 
8172
8883
  // src/commands/effort/report.ts
8173
8884
  var report_default = wrapPhaseZeroCommand({
@@ -8223,11 +8934,11 @@ var report_default = wrapPhaseZeroCommand({
8223
8934
  }, report);
8224
8935
 
8225
8936
  // src/commands/effort/schema.ts
8226
- import { join as join21 } from "path";
8227
- var COMMAND_NAME8 = "rafa effort schema";
8937
+ import { join as join23 } from "path";
8938
+ var COMMAND_NAME10 = "rafa effort schema";
8228
8939
  var SCHEMA_USAGE = "rafa effort schema [--check]";
8229
8940
  var CHECK_FLAG = "check";
8230
- var REFUSED_EXIT3 = 1;
8941
+ var REFUSED_EXIT5 = 1;
8231
8942
  var FAILED_EXIT2 = 2;
8232
8943
  function listLine(name2, items) {
8233
8944
  const shown = items.length === 0 ? "none" : items.join(", ");
@@ -8267,29 +8978,29 @@ function renderSchema(report2) {
8267
8978
  `Next safe step: ${report2.nextStep}`
8268
8979
  ];
8269
8980
  }
8270
- function storePath2(context, root) {
8981
+ function storePath3(context, root) {
8271
8982
  try {
8272
- return join21(effortStoreDir(root, context.env), SQLITE_STORE_FILE_NAME);
8983
+ return join23(effortStoreDir(root, context.env), SQLITE_STORE_FILE_NAME);
8273
8984
  } catch (error) {
8274
- throw new CommandExit(REFUSED_EXIT3, `\u274C ${COMMAND_NAME8}: ${messageOf(error)}`);
8985
+ throw new CommandExit(REFUSED_EXIT5, `\u274C ${COMMAND_NAME10}: ${messageOf(error)}`);
8275
8986
  }
8276
8987
  }
8277
8988
  function runSchema(context, seams) {
8278
8989
  expectNoArgument(context.args, SCHEMA_USAGE);
8279
8990
  const check = readSwitch(CHECK_FLAG, context.flags[CHECK_FLAG], `Usage: ${SCHEMA_USAGE}`);
8280
- const path = storePath2(context, requireProject(context, COMMAND_NAME8).root);
8991
+ const path = storePath3(context, requireProject(context, COMMAND_NAME10).root);
8281
8992
  let report2;
8282
8993
  try {
8283
8994
  report2 = readSchemaReport(path, seams);
8284
8995
  } catch (error) {
8285
- throw new CommandExit(FAILED_EXIT2, `\u274C ${COMMAND_NAME8}: ${path} cannot be read as an effort store: ${messageOf(error)}`);
8996
+ throw new CommandExit(FAILED_EXIT2, `\u274C ${COMMAND_NAME10}: ${path} cannot be read as an effort store: ${messageOf(error)}`);
8286
8997
  }
8287
8998
  if (context.outputMode !== "json") {
8288
8999
  for (const line of renderSchema(report2))
8289
9000
  context.output.info(line);
8290
9001
  }
8291
9002
  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}`);
9003
+ throw new CommandExit(REFUSED_EXIT5, `\u274C ${COMMAND_NAME10} --check: this rafa refuses ${path} (${report2.status}). Next safe step: ${report2.nextStep}`);
8293
9004
  }
8294
9005
  if (context.outputMode === "json")
8295
9006
  context.output.result(report2);
@@ -8695,7 +9406,7 @@ async function applyHorizonChange(board, reading, reason) {
8695
9406
  }
8696
9407
 
8697
9408
  // src/commands/epic/horizon-change.ts
8698
- var TO_FLAG2 = "to";
9409
+ var TO_FLAG3 = "to";
8699
9410
  var ISSUE_NUMBER2 = /^[1-9]\d*$/u;
8700
9411
  var NO_ANSWERS = ["n", "no"];
8701
9412
  var DEFER_ACTION = Object.freeze({
@@ -8721,9 +9432,9 @@ function readHorizonLine(context, action) {
8721
9432
  if (!ISSUE_NUMBER2.test(word)) {
8722
9433
  throw lineRefusal(`"${word}" is no epic number, which is a whole number from 1`, action.usage);
8723
9434
  }
8724
- const to = readChoiceFlag(flags, TO_FLAG2, action.targets, action.usage);
9435
+ const to = readChoiceFlag(flags, TO_FLAG3, action.targets, action.usage);
8725
9436
  if (to === undefined)
8726
- throw lineRefusal(`--${TO_FLAG2} is required: --${TO_FLAG2}=<horizon>`, action.usage);
9437
+ throw lineRefusal(`--${TO_FLAG3} is required: --${TO_FLAG3}=<horizon>`, action.usage);
8727
9438
  const reason = readTextFlag(flags, REASON_FLAG, action.usage) ?? null;
8728
9439
  return Object.freeze({ epic: Number(word), to, reason });
8729
9440
  }
@@ -8736,7 +9447,7 @@ function wrongDirection(action, change) {
8736
9447
  return null;
8737
9448
  const other = action.name === "defer" ? "promote" : "defer";
8738
9449
  const verb = later ? "a deferral" : "a promotion";
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`);
9450
+ 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_FLAG3}=${change.to}. Nothing was changed`);
8740
9451
  }
8741
9452
  function workPhrase(work) {
8742
9453
  return [
@@ -8923,9 +9634,9 @@ function readMoveLine(context) {
8923
9634
  throw lineRefusal(`Expected one issue number, got ${got}`, USAGE8);
8924
9635
  }
8925
9636
  const issue = issueNumber(word, "issue number");
8926
- const target = readTextFlag(flags, TO_FLAG2, USAGE8);
9637
+ const target = readTextFlag(flags, TO_FLAG3, USAGE8);
8927
9638
  if (target === undefined)
8928
- throw lineRefusal(`--${TO_FLAG2} is required: --${TO_FLAG2}=<epic>`, USAGE8);
9639
+ throw lineRefusal(`--${TO_FLAG3} is required: --${TO_FLAG3}=<epic>`, USAGE8);
8929
9640
  const to = issueNumber(target, "epic number");
8930
9641
  const reason = readTextFlag(flags, REASON_FLAG, USAGE8) ?? null;
8931
9642
  return Object.freeze({ issue, to, reason });
@@ -9184,7 +9895,7 @@ function createEpicMoveCommand(seams = {}) {
9184
9895
  ],
9185
9896
  flags: [
9186
9897
  {
9187
- name: TO_FLAG2,
9898
+ name: TO_FLAG3,
9188
9899
  description: "The number of the open epic it moves to. Required.",
9189
9900
  type: "string",
9190
9901
  required: true
@@ -9206,7 +9917,7 @@ function createEpicMoveCommand(seams = {}) {
9206
9917
  };
9207
9918
  return Object.freeze(command);
9208
9919
  }
9209
- var move_default = createEpicMoveCommand();
9920
+ var move_default2 = createEpicMoveCommand();
9210
9921
 
9211
9922
  // src/commands/epic/cancel.ts
9212
9923
  var EPIC_CANCEL_REFUSAL_EXIT = 2;
@@ -9612,28 +10323,28 @@ function createEpicCancelCommand(seams = {}) {
9612
10323
  var cancel_default = createEpicCancelCommand();
9613
10324
 
9614
10325
  // src/commands/epic/close.ts
9615
- import { randomUUID as randomUUID3 } from "crypto";
9616
- import { join as join25 } from "path";
10326
+ import { randomUUID as randomUUID4 } from "crypto";
10327
+ import { join as join27 } from "path";
9617
10328
 
9618
10329
  // src/board/epic-template.ts
9619
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
9620
- import { dirname as dirname3, join as join22 } from "path";
10330
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
10331
+ import { dirname as dirname4, join as join24 } from "path";
9621
10332
  import { fileURLToPath as fileURLToPath2 } from "url";
9622
- var MODULE_DIR2 = dirname3(fileURLToPath2(import.meta.url));
10333
+ var MODULE_DIR2 = dirname4(fileURLToPath2(import.meta.url));
9623
10334
  var EPIC_TEMPLATE_FILE = "epic.md";
9624
10335
  var CRITERIA_PLACEHOLDERS = Object.freeze([
9625
10336
  "- Placeholder: one line per thing that makes this epic finished," + " written so a check against main can tell whether it holds."
9626
10337
  ]);
9627
10338
  var PLACEHOLDER_REASON = "the line is still the epic template's placeholder, so there is nothing to check";
9628
10339
  function epicTemplateSource(moduleDir = MODULE_DIR2) {
9629
- return join22(moduleDir, TEMPLATES_DIRNAME, EPIC_TEMPLATE_FILE);
10340
+ return join24(moduleDir, TEMPLATES_DIRNAME, EPIC_TEMPLATE_FILE);
9630
10341
  }
9631
10342
  function readEpicTemplate(moduleDir = MODULE_DIR2) {
9632
10343
  const source = epicTemplateSource(moduleDir);
9633
- if (!existsSync9(source)) {
10344
+ if (!existsSync10(source)) {
9634
10345
  throw new Error(`epic template: the epic issue template is missing: no file at ${source}`);
9635
10346
  }
9636
- return readFileSync8(source, "utf8");
10347
+ return readFileSync9(source, "utf8");
9637
10348
  }
9638
10349
  function placeholderCriteria(criteria) {
9639
10350
  if (criteria === null)
@@ -9724,10 +10435,10 @@ function renderEpicCost(cost, estimate) {
9724
10435
  }
9725
10436
 
9726
10437
  // src/epic/verify-plan.ts
9727
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
9728
- import { dirname as dirname4, join as join23 } from "path";
10438
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
10439
+ import { dirname as dirname5, join as join25 } from "path";
9729
10440
  import { fileURLToPath as fileURLToPath3 } from "url";
9730
- var MODULE_DIR3 = dirname4(fileURLToPath3(import.meta.url));
10441
+ var MODULE_DIR3 = dirname5(fileURLToPath3(import.meta.url));
9731
10442
  var VERIFY_PROMPT_FILE = "epic-verify-prompt.md";
9732
10443
  var VERIFY_BLOCK_FENCE = "rafa:verify";
9733
10444
  var VERIFY_PROMPT_SLOTS = ["EPIC_NUMBER", "EPIC_TITLE", "CRITERIA"];
@@ -9782,15 +10493,15 @@ function criteriaToAsk(criteria) {
9782
10493
  return Object.freeze(criteria.filter((criterion) => !criterion.placeholder));
9783
10494
  }
9784
10495
  function verifyPromptCandidates(moduleDir = MODULE_DIR3) {
9785
- return [join23(moduleDir, VERIFY_PROMPT_FILE), join23(dirname4(moduleDir), VERIFY_PROMPT_FILE)];
10496
+ return [join25(moduleDir, VERIFY_PROMPT_FILE), join25(dirname5(moduleDir), VERIFY_PROMPT_FILE)];
9786
10497
  }
9787
10498
  function readVerifyPrompt(moduleDir = MODULE_DIR3) {
9788
10499
  const candidates = verifyPromptCandidates(moduleDir);
9789
- const found = candidates.find((candidate) => existsSync10(candidate));
10500
+ const found = candidates.find((candidate) => existsSync11(candidate));
9790
10501
  if (found === undefined) {
9791
10502
  throw new Error(`epic verify prompt: the verification prompt is missing: no file at ${candidates.join(" or ")}`);
9792
10503
  }
9793
- return readFileSync9(found, "utf8");
10504
+ return readFileSync10(found, "utf8");
9794
10505
  }
9795
10506
  function fenceFor2(text) {
9796
10507
  const longestRun = Math.max(0, ...(text.match(/`+/gu) ?? []).map((run) => run.length));
@@ -9926,8 +10637,8 @@ function parseVerifyPlan(output, criteria) {
9926
10637
  }
9927
10638
 
9928
10639
  // src/epic/verify-run.ts
9929
- import { randomUUID as randomUUID2 } from "crypto";
9930
- import { join as join24 } from "path";
10640
+ import { randomUUID as randomUUID3 } from "crypto";
10641
+ import { join as join26 } from "path";
9931
10642
  var CHECK_TOOLS = ["Read", "Grep", "Glob", "Bash"];
9932
10643
  var DEFAULT_VERIFY_REMOTE = "origin";
9933
10644
  var CHECK_PROMPT_PREFIX = "# Epic acceptance check instructions";
@@ -9936,7 +10647,7 @@ var CHECK_RESULTS = ["pass", "fail"];
9936
10647
  var WORKTREE_PREFIX2 = "epic-";
9937
10648
  var VERDICT_KIND = VERDICT_BLOCK_FENCE.slice("rafa:".length);
9938
10649
  function verifyWorktreePath(home, epic) {
9939
- return join24(home, WORKTREES_SUBDIR, `${WORKTREE_PREFIX2}${String(epic)}`);
10650
+ return join26(home, WORKTREES_SUBDIR, `${WORKTREE_PREFIX2}${String(epic)}`);
9940
10651
  }
9941
10652
  function fetchStep(remote) {
9942
10653
  return { id: "fetch", args: ["fetch", remote] };
@@ -10073,7 +10784,7 @@ function readCheck(check, sessionId, session) {
10073
10784
  }
10074
10785
  async function runChecks(options, worktree, prompt) {
10075
10786
  const spawn = options.spawn ?? spawnClaudeCaptured;
10076
- const pickId = options.sessionId ?? randomUUID2;
10787
+ const pickId = options.sessionId ?? randomUUID3;
10077
10788
  const results = [];
10078
10789
  for (const check of options.checks) {
10079
10790
  const sessionId = pickId();
@@ -10196,7 +10907,7 @@ async function planVerification(input) {
10196
10907
  return Object.freeze({ status: "planned", sessionId, verdicts: reading.verdicts, issues: reading.issues });
10197
10908
  }
10198
10909
  function closeTriageFile(root, epic) {
10199
- return join25(root, ".rafa", `epic-${String(epic)}-close`);
10910
+ return join27(root, ".rafa", `epic-${String(epic)}-close`);
10200
10911
  }
10201
10912
  function failedCheckArtifact(epic, criterion) {
10202
10913
  return `epic ${ref5(epic)} acceptance criterion: ${oneLine(criterion.text)}`;
@@ -10365,7 +11076,7 @@ async function closeEpic(context, seams) {
10365
11076
  config,
10366
11077
  seams,
10367
11078
  spawn: seams.spawn ?? spawnClaudeCaptured,
10368
- sessionId: seams.sessionId ?? randomUUID3
11079
+ sessionId: seams.sessionId ?? randomUUID4
10369
11080
  };
10370
11081
  const gh = seams.gh ?? createGhRunner({ cwd: project.root });
10371
11082
  let listing;
@@ -10472,7 +11183,7 @@ function createEpicDeferCommand(seams = {}) {
10472
11183
  ],
10473
11184
  flags: [
10474
11185
  {
10475
- name: TO_FLAG2,
11186
+ name: TO_FLAG3,
10476
11187
  description: `The horizon it moves to: one of ${DEFER_ACTION.targets.join(", ")}. Required.`,
10477
11188
  type: "string",
10478
11189
  required: true
@@ -10708,7 +11419,7 @@ function createEpicPromoteCommand(seams = {}) {
10708
11419
  ],
10709
11420
  flags: [
10710
11421
  {
10711
- name: TO_FLAG2,
11422
+ name: TO_FLAG3,
10712
11423
  description: `The horizon it moves to: one of ${PROMOTE_ACTION.targets.join(", ")}. Required.`,
10713
11424
  type: "string",
10714
11425
  required: true
@@ -10733,13 +11444,13 @@ function createEpicPromoteCommand(seams = {}) {
10733
11444
  var promote_default = createEpicPromoteCommand();
10734
11445
 
10735
11446
  // src/commands/init.ts
10736
- import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
11447
+ import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
10737
11448
  import { homedir } from "os";
10738
- import { join as join30, relative as relative6, sep as sep4 } from "path";
11449
+ import { join as join32, relative as relative6, sep as sep4 } from "path";
10739
11450
 
10740
11451
  // src/agents/vendorable.ts
10741
- import { readdirSync as readdirSync2, readFileSync as readFileSync10 } from "fs";
10742
- import { isAbsolute as isAbsolute5, join as join26 } from "path";
11452
+ import { readdirSync as readdirSync2, readFileSync as readFileSync11 } from "fs";
11453
+ import { isAbsolute as isAbsolute5, join as join28 } from "path";
10743
11454
  function planFiles(dir) {
10744
11455
  let entries;
10745
11456
  try {
@@ -10751,7 +11462,7 @@ function planFiles(dir) {
10751
11462
  }
10752
11463
  function readText2(path) {
10753
11464
  try {
10754
- return readFileSync10(path, "utf8");
11465
+ return readFileSync11(path, "utf8");
10755
11466
  } catch {
10756
11467
  return null;
10757
11468
  }
@@ -10761,7 +11472,7 @@ function rafaHolds(roster, name2) {
10761
11472
  return item?.holders.some((holder) => holder.source === "rafa") === true;
10762
11473
  }
10763
11474
  function vendorableAgents(scan) {
10764
- const dir = isAbsolute5(scan.planDir) ? scan.planDir : join26(scan.repoRoot, scan.planDir);
11475
+ const dir = isAbsolute5(scan.planDir) ? scan.planDir : join28(scan.repoRoot, scan.planDir);
10765
11476
  const roots = {
10766
11477
  repoRoot: scan.repoRoot,
10767
11478
  home: scan.home,
@@ -10770,7 +11481,7 @@ function vendorableAgents(scan) {
10770
11481
  const roster = resolveAgentRoster(roots, scan.settings);
10771
11482
  const found = [];
10772
11483
  for (const file of planFiles(dir)) {
10773
- const path = join26(dir, file);
11484
+ const path = join28(dir, file);
10774
11485
  const markdown = readText2(path);
10775
11486
  if (markdown === null)
10776
11487
  continue;
@@ -10796,13 +11507,13 @@ function vendorableAgentWarnings(agents, root) {
10796
11507
 
10797
11508
  // src/project/gitignore.ts
10798
11509
  import { createHash } from "crypto";
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";
11510
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "fs";
11511
+ import { dirname as dirname6, isAbsolute as isAbsolute6, join as join29 } from "path";
10801
11512
  var GITIGNORE_FILE = ".gitignore";
10802
11513
  var DIGEST_NAME = "tracking.digest";
10803
11514
  var PRIVATE_TRIAGE = "triage/private";
10804
11515
  var CACHE_DIR = "cache";
10805
- var TRACKING_DIGEST_FILE = join27(SCOPE_DIR, DIGEST_NAME);
11516
+ var TRACKING_DIGEST_FILE = join29(SCOPE_DIR, DIGEST_NAME);
10806
11517
  var BLOCK_BEGIN = "# >>> rafa tracking: rafa rewrites the lines up to the end marker";
10807
11518
  var BLOCK_END = "# <<< rafa tracking";
10808
11519
  var SUB_PATHS = [
@@ -10823,6 +11534,7 @@ function trackingEntry(flags) {
10823
11534
  `${SCOPE_DIR}/${PRIVATE_TRIAGE}/`,
10824
11535
  `${SCOPE_DIR}/${DIGEST_NAME}`,
10825
11536
  `${SCOPE_DIR}/${POSITION_FILE}`,
11537
+ `${SCOPE_DIR}/${HOP_FILE}`,
10826
11538
  `${SCOPE_DIR}/${CACHE_DIR}/`
10827
11539
  ];
10828
11540
  }
@@ -10890,19 +11602,19 @@ function attempt(file, failure3, act) {
10890
11602
  }
10891
11603
  }
10892
11604
  function readIfPresent2(file) {
10893
- if (!existsSync11(file))
11605
+ if (!existsSync12(file))
10894
11606
  return null;
10895
- return attempt(file, "cannot be read", () => readFileSync11(file, "utf8"));
11607
+ return attempt(file, "cannot be read", () => readFileSync12(file, "utf8"));
10896
11608
  }
10897
11609
  function writeTrackingGitignore(root, flags) {
10898
11610
  requireAbsolute(root);
10899
- const file = join27(root, GITIGNORE_FILE);
11611
+ const file = join29(root, GITIGNORE_FILE);
10900
11612
  const text = readIfPresent2(file);
10901
11613
  const next = withTrackingBlock(text, flags, file);
10902
11614
  if (next === text)
10903
11615
  return { file, change: "unchanged" };
10904
11616
  attempt(file, "cannot be written", () => {
10905
- writeFileSync2(file, next);
11617
+ writeFileSync3(file, next);
10906
11618
  });
10907
11619
  const change = text === null ? "created" : "updated";
10908
11620
  return { file, change };
@@ -10918,7 +11630,7 @@ function trackingDigest(flags) {
10918
11630
  }
10919
11631
  function exposureNotice(root) {
10920
11632
  return [
10921
- `tracking.all is true, so ${join27(root, GITIGNORE_FILE)} no longer ignores ${SCOPE_DIR}/` + " and a commit can carry what it holds, including:",
11633
+ `tracking.all is true, so ${join29(root, GITIGNORE_FILE)} no longer ignores ${SCOPE_DIR}/` + " and a commit can carry what it holds, including:",
10922
11634
  " - unpatched issues described in specs and plans",
10923
11635
  " - effort rows with model names and token counts",
10924
11636
  " - instincts that may quote error output",
@@ -10931,7 +11643,7 @@ function printWarning(message) {
10931
11643
  }
10932
11644
  function noticeTrackingChange(root, flags, print2 = printWarning) {
10933
11645
  requireAbsolute(root);
10934
- const digestFile = join27(root, TRACKING_DIGEST_FILE);
11646
+ const digestFile = join29(root, TRACKING_DIGEST_FILE);
10935
11647
  const recorded = `${trackingDigest(flags)}
10936
11648
  `;
10937
11649
  if (readIfPresent2(digestFile) === recorded) {
@@ -10940,8 +11652,8 @@ function noticeTrackingChange(root, flags, print2 = printWarning) {
10940
11652
  if (flags.trackingAll)
10941
11653
  print2(exposureNotice(root));
10942
11654
  attempt(digestFile, "cannot be written", () => {
10943
- mkdirSync4(dirname5(digestFile), { recursive: true });
10944
- writeFileSync2(digestFile, recorded);
11655
+ mkdirSync4(dirname6(digestFile), { recursive: true });
11656
+ writeFileSync3(digestFile, recorded);
10945
11657
  });
10946
11658
  return { digestFile, changed: true, printed: flags.trackingAll };
10947
11659
  }
@@ -10951,12 +11663,12 @@ function applyTracking(root, flags, print2 = printWarning) {
10951
11663
  }
10952
11664
 
10953
11665
  // src/project/root-choice.ts
10954
- import { resolve as resolve9 } from "path";
11666
+ import { resolve as resolve10 } from "path";
10955
11667
 
10956
11668
  // src/project/roots.ts
10957
11669
  import { spawnSync } from "child_process";
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";
11670
+ import { accessSync, constants as constants2, readFileSync as readFileSync13, statSync as statSync8 } from "fs";
11671
+ import { isAbsolute as isAbsolute7, join as join30, resolve as resolve9 } from "path";
10960
11672
  var REFUSED_ROOTS = Object.freeze([
10961
11673
  { path: "/", why: "is the filesystem root" },
10962
11674
  { path: "/var", why: "holds system state" },
@@ -10976,7 +11688,7 @@ var PACKAGE_JSON = "package.json";
10976
11688
  var NOT_A_REPOSITORY = "fatal: not a git repository (or any ";
10977
11689
  var DISK_ROOTS_FILE_SYSTEM = Object.freeze({
10978
11690
  ...DISK_FILE_SYSTEM,
10979
- readFile: (path) => readFileSync12(path, "utf8"),
11691
+ readFile: (path) => readFileSync13(path, "utf8"),
10980
11692
  isDirectory: (path) => statSync8(path).isDirectory(),
10981
11693
  assertWritable: (path) => accessSync(path, constants2.W_OK)
10982
11694
  });
@@ -11019,7 +11731,7 @@ function realOrSpelled(path, fs) {
11019
11731
  try {
11020
11732
  return fs.realpath(path);
11021
11733
  } catch {
11022
- return resolve8(path);
11734
+ return resolve9(path);
11023
11735
  }
11024
11736
  }
11025
11737
  function namedRoots(home, fs) {
@@ -11061,7 +11773,7 @@ function writeRefusal(given, real, fs) {
11061
11773
  }
11062
11774
  function refusalOf(given, named2, fs) {
11063
11775
  const resolved = resolveReal(given, fs);
11064
- const byName2 = namedRefusal(given, resolved.real ?? resolve8(given), named2);
11776
+ const byName2 = namedRefusal(given, resolved.real ?? resolve9(given), named2);
11065
11777
  if (byName2 !== null)
11066
11778
  return byName2;
11067
11779
  if (resolved.real === null) {
@@ -11089,8 +11801,8 @@ function readManifest2(file, fs) {
11089
11801
  }
11090
11802
  }
11091
11803
  function readDir(dir, fs) {
11092
- const manifest = readManifest2(join28(dir, PACKAGE_JSON), fs);
11093
- const files = MONOREPO_MARKER_FILES.filter((name2) => fs.exists(join28(dir, name2)));
11804
+ const manifest = readManifest2(join30(dir, PACKAGE_JSON), fs);
11805
+ const files = MONOREPO_MARKER_FILES.filter((name2) => fs.exists(join30(dir, name2)));
11094
11806
  const markers = manifest.marks ? [PACKAGE_JSON, ...files] : files;
11095
11807
  return { ...manifest, dir, markers };
11096
11808
  }
@@ -11160,7 +11872,7 @@ function firstCandidate(found) {
11160
11872
  return first.refusal === null ? chosen(first.path, first.source) : problem(first.refusal.reason);
11161
11873
  }
11162
11874
  function namedRoot(typed, start2, source, seams) {
11163
- const path = resolve9(start2, typed);
11875
+ const path = resolve10(start2, typed);
11164
11876
  const refusal9 = rootRefusal(path, seams);
11165
11877
  if (refusal9 !== null)
11166
11878
  return problem(refusal9.reason);
@@ -11201,8 +11913,8 @@ async function promptForRoot(found, prompter, seams) {
11201
11913
  }
11202
11914
 
11203
11915
  // src/project/scaffold.ts
11204
- import { existsSync as existsSync12, lstatSync, mkdirSync as mkdirSync5, statSync as statSync9, writeFileSync as writeFileSync3 } from "fs";
11205
- import { join as join29 } from "path";
11916
+ import { existsSync as existsSync13, lstatSync, mkdirSync as mkdirSync5, statSync as statSync9, writeFileSync as writeFileSync4 } from "fs";
11917
+ import { join as join31 } from "path";
11206
11918
  var PROJECT_TREE = Object.freeze(["specs", "plans", "runs", "effort", "instincts"]);
11207
11919
  var USER_TREE = Object.freeze(["instincts"]);
11208
11920
  var CONFIG_SETTINGS_LINES = Object.freeze([
@@ -11305,7 +12017,7 @@ class ScaffoldError extends Error {
11305
12017
  }
11306
12018
  function layoutOf2(base, tree) {
11307
12019
  const scope = scopeAt(base);
11308
- return { dirs: [scope.dir, ...tree.map((name2) => join29(scope.dir, name2))], configFile: configFilePath(base) };
12020
+ return { dirs: [scope.dir, ...tree.map((name2) => join31(scope.dir, name2))], configFile: configFilePath(base) };
11309
12021
  }
11310
12022
  function occupied(path) {
11311
12023
  return lstatSync(path, { throwIfNoEntry: false }) !== undefined;
@@ -11316,7 +12028,7 @@ function isDirectory2(path) {
11316
12028
  function directoryConflict(dir) {
11317
12029
  if (!occupied(dir) || isDirectory2(dir))
11318
12030
  return null;
11319
- return existsSync12(dir) ? `${dir} is not a directory` : `${dir} is a link to nothing`;
12031
+ return existsSync13(dir) ? `${dir} is not a directory` : `${dir} is a link to nothing`;
11320
12032
  }
11321
12033
  function layoutConflicts(layout) {
11322
12034
  const [scopeDir, ...tree] = layout.dirs;
@@ -11324,7 +12036,7 @@ function layoutConflicts(layout) {
11324
12036
  if (opening !== null)
11325
12037
  return [opening];
11326
12038
  const dirs = tree.flatMap((dir) => directoryConflict(dir) ?? []);
11327
- const file = occupied(layout.configFile) && !existsSync12(layout.configFile) ? [`${layout.configFile} is a link to nothing`] : [];
12039
+ const file = occupied(layout.configFile) && !existsSync13(layout.configFile) ? [`${layout.configFile} is a link to nothing`] : [];
11328
12040
  return [...dirs, ...file];
11329
12041
  }
11330
12042
  function scaffoldConflicts(root, home) {
@@ -11338,7 +12050,7 @@ function attempt2(path, failure3, act) {
11338
12050
  }
11339
12051
  }
11340
12052
  function ensureDirectory(path) {
11341
- if (existsSync12(path))
12053
+ if (existsSync13(path))
11342
12054
  return { path, kind: "directory", change: "unchanged" };
11343
12055
  attempt2(path, "cannot be created", () => {
11344
12056
  mkdirSync5(path, { recursive: true });
@@ -11346,10 +12058,10 @@ function ensureDirectory(path) {
11346
12058
  return { path, kind: "directory", change: "created" };
11347
12059
  }
11348
12060
  function ensureFile(path, text) {
11349
- if (existsSync12(path))
12061
+ if (existsSync13(path))
11350
12062
  return { path, kind: "file", change: "unchanged" };
11351
12063
  attempt2(path, "cannot be written", () => {
11352
- writeFileSync3(path, text, { encoding: "utf8", flag: "wx" });
12064
+ writeFileSync4(path, text, { encoding: "utf8", flag: "wx" });
11353
12065
  });
11354
12066
  return { path, kind: "file", change: "created" };
11355
12067
  }
@@ -11366,10 +12078,10 @@ function writeUserScope(home) {
11366
12078
  }
11367
12079
 
11368
12080
  // src/commands/init-release.ts
11369
- import { writeFileSync as writeFileSync4 } from "fs";
12081
+ import { writeFileSync as writeFileSync5 } from "fs";
11370
12082
 
11371
12083
  // src/release/setting.ts
11372
- import { readFileSync as readFileSync13 } from "fs";
12084
+ import { readFileSync as readFileSync14 } from "fs";
11373
12085
  var RELEASE_ENABLED_SETTING = "release.enabled";
11374
12086
  function releaseEnabledIn(text, path) {
11375
12087
  return parseConfigText(text, path).values.releaseEnabled ?? null;
@@ -11378,7 +12090,7 @@ function readReleaseSetting(root) {
11378
12090
  const path = configFilePath(root);
11379
12091
  let text;
11380
12092
  try {
11381
- text = readFileSync13(path, "utf8");
12093
+ text = readFileSync14(path, "utf8");
11382
12094
  } catch (error) {
11383
12095
  return { enabled: null, text: "", problem: `${path} could not be read: ${messageOf(error)}` };
11384
12096
  }
@@ -11499,7 +12211,7 @@ function writeSetting(root, text, enabled) {
11499
12211
  return `the file this run would have written reads ${RELEASE_ENABLED_SETTING} as ${describeValue(read)}`;
11500
12212
  }
11501
12213
  try {
11502
- writeFileSync4(path, written, "utf8");
12214
+ writeFileSync5(path, written, "utf8");
11503
12215
  } catch (error) {
11504
12216
  return `it could not be written: ${messageOf(error)}`;
11505
12217
  }
@@ -11662,9 +12374,9 @@ function resolvedConfig2(root, home, warn3) {
11662
12374
  }
11663
12375
  }
11664
12376
  function checkGitignore(root, flags) {
11665
- const file = join30(root, GITIGNORE_FILE);
12377
+ const file = join32(root, GITIGNORE_FILE);
11666
12378
  try {
11667
- const text = existsSync13(file) ? readFileSync14(file, "utf8") : null;
12379
+ const text = existsSync14(file) ? readFileSync15(file, "utf8") : null;
11668
12380
  withTrackingBlock(text, flags, file);
11669
12381
  } catch (error) {
11670
12382
  const message = error instanceof GitignoreError ? error.message : `rafa init: ${file} cannot be read (${messageOf(error)})`;
@@ -11695,8 +12407,8 @@ function initialise(root, start2, home, context, entry) {
11695
12407
  checkConflicts(root.path, home);
11696
12408
  const config = resolvedConfig2(root.path, home, warn3);
11697
12409
  checkGitignore(root.path, config);
11698
- const configExisted = existsSync13(configFilePath(root.path));
11699
- const digestExisted = existsSync13(join30(root.path, TRACKING_DIGEST_FILE));
12410
+ const configExisted = existsSync14(configFilePath(root.path));
12411
+ const digestExisted = existsSync14(join32(root.path, TRACKING_DIGEST_FILE));
11700
12412
  const project = writeProjectScope(root.path);
11701
12413
  const tracking = applyTracking(root.path, config, warn3);
11702
12414
  const writes = [...project, ...trackingWrites(tracking, digestExisted), ...writeUserScope(home)];
@@ -11874,7 +12586,7 @@ function createInitCommand(seams = DEFAULT_INIT_SEAMS) {
11874
12586
  var init_default = createInitCommand();
11875
12587
 
11876
12588
  // src/commands/check-report.ts
11877
- import { resolve as resolve10 } from "path";
12589
+ import { resolve as resolve11 } from "path";
11878
12590
  var DEFAULT_CHECK_SEAMS = Object.freeze({
11879
12591
  cwd: () => process.cwd()
11880
12592
  });
@@ -11890,7 +12602,7 @@ Usage: ${usage2}`);
11890
12602
  throw new CommandExit(1, `\u274C Expected one directory, and read "${extra}" as a second
11891
12603
  Usage: ${usage2}`);
11892
12604
  }
11893
- return resolve10(cwd(), dir);
12605
+ return resolve11(cwd(), dir);
11894
12606
  }
11895
12607
  function readFix(value, usage2) {
11896
12608
  if (value === undefined || value === false || value === "false")
@@ -11907,7 +12619,7 @@ function readProjectRoot(value, usage2, cwd) {
11907
12619
  throw new CommandExit(1, `\u274C --project needs a path, and the line gave it none
11908
12620
  Usage: ${usage2}`);
11909
12621
  }
11910
- return resolve10(cwd(), value);
12622
+ return resolve11(cwd(), value);
11911
12623
  }
11912
12624
  function reportMarker(report2) {
11913
12625
  if (report2.failed)
@@ -12543,8 +13255,8 @@ function createInstinctShowCommand() {
12543
13255
  var show_default3 = createInstinctShowCommand();
12544
13256
 
12545
13257
  // src/commands/issue/check.ts
12546
- import { readdirSync as readdirSync3, readFileSync as readFileSync15, statSync as statSync10 } from "fs";
12547
- import { join as join31, resolve as resolve11 } from "path";
13258
+ import { readdirSync as readdirSync3, readFileSync as readFileSync16, statSync as statSync10 } from "fs";
13259
+ import { join as join33, resolve as resolve12 } from "path";
12548
13260
  var CHECK_USAGE = "rafa issue check <n> [--stamp]";
12549
13261
  var STAMP_FLAG = "stamp";
12550
13262
  var ISSUE_NUMBER6 = /^[1-9]\d*$/u;
@@ -12576,22 +13288,22 @@ function copyNames(dir, issue) {
12576
13288
  }
12577
13289
  const prefix = `${boardId(issue)}-`;
12578
13290
  const notes = notesFileName(issue);
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();
13291
+ 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(join33(dir, name2)).isFile()).sort();
12580
13292
  }
12581
13293
  function findSavedCopy(root, specsDir, issue) {
12582
- const names = copyNames(resolve11(root, specsDir), issue);
13294
+ const names = copyNames(resolve12(root, specsDir), issue);
12583
13295
  const [only] = names;
12584
13296
  if (only === undefined)
12585
13297
  throw new CommandExit(1, missingCopyMessage(issue, specsDir));
12586
13298
  if (names.length > 1) {
12587
13299
  throw new CommandExit(1, [
12588
13300
  `\u274C Issue #${String(issue)} has ${String(names.length)} saved copies under ${specsDir}; remove the stale one:`,
12589
- ...names.map((name2) => ` ${join31(specsDir, name2)}`)
13301
+ ...names.map((name2) => ` ${join33(specsDir, name2)}`)
12590
13302
  ].join(`
12591
13303
  `));
12592
13304
  }
12593
- const path = join31(specsDir, only);
12594
- return { path, absolute: resolve11(root, path) };
13305
+ const path = join33(specsDir, only);
13306
+ return { path, absolute: resolve12(root, path) };
12595
13307
  }
12596
13308
  function liveText(live) {
12597
13309
  return live.kind === "unreadable" ? "unreadable" : fingerprintText(live);
@@ -12639,7 +13351,7 @@ async function readReferences(absolute, issue, verify, stamp) {
12639
13351
  return rows.map((row2) => checkedRef(row2, row2.stamp));
12640
13352
  }
12641
13353
  const once2 = memoiseVerifier(verify);
12642
- const before = await readRefsText({ copy: readFileSync15(absolute, "utf8"), issue, verify: once2 });
13354
+ const before = await readRefsText({ copy: readFileSync16(absolute, "utf8"), issue, verify: once2 });
12643
13355
  const after = await restampCopyRefs({ path: absolute, issue, verify: once2 });
12644
13356
  return before.rows.map((row2, index) => checkedRef(row2, after.rows[index]?.stamp ?? null));
12645
13357
  }
@@ -12920,7 +13632,7 @@ function createIssueMoveCommand(seams = DEFAULT_ISSUE_SEAMS) {
12920
13632
  };
12921
13633
  return Object.freeze(command);
12922
13634
  }
12923
- var move_default2 = createIssueMoveCommand();
13635
+ var move_default3 = createIssueMoveCommand();
12924
13636
 
12925
13637
  // src/commands/issue/show.ts
12926
13638
  var USAGE19 = "rafa issue show <id>";
@@ -13494,7 +14206,7 @@ var wrapped = wrapPhaseZeroCommand({
13494
14206
  subject: "loop",
13495
14207
  action: "start",
13496
14208
  summary: "run a plan task by task, then wrap up, open the PR and wait for CI",
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.",
14209
+ 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, and under `--roadmap`" + " the hop away, when one is. 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.",
13498
14210
  args: [],
13499
14211
  flags: [
13500
14212
  {
@@ -13550,6 +14262,11 @@ var wrapped = wrapPhaseZeroCommand({
13550
14262
  description: "Runs on `main` or `master`, which the loop otherwise refuses.",
13551
14263
  type: "boolean"
13552
14264
  },
14265
+ {
14266
+ name: "roadmap",
14267
+ description: "Stamps the hop `rafa next --roadmap` is away on, when one is, on the session record as" + " its `hop`: the hop record `.rafa/hop.json` holds, still `away` and its home still the" + " position's. What `rafa next --roadmap` passes to the loop it starts.",
14268
+ type: "boolean"
14269
+ },
13553
14270
  {
13554
14271
  name: "detached",
13555
14272
  description: "Runs the loop in the background. Refused until phase 6, before anything is read:" + " until then a run holds the terminal it starts in.",
@@ -13904,15 +14621,18 @@ function isAlwaysAsked(action) {
13904
14621
  return ALWAYS_ASKED.has(action);
13905
14622
  }
13906
14623
  var CEILING_REFUSAL_EXIT = 2;
14624
+ var ROADMAP_ACTIONS = new Set(["hop", "home"]);
13907
14625
  var YES_ACTIONS = Object.freeze([
13908
14626
  "sync",
13909
- ...NEXT_COMMAND_ACTIONS.filter((action) => !ALWAYS_ASKED.has(action))
14627
+ ...NEXT_COMMAND_ACTIONS.filter((action) => !ALWAYS_ASKED.has(action)),
14628
+ ...ROADMAP_ACTIONS
13910
14629
  ]);
13911
14630
  var BARE_YES_ACTIONS = Object.freeze([
13912
14631
  "sync",
13913
14632
  "wait",
13914
14633
  "unblock",
13915
- "plan"
14634
+ "plan",
14635
+ "home"
13916
14636
  ]);
13917
14637
  function ceilingRefusal(problem2, usage2) {
13918
14638
  return new CommandExit(CEILING_REFUSAL_EXIT, `\u274C ${problem2}
@@ -13941,6 +14661,85 @@ function allowedUnasked(action, ceiling) {
13941
14661
  return ceiling.includes(action);
13942
14662
  }
13943
14663
 
14664
+ // src/next/lines.ts
14665
+ var NEXT_USAGE = "rafa next [--dry-run] [--roadmap] [--yes[=<action ids>]]";
14666
+ var DRY_RUN_FLAG5 = "dry-run";
14667
+ var ROADMAP_FLAG = "roadmap";
14668
+ var STATE_MARK = "\uD83D\uDCCD";
14669
+ var PROPOSAL_MARK = "\uD83D\uDC49";
14670
+ var STOP_MARK = "\u23F9";
14671
+ var MAX_ACTIONS = 12;
14672
+ function stateLine(state) {
14673
+ return `${STATE_MARK} ${state.reading}.`;
14674
+ }
14675
+ function proposalLine(state, invocation) {
14676
+ const runs = invocation === null ? "" : ` \u2014 rafa ${commandWords(invocation)}`;
14677
+ return `${PROPOSAL_MARK} ${state.proposal}${runs}`;
14678
+ }
14679
+ function shownIds(ids, roadmap) {
14680
+ return roadmap ? ids : ids.filter((action) => !ROADMAP_ACTIONS.has(action));
14681
+ }
14682
+ function namedIds(ceiling, roadmap) {
14683
+ const shown = shownIds(ceiling, roadmap);
14684
+ return shown.length === 0 ? "no action" : shown.join(", ");
14685
+ }
14686
+ function dryRunLine(dryRun, roadmap) {
14687
+ if (dryRun === "no-terminal") {
14688
+ return `${STOP_MARK} There is no terminal to answer on, so nothing ran; run rafa next where you can answer,` + ` or type --${YES_FLAG}=${shownIds(BARE_YES_ACTIONS, roadmap).join(",")} to allow those steps unasked.`;
14689
+ }
14690
+ return `${STOP_MARK} --${DRY_RUN_FLAG5}: nothing ran.`;
14691
+ }
14692
+ function stopLine(stop, state, ceiling, dryRun, roadmap = false) {
14693
+ if (stop === "dry-run")
14694
+ return dryRunLine(dryRun, roadmap);
14695
+ if (stop === "declined")
14696
+ return `${STOP_MARK} Nothing ran.`;
14697
+ if (stop === "unasked") {
14698
+ const allows = `${STOP_MARK} --${YES_FLAG} allows ${namedIds(ceiling ?? [], roadmap)}, and this step is ${state.action},` + " so nothing ran;";
14699
+ if (ALWAYS_ASKED.has(state.action)) {
14700
+ return `${allows} no --${YES_FLAG} list allows it, so drop --${YES_FLAG} to be asked.`;
14701
+ }
14702
+ return `${allows} type --${YES_FLAG}=${[...shownIds(ceiling ?? [], roadmap), state.action].join(",")} to allow it,` + ` or drop --${YES_FLAG} to be asked.`;
14703
+ }
14704
+ if (stop === "loop-started") {
14705
+ return `${STOP_MARK} The loop has run; rafa next reads where it left the project.`;
14706
+ }
14707
+ if (stop === "unchanged") {
14708
+ return `${STOP_MARK} That last step left the project where it was, so the chain stops rather than repeating it.`;
14709
+ }
14710
+ if (stop === "capped") {
14711
+ return `${STOP_MARK} ${MAX_ACTIONS} actions have run, which is as many as one chain runs; run rafa next again.`;
14712
+ }
14713
+ return null;
14714
+ }
14715
+ function lineRefusal2(problem2) {
14716
+ return new CommandExit(1, `\u274C ${problem2}
14717
+ Usage: ${NEXT_USAGE}`);
14718
+ }
14719
+ function readDryRun(flags) {
14720
+ return readBareFlag(flags, DRY_RUN_FLAG5);
14721
+ }
14722
+ function readRoadmap(flags) {
14723
+ return readBareFlag(flags, ROADMAP_FLAG);
14724
+ }
14725
+ function readBareFlag(flags, name2) {
14726
+ const value = flags[name2];
14727
+ if (value === undefined)
14728
+ return false;
14729
+ if (typeof value === "boolean")
14730
+ return value;
14731
+ if (value === "true" || value === "false")
14732
+ return value === "true";
14733
+ throw lineRefusal2(`--${name2} takes no value, and read "${value}" as one`);
14734
+ }
14735
+ function dryRunOf(flag, ceiling, hasTerminal) {
14736
+ if (flag)
14737
+ return "flag";
14738
+ if (ceiling === null && !hasTerminal)
14739
+ return "no-terminal";
14740
+ return null;
14741
+ }
14742
+
13944
14743
  // src/next/sync.ts
13945
14744
  var INDENT = " ";
13946
14745
  function refuse(lines2) {
@@ -13995,53 +14794,8 @@ function fastForwardBase(git, plan2) {
13995
14794
  }
13996
14795
 
13997
14796
  // src/commands/next.ts
13998
- var NEXT_USAGE = "rafa next [--dry-run] [--yes[=<action ids>]]";
13999
- var DRY_RUN_FLAG4 = "dry-run";
14000
- var STATE_MARK = "\uD83D\uDCCD";
14001
- var PROPOSAL_MARK = "\uD83D\uDC49";
14002
- var STOP_MARK = "\u23F9";
14003
- var MAX_ACTIONS = 12;
14004
14797
  var LOOP_ACTIONS = Object.freeze(["start", "resume"]);
14005
14798
  var QUESTION_HANDED_OVER = new Set(["merge-unchecked"]);
14006
- function stateLine(state) {
14007
- return `${STATE_MARK} ${state.reading}.`;
14008
- }
14009
- function proposalLine(state, invocation) {
14010
- const runs = invocation === null ? "" : ` \u2014 rafa ${commandWords(invocation)}`;
14011
- return `${PROPOSAL_MARK} ${state.proposal}${runs}`;
14012
- }
14013
- function namedIds(ceiling) {
14014
- return ceiling.length === 0 ? "no action" : ceiling.join(", ");
14015
- }
14016
- function dryRunLine(dryRun) {
14017
- if (dryRun === "no-terminal") {
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.`;
14019
- }
14020
- return `${STOP_MARK} --${DRY_RUN_FLAG4}: nothing ran.`;
14021
- }
14022
- function stopLine(stop, state, ceiling, dryRun) {
14023
- if (stop === "dry-run")
14024
- return dryRunLine(dryRun);
14025
- if (stop === "declined")
14026
- return `${STOP_MARK} Nothing ran.`;
14027
- if (stop === "unasked") {
14028
- const allows = `${STOP_MARK} --${YES_FLAG} allows ${namedIds(ceiling ?? [])}, and this step is ${state.action},` + " so nothing ran;";
14029
- if (ALWAYS_ASKED.has(state.action)) {
14030
- return `${allows} no --${YES_FLAG} list allows it, so drop --${YES_FLAG} to be asked.`;
14031
- }
14032
- return `${allows} type --${YES_FLAG}=${[...ceiling ?? [], state.action].join(",")} to allow it,` + ` or drop --${YES_FLAG} to be asked.`;
14033
- }
14034
- if (stop === "loop-started") {
14035
- return `${STOP_MARK} The loop has run; rafa next reads where it left the project.`;
14036
- }
14037
- if (stop === "unchanged") {
14038
- return `${STOP_MARK} That last step left the project where it was, so the chain stops rather than repeating it.`;
14039
- }
14040
- if (stop === "capped") {
14041
- return `${STOP_MARK} ${MAX_ACTIONS} actions have run, which is as many as one chain runs; run rafa next again.`;
14042
- }
14043
- return null;
14044
- }
14045
14799
  function repeats(previous, state) {
14046
14800
  if (previous === null)
14047
14801
  return false;
@@ -14057,82 +14811,74 @@ function stepOf2(state, invocation, over = {}) {
14057
14811
  ...over
14058
14812
  });
14059
14813
  }
14060
- function ended(steps, step, stop) {
14061
- return Object.freeze({ steps: Object.freeze([...steps, step]), stop });
14814
+ async function playTurn(options, state, previous) {
14815
+ const { run, ask, handOver, ceiling, dryRun, info: info2, warn: warn3, roadmap } = options;
14816
+ for (const problem2 of state.problems)
14817
+ warn3(problem2);
14818
+ const invocation = actionInvocation(state, { roadmap: roadmap !== undefined });
14819
+ info2(stateLine(state));
14820
+ info2(proposalLine(state, invocation));
14821
+ const halt = (stop, over = {}) => ({ stop, step: stepOf2(state, invocation, over) });
14822
+ if (dryRun !== null)
14823
+ return halt("dry-run");
14824
+ if (state.action === "none")
14825
+ return halt("nothing-to-run");
14826
+ if (repeats(previous, state))
14827
+ return halt("unchanged");
14828
+ const unasked = allowedUnasked(state.action, ceiling);
14829
+ if (!unasked && ceiling !== null)
14830
+ return halt("unasked");
14831
+ const handedOver = QUESTION_HANDED_OVER.has(state.action);
14832
+ const asks = !unasked && !handedOver;
14833
+ if (asks && !await ask(nextQuestion(state)))
14834
+ return halt("declined", { asked: true });
14835
+ if (handedOver)
14836
+ handOver?.();
14837
+ await run(state);
14838
+ return { stop: null, step: stepOf2(state, invocation, { asked: asks, ran: true }) };
14062
14839
  }
14063
14840
  async function runNextChain(options) {
14064
- const { read, run, ask, handOver, ceiling, dryRun, info: info2, warn: warn3 } = options;
14841
+ const { read, ceiling, dryRun, info: info2, roadmap } = options;
14065
14842
  let steps = [];
14843
+ let hops = [];
14066
14844
  let previous = null;
14845
+ const report2 = (stop) => Object.freeze({
14846
+ steps: Object.freeze([...steps]),
14847
+ stop,
14848
+ ...roadmap === undefined ? {} : { hops: Object.freeze([...hops]) }
14849
+ });
14850
+ const stopOn = (stop, state) => {
14851
+ const line = stopLine(stop, state, ceiling, dryRun, roadmap !== undefined);
14852
+ if (line !== null)
14853
+ info2(line);
14854
+ return report2(stop);
14855
+ };
14856
+ const turn = async (state, before) => {
14857
+ const end = await playTurn(options, state, before);
14858
+ steps = [...steps, end.step];
14859
+ if (end.stop === null && state.hop !== undefined)
14860
+ hops = [...hops, state.hop];
14861
+ return end.stop;
14862
+ };
14067
14863
  for (;; ) {
14068
14864
  const state = await read();
14069
- for (const problem2 of state.problems)
14070
- warn3(problem2);
14071
- const invocation = actionInvocation(state);
14072
- info2(stateLine(state));
14073
- info2(proposalLine(state, invocation));
14074
- const announce = (stop) => {
14075
- const line = stopLine(stop, state, ceiling, dryRun);
14076
- if (line !== null)
14077
- info2(line);
14078
- };
14079
- const stopHere = (stop, over = {}) => {
14080
- announce(stop);
14081
- return ended(steps, stepOf2(state, invocation, over), stop);
14082
- };
14083
- const stopAfter = (stop) => {
14084
- announce(stop);
14085
- return Object.freeze({ steps, stop });
14086
- };
14087
- if (dryRun !== null)
14088
- return stopHere("dry-run");
14089
- if (state.action === "none")
14090
- return stopHere("nothing-to-run");
14091
- if (repeats(previous, state))
14092
- return stopHere("unchanged");
14093
- const unasked = allowedUnasked(state.action, ceiling);
14094
- if (!unasked && ceiling !== null)
14095
- return stopHere("unasked");
14096
- const handedOver = QUESTION_HANDED_OVER.has(state.action);
14097
- const asks = !unasked && !handedOver;
14098
- if (asks && !await ask(nextQuestion(state)))
14099
- return stopHere("declined", { asked: true });
14100
- if (handedOver)
14101
- handOver?.();
14102
- await run(state);
14103
- steps = [...steps, stepOf2(state, invocation, { asked: asks, ran: true })];
14104
- if (LOOP_ACTIONS.includes(state.action))
14105
- return stopAfter("loop-started");
14865
+ const stop = await turn(state, previous);
14866
+ if (stop !== null)
14867
+ return stopOn(stop, state);
14868
+ if (LOOP_ACTIONS.includes(state.action)) {
14869
+ const home = roadmap === undefined ? null : await roadmap.afterLoop();
14870
+ const homeStop = home === null ? null : await turn(home, null);
14871
+ return homeStop === null ? stopOn("loop-started", state) : stopOn(homeStop, home ?? state);
14872
+ }
14106
14873
  if (steps.length >= MAX_ACTIONS)
14107
- return stopAfter("capped");
14874
+ return stopOn("capped", state);
14108
14875
  previous = state;
14109
14876
  }
14110
14877
  }
14111
- function lineRefusal2(problem2) {
14112
- return new CommandExit(1, `\u274C ${problem2}
14113
- Usage: ${NEXT_USAGE}`);
14114
- }
14115
- function readDryRun(flags) {
14116
- const value = flags[DRY_RUN_FLAG4];
14117
- if (value === undefined)
14118
- return false;
14119
- if (typeof value === "boolean")
14120
- return value;
14121
- if (value === "true" || value === "false")
14122
- return value === "true";
14123
- throw lineRefusal2(`--${DRY_RUN_FLAG4} takes no value, and read "${value}" as one`);
14124
- }
14125
- function dryRunOf(flag, ceiling, hasTerminal) {
14126
- if (flag)
14127
- return "flag";
14128
- if (ceiling === null && !hasTerminal)
14129
- return "no-terminal";
14130
- return null;
14131
- }
14132
14878
  var DEFAULT_NEXT_SEAMS = Object.freeze({});
14133
14879
  async function runStateAction(context, sources, state) {
14134
14880
  if (state.action !== "sync") {
14135
- await runAction({ ...context, output: actionOutput(context.output) }, state);
14881
+ await runAction({ ...context, output: actionOutput(context.output) }, state, { roadmap: sources.roadmap !== undefined });
14136
14882
  return;
14137
14883
  }
14138
14884
  const outcome2 = fastForwardBase(sources.git, { base: sources.base, remote: sources.remote ?? REMOTE });
@@ -14150,13 +14896,22 @@ function endOfEpic(context, sources, report2, epic) {
14150
14896
  for (const line of epicEndLines(epic, release))
14151
14897
  context.output.info(line);
14152
14898
  }
14899
+ async function homeAfterLoop(context, sources) {
14900
+ try {
14901
+ return await readHomeAfterLoop(sources.answer());
14902
+ } catch (error) {
14903
+ context.output.warn(`the hop could not be read after the loop, so rafa next does not go home: ${messageOf(error)}`);
14904
+ return null;
14905
+ }
14906
+ }
14153
14907
  async function runNext(context, seams) {
14154
14908
  expectNoArgument(context.args, NEXT_USAGE);
14155
14909
  const flagged = readDryRun(context.flags);
14910
+ const roadmap = readRoadmap(context.flags);
14156
14911
  const ceiling = readYesCeiling(context.flags, NEXT_USAGE);
14157
14912
  const isTerminal = seams.isTerminal ?? (() => process.stdin.isTTY === true);
14158
14913
  const dryRun = dryRunOf(flagged, ceiling, isTerminal());
14159
- const sources = openNextSources(context, seams);
14914
+ const sources = openNextSources(context, seams, roadmap ? { roadmap } : {});
14160
14915
  const openPrompter = seams.openPrompter ?? (() => createLinePrompter(process.stdin, process.stderr));
14161
14916
  const prompter = lazyPrompter(openPrompter);
14162
14917
  const dry = watchDryEpic();
@@ -14173,7 +14928,8 @@ async function runNext(context, seams) {
14173
14928
  },
14174
14929
  warn: (line) => {
14175
14930
  context.output.warn(line);
14176
- }
14931
+ },
14932
+ ...roadmap ? { roadmap: { afterLoop: () => homeAfterLoop(context, sources) } } : {}
14177
14933
  });
14178
14934
  endOfEpic(context, sources, report2, dry.last());
14179
14935
  return report2;
@@ -14184,10 +14940,15 @@ async function runNext(context, seams) {
14184
14940
  var YES_ID_LIST = YES_ACTIONS.join(", ");
14185
14941
  var NEXT_FLAGS = Object.freeze([
14186
14942
  {
14187
- name: DRY_RUN_FLAG4,
14943
+ name: DRY_RUN_FLAG5,
14188
14944
  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.",
14189
14945
  type: "boolean"
14190
14946
  },
14947
+ {
14948
+ name: ROADMAP_FLAG,
14949
+ description: "Follow one blocker into another epic or board and come home: where the walk stops at an issue" + " blocked by one in another epic, or at an epic with no line left, propose the hop there, pass" + " --roadmap to plan create and loop start, and go home once the loop has run. A blocker that is" + " itself blocked halts with the chain instead, and a pull request whose owner has not approved is" + " not merged.",
14950
+ type: "boolean"
14951
+ },
14191
14952
  {
14192
14953
  name: YES_FLAG,
14193
14954
  description: "Run the actions named without asking, as a comma list of action ids, and stop at the first" + ` action the list leaves out. Bare it names ${BARE_YES_ACTIONS.join(", ")}. The ids are ${YES_ID_LIST};` + ` a list naming ${[...ALWAYS_ASKED].join(" or ")}, which are always asked, or a word that is no id at all` + " is refused.",
@@ -14200,7 +14961,7 @@ function createNextCommand(seams = DEFAULT_NEXT_SEAMS) {
14200
14961
  subject: "next",
14201
14962
  action: "next",
14202
14963
  summary: "say where the project stands, propose the one next step, and run it on a yes",
14203
- description: "Reads the project once \u2014 the running loops, the branch and its base, the plans and their" + " trackers, the open pull request and its checks, and the roadmap \u2014 and prints where it stands on one" + " line and the one thing to do about it on the next. On a yes it runs that action by calling the" + " command that does it, then reads the project again and proposes what follows. It stops when the" + " answer is no, when an action fails, when there is nothing to run, and once a loop has started. A" + " working tree with changes to tracked files and a pull request provider that cannot be asked are" + " reported in place of a proposal. A pull request no check has reported on is proposed as" + " `rafa pr merge <n> --skip-checks`, which asks its own question after the warning it prints, so" + " rafa next asks none before it. `--dry-run` prints the two lines and stops, and so does a run" + " with no terminal to answer on and no `--yes`, since there is nobody to put the question to." + " With `--output=json`" + " the steps and why the chain stopped are the data of the terminal result event.",
14964
+ description: "Reads the project once \u2014 the running loops, the branch and its base, the plans and their" + " trackers, the open pull request and its checks, and the roadmap \u2014 and prints where it stands on one" + " line and the one thing to do about it on the next. On a yes it runs that action by calling the" + " command that does it, then reads the project again and proposes what follows. It stops when the" + " answer is no, when an action fails, when there is nothing to run, and once a loop has started. A" + " working tree with changes to tracked files and a pull request provider that cannot be asked are" + " reported in place of a proposal. A pull request no check has reported on is proposed as" + " `rafa pr merge <n> --skip-checks`, which asks its own question after the warning it prints, so" + " rafa next asks none before it. `--dry-run` prints the two lines and stops, and so does a run" + " with no terminal to answer on and no `--yes`, since there is nobody to put the question to." + " With `--roadmap` it follows one blocker into another epic or board, works it up to its pull" + " request and comes home, halting where that blocker is blocked in turn." + " With `--output=json`" + " the steps and why the chain stopped are the data of the terminal result event, and under" + " `--roadmap` the hops that ran as well.",
14204
14965
  args: [],
14205
14966
  flags: [...NEXT_FLAGS],
14206
14967
  examples: [
@@ -14215,6 +14976,10 @@ function createNextCommand(seams = DEFAULT_NEXT_SEAMS) {
14215
14976
  {
14216
14977
  cmd: "rafa next --yes=sync,plan",
14217
14978
  note: "Fast-forwards the base and creates the next plan unasked, stopping at any other step."
14979
+ },
14980
+ {
14981
+ cmd: "rafa next --roadmap --yes=hop,plan,start,home",
14982
+ note: "Hops to the epic holding the blocker, plans and runs it, and comes home unasked."
14218
14983
  }
14219
14984
  ],
14220
14985
  outputs: ["text", "json"],
@@ -14253,6 +15018,11 @@ var wrapped2 = wrapPhaseZeroCommand({
14253
15018
  description: "Plans the first line of the roadmap issue that is neither done nor taken, then as" + " `--issue`. The roadmap is `roadmap.issue` in the config, else the pinned issue titled `Roadmap`;" + " `--next=<n>` reads issue `<n>` as the roadmap instead.",
14254
15019
  type: "boolean"
14255
15020
  },
15021
+ {
15022
+ name: "roadmap",
15023
+ description: "With `--next`, plans the target of the `rafa next --roadmap` hop under way, the blocker it" + " went to another epic or board for, through the same checks as any line; with no hop away it picks" + " as `--next` does. Refused with exit 2 without `--next`.",
15024
+ type: "boolean"
15025
+ },
14256
15026
  {
14257
15027
  name: "refresh",
14258
15028
  description: "Rebuilds a snapshot that no longer matches the issue without asking; the old copy moves under" + " `previous/`.",
@@ -14309,6 +15079,10 @@ var wrapped2 = wrapPhaseZeroCommand({
14309
15079
  cmd: "rafa plan create --next --dry-run",
14310
15080
  note: "Prints the first roadmap line that is neither done nor taken, with what it skipped to reach" + " it, and stops without writing a snapshot or starting a session."
14311
15081
  },
15082
+ {
15083
+ cmd: "rafa plan create --next --roadmap",
15084
+ note: "While a rafa next --roadmap hop is away, plans the blocker it went for on that blocker's own" + " board; with none away, plans as rafa plan create --next does."
15085
+ },
14312
15086
  {
14313
15087
  cmd: "rafa plan create --issue=20 --refresh --skip-review --no-comment",
14314
15088
  note: "Takes issue 20 as it reads now over the snapshot already there, plans without the planner" + " judging the spec first, and keeps any gaps off the board."
@@ -14321,8 +15095,8 @@ var wrapped2 = wrapPhaseZeroCommand({
14321
15095
  var create_default2 = endingWith(wrapped2);
14322
15096
 
14323
15097
  // src/commands/plan/risk.ts
14324
- import { readFileSync as readFileSync16 } from "fs";
14325
- import { resolve as resolve12 } from "path";
15098
+ import { readFileSync as readFileSync17 } from "fs";
15099
+ import { resolve as resolve13 } from "path";
14326
15100
  var COMMAND2 = "rafa plan risk";
14327
15101
  var USAGE27 = "rafa plan risk [<plan>] [--strict]";
14328
15102
  var STRICT_HINT = `Type the plan first: ${COMMAND2} <plan> --strict`;
@@ -14335,7 +15109,7 @@ function riskPlanPath(root, planDir, typed) {
14335
15109
  return path;
14336
15110
  if (typed !== null)
14337
15111
  throw new CommandExit(1, `\u274C Plan file not found: ${path}`);
14338
- const lookedFor = [...new Set([resolve12(root, planDir, DEFAULT_PLAN_FILE), resolve12(root, DEFAULT_PLAN_FILE)])];
15112
+ const lookedFor = [...new Set([resolve13(root, planDir, DEFAULT_PLAN_FILE), resolve13(root, DEFAULT_PLAN_FILE)])];
14339
15113
  throw new CommandExit(1, `\u274C No plan named, and no default plan at ${lookedFor.join(" or ")}
14340
15114
  Usage: ${USAGE27}`);
14341
15115
  }
@@ -14349,7 +15123,7 @@ function strictRefusal(report2) {
14349
15123
  }
14350
15124
  async function readRisk(path, project, config, context, seams) {
14351
15125
  const makeRunners = seams.runners ?? spawningRunners;
14352
- return assessPlanRisk({ path, text: readFileSync16(path, "utf8") }, {
15126
+ return assessPlanRisk({ path, text: readFileSync17(path, "utf8") }, {
14353
15127
  repoRoot: project.root,
14354
15128
  home: project.home,
14355
15129
  settingSources: config.settingSources,
@@ -14432,8 +15206,8 @@ function createPlanRiskCommand(seams = {}) {
14432
15206
  var risk_default = createPlanRiskCommand();
14433
15207
 
14434
15208
  // src/commands/plan/show.ts
14435
- import { readFileSync as readFileSync17 } from "fs";
14436
- import { join as join32 } from "path";
15209
+ import { readFileSync as readFileSync18 } from "fs";
15210
+ import { join as join34 } from "path";
14437
15211
  var USAGE28 = "rafa plan show <stub> [--tracker]";
14438
15212
  var COMMAND3 = "rafa plan show";
14439
15213
  function readTrackerFlag(value) {
@@ -14451,10 +15225,10 @@ function showPlan(plans, stub, tracker) {
14451
15225
  throw new CommandExit(1, `\u274C "${stub}" is no plan stub: a stub is letters, digits, ".", "_" and "-"`);
14452
15226
  }
14453
15227
  const name2 = planFileName(stub, tracker);
14454
- const file = join32(plans.path, name2);
15228
+ const file = join34(plans.path, name2);
14455
15229
  if (!isFile(file))
14456
- throw new CommandExit(1, missingText(stub, tracker, join32(plans.label, name2)));
14457
- const model = parsePlan(readFileSync17(file, "utf8"));
15230
+ throw new CommandExit(1, missingText(stub, tracker, join34(plans.label, name2)));
15231
+ const model = parsePlan(readFileSync18(file, "utf8"));
14458
15232
  const { header } = model;
14459
15233
  return {
14460
15234
  stub,
@@ -14477,7 +15251,7 @@ function taskLine(task) {
14477
15251
  return ` ${checkbox(task.status)} ${task.text}`;
14478
15252
  }
14479
15253
  function renderShownPlan(shown, dirLabel) {
14480
- const relative7 = join32(dirLabel, planFileName(shown.stub, shown.tracker));
15254
+ const relative7 = join34(dirLabel, planFileName(shown.stub, shown.tracker));
14481
15255
  const fileLabel = shown.tracker ? "Tracker" : "File";
14482
15256
  const lines2 = [`Plan: ${shown.stub}`, `${fileLabel}: ${relative7}`];
14483
15257
  if (shown.header.issue !== null)
@@ -14897,24 +15671,24 @@ function createPrListCommand(seams = DEFAULT_PR_SEAMS) {
14897
15671
  var list_default9 = createPrListCommand();
14898
15672
 
14899
15673
  // src/commands/pr/merge.ts
14900
- import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
14901
- import { join as join34 } from "path";
15674
+ import { existsSync as existsSync16, readFileSync as readFileSync20 } from "fs";
15675
+ import { join as join36 } from "path";
14902
15676
 
14903
15677
  // src/runtime/install.ts
14904
15678
  import {
14905
15679
  chmodSync,
14906
15680
  copyFileSync as copyFileSync4,
14907
- existsSync as existsSync14,
15681
+ existsSync as existsSync15,
14908
15682
  mkdirSync as mkdirSync6,
14909
15683
  readdirSync as readdirSync4,
14910
- readFileSync as readFileSync18,
15684
+ readFileSync as readFileSync19,
14911
15685
  realpathSync as realpathSync5,
14912
15686
  renameSync as renameSync2,
14913
- rmSync as rmSync4,
15687
+ rmSync as rmSync5,
14914
15688
  statSync as statSync11,
14915
15689
  symlinkSync
14916
15690
  } from "fs";
14917
- import { basename as basename6, delimiter as delimiter2, dirname as dirname6, join as join33, relative as relative7, resolve as resolve13 } from "path";
15691
+ import { basename as basename6, delimiter as delimiter2, dirname as dirname7, join as join35, relative as relative7, resolve as resolve14 } from "path";
14918
15692
  var LINK_NAME = "rafa";
14919
15693
  var FORCE_FLAG = "--force";
14920
15694
  var EXIT_DONE = 0;
@@ -14975,20 +15749,20 @@ function runtimeRefusalLines(runtimeDir, version2) {
14975
15749
  ];
14976
15750
  }
14977
15751
  function listTrackers(repoRoot, planDir) {
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));
15752
+ const dir = resolve14(repoRoot, planDir);
15753
+ return entryNames(dir).filter((entry) => TRACKER_NAME.test(entry)).sort((a, b) => a.localeCompare(b)).map((entry) => join35(dir, entry)).filter((path) => statSync11(path).isFile()).map((path) => relative7(repoRoot, path));
14980
15754
  }
14981
15755
  function openTrackers(repoRoot, planDir) {
14982
15756
  return listTrackers(repoRoot, planDir).flatMap((path) => {
14983
- const task = findNextTask(readFileSync18(join33(repoRoot, path), "utf8"));
15757
+ const task = findNextTask(readFileSync19(join35(repoRoot, path), "utf8"));
14984
15758
  return task ? [{ path, task }] : [];
14985
15759
  });
14986
15760
  }
14987
15761
  function readManifestVersion2(repoRoot) {
14988
- const manifest = JSON.parse(readFileSync18(join33(repoRoot, "package.json"), "utf8"));
15762
+ const manifest = JSON.parse(readFileSync19(join35(repoRoot, "package.json"), "utf8"));
14989
15763
  const record = typeof manifest === "object" && manifest !== null ? manifest : {};
14990
15764
  if (record["name"] !== name) {
14991
- throw new Error(`${join33(repoRoot, "package.json")} names ${JSON.stringify(record["name"])}, not ${name}: this is no rafa checkout`);
15765
+ throw new Error(`${join35(repoRoot, "package.json")} names ${JSON.stringify(record["name"])}, not ${name}: this is no rafa checkout`);
14992
15766
  }
14993
15767
  const version2 = record["version"];
14994
15768
  if (typeof version2 !== "string" || !VERSION_SHAPE.test(version2)) {
@@ -15001,8 +15775,8 @@ function copyTree(from, to) {
15001
15775
  let copied = 0;
15002
15776
  const entries = readdirSync4(from, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
15003
15777
  for (const entry of entries) {
15004
- const source = join33(from, entry.name);
15005
- const target = join33(to, entry.name);
15778
+ const source = join35(from, entry.name);
15779
+ const target = join35(to, entry.name);
15006
15780
  if (entry.isDirectory()) {
15007
15781
  copied += copyTree(source, target);
15008
15782
  } else if (entry.isFile()) {
@@ -15016,23 +15790,23 @@ function copyTree(from, to) {
15016
15790
  }
15017
15791
  function replaceTree(from, to) {
15018
15792
  const staging = temporaryName(to);
15019
- rmSync4(staging, { recursive: true, force: true });
15793
+ rmSync5(staging, { recursive: true, force: true });
15020
15794
  try {
15021
15795
  const copied = copyTree(from, staging);
15022
15796
  swapIn2(staging, to);
15023
15797
  return copied;
15024
15798
  } catch (err) {
15025
- rmSync4(staging, { recursive: true, force: true });
15799
+ rmSync5(staging, { recursive: true, force: true });
15026
15800
  throw err;
15027
15801
  }
15028
15802
  }
15029
15803
  function swapIn2(staging, to) {
15030
- if (!existsSync14(to)) {
15804
+ if (!existsSync15(to)) {
15031
15805
  renameSync2(staging, to);
15032
15806
  return;
15033
15807
  }
15034
15808
  const outgoing = `${temporaryName(to)}.outgoing`;
15035
- rmSync4(outgoing, { recursive: true, force: true });
15809
+ rmSync5(outgoing, { recursive: true, force: true });
15036
15810
  renameSync2(to, outgoing);
15037
15811
  try {
15038
15812
  renameSync2(staging, to);
@@ -15040,17 +15814,17 @@ function swapIn2(staging, to) {
15040
15814
  renameSync2(outgoing, to);
15041
15815
  throw err;
15042
15816
  }
15043
- rmSync4(outgoing, { recursive: true, force: true });
15817
+ rmSync5(outgoing, { recursive: true, force: true });
15044
15818
  }
15045
15819
  function relink(linkPath, target) {
15046
- mkdirSync6(dirname6(linkPath), { recursive: true });
15820
+ mkdirSync6(dirname7(linkPath), { recursive: true });
15047
15821
  const temporary = temporaryName(linkPath);
15048
- rmSync4(temporary, { force: true });
15822
+ rmSync5(temporary, { force: true });
15049
15823
  try {
15050
15824
  symlinkSync(target, temporary);
15051
15825
  renameSync2(temporary, linkPath);
15052
15826
  } catch (err) {
15053
- rmSync4(temporary, { force: true });
15827
+ rmSync5(temporary, { force: true });
15054
15828
  throw err;
15055
15829
  }
15056
15830
  const resolved = realpathSync5(linkPath);
@@ -15066,7 +15840,7 @@ function runBuild(repoRoot, lines2) {
15066
15840
  cwd: repoRoot,
15067
15841
  env: {
15068
15842
  ...process.env,
15069
- PATH: [dirname6(process.execPath), process.env["PATH"] ?? ""].join(delimiter2)
15843
+ PATH: [dirname7(process.execPath), process.env["PATH"] ?? ""].join(delimiter2)
15070
15844
  },
15071
15845
  stdin: "ignore",
15072
15846
  stdout: stdio,
@@ -15097,20 +15871,20 @@ function installRuntime(seams, options = {}) {
15097
15871
  }
15098
15872
  function runInstall(seams, options) {
15099
15873
  const { repoRoot, home, log } = seams;
15100
- const runtimeRoot = join33(home, RUNTIME_SUBDIR);
15101
- const linkPath = join33(home, RAFA_BIN_DIR, LINK_NAME);
15874
+ const runtimeRoot = join35(home, RUNTIME_SUBDIR);
15875
+ const linkPath = join35(home, RAFA_BIN_DIR, LINK_NAME);
15102
15876
  log(`repo root: ${repoRoot}`);
15103
15877
  log(`runtime root: ${runtimeRoot}`);
15104
15878
  log(`link: ${linkPath}`);
15105
15879
  const version2 = inStage("manifest", () => readManifestVersion2(repoRoot));
15106
- const planDir = inStage("config", () => resolve13(repoRoot, loadConfig({ root: repoRoot, home }, {}, seams.warn).config.planDir));
15880
+ const planDir = inStage("config", () => resolve14(repoRoot, loadConfig({ root: repoRoot, home }, {}, seams.warn).config.planDir));
15107
15881
  log(`plan dir: ${planDir}`);
15108
15882
  const open = inStage("trackers", () => openTrackers(repoRoot, planDir));
15109
15883
  if (open.length > 0)
15110
15884
  return { kind: "refused", reason: "trackers", planDir, trackers: open };
15111
- const distDir = join33(repoRoot, "dist");
15112
- const runtimeDir = join33(runtimeRoot, version2);
15113
- const installed = existsSync14(runtimeDir);
15885
+ const distDir = join35(repoRoot, "dist");
15886
+ const runtimeDir = join35(runtimeRoot, version2);
15887
+ const installed = existsSync15(runtimeDir);
15114
15888
  if (installed && options.force !== true) {
15115
15889
  return { kind: "refused", reason: "runtime-exists", version: version2, runtimeDir };
15116
15890
  }
@@ -15118,13 +15892,13 @@ function runInstall(seams, options) {
15118
15892
  const code = inStage("build", () => seams.build(repoRoot));
15119
15893
  if (code !== 0)
15120
15894
  throw new StageFailure("build", `the build exited ${code}`);
15121
- if (!existsSync14(join33(distDir, RUNTIME_ENTRY))) {
15122
- throw new StageFailure("build", `the build exited 0 and left no ${join33(distDir, RUNTIME_ENTRY)}`);
15895
+ if (!existsSync15(join35(distDir, RUNTIME_ENTRY))) {
15896
+ throw new StageFailure("build", `the build exited 0 and left no ${join35(distDir, RUNTIME_ENTRY)}`);
15123
15897
  }
15124
15898
  log(installed ? `copying ${distDir} over ${runtimeDir}, replacing it whole` : `copying ${distDir} into ${runtimeDir}`);
15125
15899
  const copied = inStage("copy", () => replaceTree(distDir, runtimeDir));
15126
15900
  log(`copied ${copied} file(s)`);
15127
- const resolved = inStage("link", () => relink(linkPath, join33(runtimeDir, RUNTIME_ENTRY)));
15901
+ const resolved = inStage("link", () => relink(linkPath, join35(runtimeDir, RUNTIME_ENTRY)));
15128
15902
  log(`${linkPath} resolves to ${resolved}`);
15129
15903
  return { kind: "done", version: version2, runtimeDir, linkPath, resolved, copied };
15130
15904
  }
@@ -15153,12 +15927,12 @@ function replaceFile(source, target) {
15153
15927
  chmodSync(temporary, statSync11(source).mode & 4095);
15154
15928
  renameSync2(temporary, target);
15155
15929
  } catch (err) {
15156
- rmSync4(temporary, { force: true });
15930
+ rmSync5(temporary, { force: true });
15157
15931
  throw err;
15158
15932
  }
15159
15933
  }
15160
15934
  function temporaryName(path) {
15161
- return join33(dirname6(path), `.${basename6(path)}.snapshot-${process.pid}`);
15935
+ return join35(dirname7(path), `.${basename6(path)}.snapshot-${process.pid}`);
15162
15936
  }
15163
15937
 
15164
15938
  // src/commands/pr/merge-followups.ts
@@ -15555,13 +16329,13 @@ function localHoldsBranch(git, branch, warn3) {
15555
16329
  }
15556
16330
  function textOf(path) {
15557
16331
  try {
15558
- return readFileSync19(path, "utf8");
16332
+ return readFileSync20(path, "utf8");
15559
16333
  } catch {
15560
16334
  return "";
15561
16335
  }
15562
16336
  }
15563
16337
  function followUpsFor(pr, git) {
15564
- const facts = readPackageFacts(textOf(join34(pr.project.root, "package.json")));
16338
+ const facts = readPackageFacts(textOf(join36(pr.project.root, "package.json")));
15565
16339
  if (facts.version === null)
15566
16340
  return [];
15567
16341
  const tags = git(["tag", "--list", versionTag(facts.version)]);
@@ -15569,7 +16343,7 @@ function followUpsFor(pr, git) {
15569
16343
  version: facts.version,
15570
16344
  tagged: tags.ok && tags.stdout.trim() !== "",
15571
16345
  rafaCheckout: facts.rafaCheckout,
15572
- runtimeInstalled: existsSync15(join34(pr.project.home, RUNTIME_SUBDIR, facts.version))
16346
+ runtimeInstalled: existsSync16(join36(pr.project.home, RUNTIME_SUBDIR, facts.version))
15573
16347
  });
15574
16348
  }
15575
16349
  function openGh(pr, seams) {
@@ -15699,7 +16473,7 @@ async function commentIfUnchecked(context, pr, number, unchecked) {
15699
16473
  context.output.info(`Commented on #${number} that it was merged with no checks: ${posted.url}`);
15700
16474
  return posted.url;
15701
16475
  }
15702
- async function runMerge(context, seams) {
16476
+ async function runMerge2(context, seams) {
15703
16477
  const yes = readBooleanFlag(context.flags, "yes", USAGE31);
15704
16478
  const skipChecks = readBooleanFlag(context.flags, "skip-checks", USAGE31);
15705
16479
  const wanted = readMethodFlag(context.flags, USAGE31);
@@ -15834,7 +16608,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
15834
16608
  ],
15835
16609
  outputs: ["text", "json"],
15836
16610
  run: async (context) => {
15837
- const merged2 = await runMerge(context, seams);
16611
+ const merged2 = await runMerge2(context, seams);
15838
16612
  if (context.outputMode === "json")
15839
16613
  context.output.result(merged2);
15840
16614
  if (merged2.merged)
@@ -15843,7 +16617,7 @@ function createPrMergeCommand(seams = DEFAULT_MERGE_SEAMS) {
15843
16617
  };
15844
16618
  return Object.freeze(command);
15845
16619
  }
15846
- var merge_default = createPrMergeCommand();
16620
+ var merge_default2 = createPrMergeCommand();
15847
16621
 
15848
16622
  // src/pr/triage/evidence.ts
15849
16623
  var FAILED_LOG_TAIL_LINES = 40;
@@ -16600,7 +17374,7 @@ function fileList(files) {
16600
17374
  function conflictReason(files) {
16601
17375
  return files.length === 0 ? "the head conflicts with the base and no conflicting file was read" : `the head conflicts with the base on ${fileList(files)}`;
16602
17376
  }
16603
- function failureReason2(step, failing) {
17377
+ function failureReason(step, failing) {
16604
17378
  const count2 = checkCount(failing.length);
16605
17379
  return step === undefined ? `${count2} failing, and no failing step was named` : `${count2} failing, the failing step being "${step.name}"`;
16606
17380
  }
@@ -16632,7 +17406,7 @@ function decideClass(input, verdict, conflicting) {
16632
17406
  triageClass: classifyFailedStep(step?.name),
16633
17407
  files: [],
16634
17408
  step,
16635
- reason: failureReason2(step, failingRows(rows))
17409
+ reason: failureReason(step, failingRows(rows))
16636
17410
  };
16637
17411
  }
16638
17412
  if (verdict === "none") {
@@ -16807,8 +17581,8 @@ function describeAge(updatedAt, now) {
16807
17581
  return `${plural4(hours, "hour", "hours")} ago`;
16808
17582
  return `${plural4(Math.floor(age / DAY_MS2), "day", "days")} ago`;
16809
17583
  }
16810
- function triageCommand(number, resolve14 = false) {
16811
- return resolve14 ? `rafa pr triage ${number} --resolve` : `rafa pr triage ${number}`;
17584
+ function triageCommand(number, resolve15 = false) {
17585
+ return resolve15 ? `rafa pr triage ${number} --resolve` : `rafa pr triage ${number}`;
16812
17586
  }
16813
17587
  function joinNumbers(pulls) {
16814
17588
  const names = pulls.map((pull) => `#${pull.number}`);
@@ -16819,14 +17593,14 @@ function joinNumbers(pulls) {
16819
17593
  function byNumber(pulls) {
16820
17594
  return Object.freeze([...pulls].sort((left, right) => left.number - right.number));
16821
17595
  }
16822
- function skipped(pull, now, resolve14) {
17596
+ function skipped(pull, now, resolve15) {
16823
17597
  return Object.freeze({
16824
17598
  number: pull.number,
16825
17599
  title: pull.title,
16826
17600
  updatedAt: pull.updatedAt,
16827
17601
  age: describeAge(pull.updatedAt, now),
16828
17602
  fresh: isFreshCandidate(pull.updatedAt, now),
16829
- command: triageCommand(pull.number, resolve14)
17603
+ command: triageCommand(pull.number, resolve15)
16830
17604
  });
16831
17605
  }
16832
17606
  function skippedLine(one) {
@@ -16870,15 +17644,15 @@ function listReading(rest, count2) {
16870
17644
  return listing("list", Object.freeze([]), rest, headline);
16871
17645
  }
16872
17646
  function selectTriagePullRequests(input) {
16873
- const resolve14 = input.resolve === true;
17647
+ const resolve15 = input.resolve === true;
16874
17648
  const { current, now } = input;
16875
17649
  if (current !== null) {
16876
17650
  return plain("branch", Object.freeze([current]), `assessing #${current.number}, the pull request open on ${current.headRefName}`);
16877
17651
  }
16878
17652
  const candidates = byNumber(input.candidates);
16879
17653
  const count2 = candidates.length;
16880
- const lines2 = candidates.map((pull) => skipped(pull, now, resolve14));
16881
- if (resolve14 && count2 > 1)
17654
+ const lines2 = candidates.map((pull) => skipped(pull, now, resolve15));
17655
+ if (resolve15 && count2 > 1)
16882
17656
  return refusal12(lines2, count2);
16883
17657
  if (count2 === 0) {
16884
17658
  return plain("none", Object.freeze([]), "no red pull request to triage, and no pull request on the current branch");
@@ -17213,10 +17987,10 @@ function withTaskBudget(plan2, usd) {
17213
17987
  }
17214
17988
 
17215
17989
  // src/pr/plans/load.ts
17216
- import { existsSync as existsSync16, readFileSync as readFileSync20 } from "fs";
17217
- import { dirname as dirname7, join as join35 } from "path";
17990
+ import { existsSync as existsSync17, readFileSync as readFileSync21 } from "fs";
17991
+ import { dirname as dirname8, join as join37 } from "path";
17218
17992
  import { fileURLToPath as fileURLToPath4 } from "url";
17219
- var MODULE_DIR4 = dirname7(fileURLToPath4(import.meta.url));
17993
+ var MODULE_DIR4 = dirname8(fileURLToPath4(import.meta.url));
17220
17994
  var PINNED_PLANS_DIRNAME = "plans";
17221
17995
  var PINNED_PLAN_CLASSES = Object.freeze([
17222
17996
  ...SIMPLE_TRIAGE_CLASSES,
@@ -17231,8 +18005,8 @@ function hasPinnedPlan(triageClass) {
17231
18005
  function pinnedPlanCandidates(moduleDir, triageClass) {
17232
18006
  const name2 = pinnedPlanFileName(triageClass);
17233
18007
  return [
17234
- join35(moduleDir, PINNED_PLANS_DIRNAME, name2),
17235
- join35(moduleDir, name2)
18008
+ join37(moduleDir, PINNED_PLANS_DIRNAME, name2),
18009
+ join37(moduleDir, name2)
17236
18010
  ];
17237
18011
  }
17238
18012
  function readPinnedPlan(triageClass, lookup = {}) {
@@ -17240,11 +18014,11 @@ function readPinnedPlan(triageClass, lookup = {}) {
17240
18014
  throw new Error(`No pinned resolve plan ships for ${triageClass}; ` + `one ships for ${PINNED_PLAN_CLASSES.join(", ")}`);
17241
18015
  }
17242
18016
  const candidates = pinnedPlanCandidates(lookup.moduleDir ?? MODULE_DIR4, triageClass);
17243
- const found = candidates.find((candidate) => existsSync16(candidate));
18017
+ const found = candidates.find((candidate) => existsSync17(candidate));
17244
18018
  if (found === undefined) {
17245
18019
  throw new Error(`The pinned resolve plan for ${triageClass} is missing: ` + `no file at ${candidates.join(" or ")}`);
17246
18020
  }
17247
- return readFileSync20(found, "utf8");
18021
+ return readFileSync21(found, "utf8");
17248
18022
  }
17249
18023
  var SLOT_PATTERN2 = /\{[A-Z][A-Z0-9_]*\}/g;
17250
18024
  var CONFLICT_SENTENCE_SLOT = "CONFLICT_SENTENCE";
@@ -17384,21 +18158,21 @@ function readAttemptRepeat(input) {
17384
18158
  }
17385
18159
 
17386
18160
  // src/commands/pr/resolve-loop.ts
17387
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
17388
- import { join as join36 } from "path";
17389
- var RESOLVE_SUBDIR = join36(".rafa", "resolve");
18161
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
18162
+ import { join as join38 } from "path";
18163
+ var RESOLVE_SUBDIR = join38(".rafa", "resolve");
17390
18164
  var RUN_PREFIX = "pr-";
17391
18165
  var ATTEMPT_PREFIX = "attempt-";
17392
18166
  var LOOP_WORDS = Object.freeze(["loop", "start"]);
17393
18167
  var CHILD_OUTPUT = "text";
17394
18168
  function resolveRunDir(home, number, attempt3) {
17395
- return join36(home, RESOLVE_SUBDIR, `${RUN_PREFIX}${number}`, `${ATTEMPT_PREFIX}${attempt3}`);
18169
+ return join38(home, RESOLVE_SUBDIR, `${RUN_PREFIX}${number}`, `${ATTEMPT_PREFIX}${attempt3}`);
17396
18170
  }
17397
18171
  function writeResolvePlan(write) {
17398
18172
  const dir = resolveRunDir(write.home, write.number, write.attempt);
17399
18173
  mkdirSync7(dir, { recursive: true });
17400
- const path = join36(dir, pinnedPlanFileName(write.triageClass));
17401
- writeFileSync5(path, write.plan, "utf8");
18174
+ const path = join38(dir, pinnedPlanFileName(write.triageClass));
18175
+ writeFileSync6(path, write.plan, "utf8");
17402
18176
  return path;
17403
18177
  }
17404
18178
  function resolveLoopArgv(planPath) {
@@ -17661,7 +18435,7 @@ async function resolvePullRequest(run) {
17661
18435
  throw new CommandExit(RESOLVE_REFUSE_EXIT, `\u274C ${refusal13}`);
17662
18436
  return runAttempts(run, detail, first);
17663
18437
  }
17664
- function ended2(run, state, parts) {
18438
+ function ended(run, state, parts) {
17665
18439
  return {
17666
18440
  ran: parts.ran,
17667
18441
  resolved: parts.resolved,
@@ -17689,7 +18463,7 @@ async function runAttempts(run, detail, first) {
17689
18463
  if (start2.stopped) {
17690
18464
  await writeResolveComment(run, detail, state.assessment, state.spent, false, workflowCountOf2(state.reading));
17691
18465
  const removal2 = closeWorktree(run, resolveWorktreePath(run.home, run.number));
17692
- return ended2(run, state, {
18466
+ return ended(run, state, {
17693
18467
  ran: state.lines.length > 0,
17694
18468
  resolved: false,
17695
18469
  stop: start2.stop,
@@ -17721,7 +18495,7 @@ async function runAttempts(run, detail, first) {
17721
18495
  state.evidence = end.evidence;
17722
18496
  if (end.green) {
17723
18497
  await writeResolveComment(run, detail, state.assessment, state.spent, true, workflowCountOf2(state.reading));
17724
- return ended2(run, state, {
18498
+ return ended(run, state, {
17725
18499
  ran: true,
17726
18500
  resolved: true,
17727
18501
  stop: null,
@@ -17745,7 +18519,7 @@ async function runAttempts(run, detail, first) {
17745
18519
  continue;
17746
18520
  await writeResolveComment(run, detail, state.assessment, state.spent, false, workflowCountOf2(state.reading));
17747
18521
  const removal = closeWorktree(run, worktree);
17748
- return ended2(run, state, {
18522
+ return ended(run, state, {
17749
18523
  ran: true,
17750
18524
  resolved: false,
17751
18525
  stop: repeat.stop,
@@ -17797,16 +18571,16 @@ async function redPullRequests(pr) {
17797
18571
  }
17798
18572
  return red;
17799
18573
  }
17800
- async function selectTargets(pr, warn3, now, resolve14) {
18574
+ async function selectTargets(pr, warn3, now, resolve15) {
17801
18575
  const branch = readBranchOrNone(pr, warn3);
17802
18576
  const current = branch === null ? null : await onProvider(`read the open pull request for the branch "${branch}"`, () => pr.pulls.findOpen(branch));
17803
18577
  if (current !== null)
17804
- return selectTriagePullRequests({ current, candidates: [], now, resolve: resolve14 });
18578
+ return selectTriagePullRequests({ current, candidates: [], now, resolve: resolve15 });
17805
18579
  return selectTriagePullRequests({
17806
18580
  current: null,
17807
18581
  candidates: await redPullRequests(pr),
17808
18582
  now,
17809
- resolve: resolve14
18583
+ resolve: resolve15
17810
18584
  });
17811
18585
  }
17812
18586
  async function detailOf4(pr, number) {
@@ -17935,7 +18709,7 @@ async function runTriage(context, seams) {
17935
18709
  }
17936
18710
  const numbers = asked === null ? (selection?.assess ?? []).map((one) => one.number) : [asked];
17937
18711
  const readings = [];
17938
- let resolve14 = null;
18712
+ let resolve15 = null;
17939
18713
  for (const number of numbers) {
17940
18714
  const assess = () => assessOne({
17941
18715
  pr,
@@ -17954,7 +18728,7 @@ async function runTriage(context, seams) {
17954
18728
  readings.push(reading);
17955
18729
  continue;
17956
18730
  }
17957
- resolve14 = await resolvePullRequest({
18731
+ resolve15 = await resolvePullRequest({
17958
18732
  trust: {
17959
18733
  permissions,
17960
18734
  trustedAuthors: pr.trustedAuthors,
@@ -17975,16 +18749,16 @@ async function runTriage(context, seams) {
17975
18749
  ...seams.clock === undefined ? {} : { clock: seams.clock },
17976
18750
  ...seams.sleep === undefined ? {} : { sleep: seams.sleep }
17977
18751
  });
17978
- readings.push(resolve14.reading);
18752
+ readings.push(resolve15.reading);
17979
18753
  }
17980
18754
  const text = [
17981
18755
  ...selection === null ? [] : [selection.message],
17982
18756
  ...readings.length === 0 ? [] : [renderTriages(readings)],
17983
- ...resolve14 === null ? [] : resolve14.lines
18757
+ ...resolve15 === null ? [] : resolve15.lines
17984
18758
  ].join(`
17985
18759
 
17986
18760
  `);
17987
- return { selection, readings, resolve: resolve14, text, exitCode: resolve14?.exitCode ?? 0 };
18761
+ return { selection, readings, resolve: resolve15, text, exitCode: resolve15?.exitCode ?? 0 };
17988
18762
  }
17989
18763
  function createPrTriageCommand(seams = DEFAULT_TRIAGE_SEAMS) {
17990
18764
  const command = {
@@ -18273,8 +19047,8 @@ function createPrWaitCommand(seams = DEFAULT_WAIT_SEAMS) {
18273
19047
  var wait_default = createPrWaitCommand();
18274
19048
 
18275
19049
  // src/commands/release/tag.ts
18276
- import { readFileSync as readFileSync21 } from "fs";
18277
- import { join as join37 } from "path";
19050
+ import { readFileSync as readFileSync22 } from "fs";
19051
+ import { join as join39 } from "path";
18278
19052
 
18279
19053
  // src/commands/release/release-commit.ts
18280
19054
  function unread2(reason, message) {
@@ -18332,13 +19106,13 @@ function readBranch2(git) {
18332
19106
  }
18333
19107
  function textOf2(path) {
18334
19108
  try {
18335
- return readFileSync21(path, "utf8");
19109
+ return readFileSync22(path, "utf8");
18336
19110
  } catch {
18337
19111
  return null;
18338
19112
  }
18339
19113
  }
18340
19114
  function readVersion(root, configured) {
18341
- const resolved = join37(root, configured);
19115
+ const resolved = join39(root, configured);
18342
19116
  const text = textOf2(resolved);
18343
19117
  if (text === null) {
18344
19118
  return {
@@ -18352,7 +19126,7 @@ function readVersion(root, configured) {
18352
19126
  return version2 === null ? { path: configured, text, version: null, problem: `${resolved} declares no version this can read` } : { path: configured, text, version: version2, problem: null };
18353
19127
  }
18354
19128
  function readNewestRelease(root, configured) {
18355
- const resolved = join37(root, configured);
19129
+ const resolved = join39(root, configured);
18356
19130
  const text = textOf2(resolved);
18357
19131
  if (text === null) {
18358
19132
  return { path: configured, version: null, problem: `the changelog could not be read at ${resolved}` };
@@ -18693,16 +19467,16 @@ var self_update_default = createSelfUpdateCommand();
18693
19467
 
18694
19468
  // src/commands/skill/backfill.ts
18695
19469
  import { statSync as statSync13 } from "fs";
18696
- import { join as join42, resolve as resolve16 } from "path";
19470
+ import { join as join44, resolve as resolve17 } from "path";
18697
19471
 
18698
19472
  // src/backfill/backup.ts
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";
19473
+ import { copyFileSync as copyFileSync5, existsSync as existsSync18, mkdirSync as mkdirSync8 } from "fs";
19474
+ import { dirname as dirname9, isAbsolute as isAbsolute8, join as join41, relative as relative8, resolve as resolve15, sep as sep5 } from "path";
18701
19475
 
18702
19476
  // src/backfill/proposal-file.ts
18703
- import { readdirSync as readdirSync5, readFileSync as readFileSync22 } from "fs";
18704
- import { join as join38 } from "path";
18705
- var BACKFILL_PATH = join38(".rafa", "backfill");
19477
+ import { readdirSync as readdirSync5, readFileSync as readFileSync23 } from "fs";
19478
+ import { join as join40 } from "path";
19479
+ var BACKFILL_PATH = join40(".rafa", "backfill");
18706
19480
  var PROPOSAL_FILE_PREFIX = "proposals-";
18707
19481
  var PROPOSAL_FILE_EXTENSION = ".yaml";
18708
19482
  var SHA256_PATTERN = /^[0-9a-f]{64}$/;
@@ -18728,7 +19502,7 @@ var PROPOSAL_FILE_HEADER = [
18728
19502
  ].join(`
18729
19503
  `);
18730
19504
  function proposalPath(dir, batch) {
18731
- return join38(dir, `${PROPOSAL_FILE_PREFIX}${String(batch).padStart(2, "0")}${PROPOSAL_FILE_EXTENSION}`);
19505
+ return join40(dir, `${PROPOSAL_FILE_PREFIX}${String(batch).padStart(2, "0")}${PROPOSAL_FILE_EXTENSION}`);
18732
19506
  }
18733
19507
  function isProposalFileName(name2) {
18734
19508
  return name2.startsWith(PROPOSAL_FILE_PREFIX) && name2.endsWith(PROPOSAL_FILE_EXTENSION);
@@ -18864,9 +19638,9 @@ function readProposalFiles(dir) {
18864
19638
  return [];
18865
19639
  }
18866
19640
  return names.filter((name2) => isProposalFileName(name2)).sort((left, right) => left.localeCompare(right)).map((name2) => {
18867
- const path = join38(dir, name2);
19641
+ const path = join40(dir, name2);
18868
19642
  try {
18869
- return { path, result: parseProposalFile(readFileSync22(path, "utf8")) };
19643
+ return { path, result: parseProposalFile(readFileSync23(path, "utf8")) };
18870
19644
  } catch (error) {
18871
19645
  return { path, result: unreadable2(`the file cannot be read (${error instanceof Error ? error.message : String(error)})`) };
18872
19646
  }
@@ -18874,19 +19648,19 @@ function readProposalFiles(dir) {
18874
19648
  }
18875
19649
 
18876
19650
  // src/backfill/backup.ts
18877
- var BACKUP_PATH = join39(BACKFILL_PATH, "backup");
19651
+ var BACKUP_PATH = join41(BACKFILL_PATH, "backup");
18878
19652
  var BACKUP_KINDS = ["copied", "kept", "tracked", "outside", "missing"];
18879
19653
  function backupDirectory(base) {
18880
- return join39(base, BACKUP_PATH);
19654
+ return join41(base, BACKUP_PATH);
18881
19655
  }
18882
19656
  function isUnder(root, path) {
18883
- const step = relative8(resolve14(root), resolve14(path));
19657
+ const step = relative8(resolve15(root), resolve15(path));
18884
19658
  return step === "" || !step.startsWith(`..${sep5}`) && step !== ".." && !isAbsolute8(step);
18885
19659
  }
18886
19660
  function backupPathFor(path, root, backupDir) {
18887
19661
  if (!isUnder(root, path))
18888
19662
  return null;
18889
- return join39(backupDir, relative8(resolve14(root), resolve14(path)));
19663
+ return join41(backupDir, relative8(resolve15(root), resolve15(path)));
18890
19664
  }
18891
19665
  function backupFile(path, options) {
18892
19666
  if (options.repoRoot !== null && isUnder(options.repoRoot, path)) {
@@ -18895,11 +19669,11 @@ function backupFile(path, options) {
18895
19669
  const backup = backupPathFor(path, options.root, options.backupDir);
18896
19670
  if (backup === null)
18897
19671
  return { path, kind: "outside", backup: null };
18898
- if (!existsSync17(path))
19672
+ if (!existsSync18(path))
18899
19673
  return { path, kind: "missing", backup: null };
18900
- if (existsSync17(backup))
19674
+ if (existsSync18(backup))
18901
19675
  return { path, kind: "kept", backup };
18902
- mkdirSync8(dirname8(backup), { recursive: true });
19676
+ mkdirSync8(dirname9(backup), { recursive: true });
18903
19677
  copyFileSync5(path, backup);
18904
19678
  return { path, kind: "copied", backup };
18905
19679
  }
@@ -18922,7 +19696,7 @@ function countBackups(results) {
18922
19696
  }
18923
19697
 
18924
19698
  // src/backfill/derive.ts
18925
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync6 } from "fs";
19699
+ import { readFileSync as readFileSync24, writeFileSync as writeFileSync7 } from "fs";
18926
19700
  import { basename as basename7 } from "path";
18927
19701
  var DERIVED_FIELDS = ["stack", "paths", "when_to_use"];
18928
19702
  function renderWhenToUse(trigger, prevents) {
@@ -19048,7 +19822,7 @@ function planFile(entry, options) {
19048
19822
  }
19049
19823
  let original;
19050
19824
  try {
19051
- original = readFileSync23(entry.path, "utf8");
19825
+ original = readFileSync24(entry.path, "utf8");
19052
19826
  } catch (error) {
19053
19827
  return inert(entry, "skipped", `the file cannot be read (${error instanceof Error ? error.message : String(error)})`);
19054
19828
  }
@@ -19092,7 +19866,7 @@ function planDerivation(root, options) {
19092
19866
  function applyFile(action, options) {
19093
19867
  if (action.text === null || action.original === null)
19094
19868
  return action;
19095
- const current = readFileSync23(action.path, "utf8");
19869
+ const current = readFileSync24(action.path, "utf8");
19096
19870
  if (current !== action.original) {
19097
19871
  return {
19098
19872
  ...action,
@@ -19101,7 +19875,7 @@ function applyFile(action, options) {
19101
19875
  text: null
19102
19876
  };
19103
19877
  }
19104
- writeFileSync6(action.path, action.text, "utf8");
19878
+ writeFileSync7(action.path, action.text, "utf8");
19105
19879
  const after = failureLines(checkFile(action.path, "skill", {
19106
19880
  projectRoot: options.projectRoot,
19107
19881
  pathDirs: options.pathDirs
@@ -19109,7 +19883,7 @@ function applyFile(action, options) {
19109
19883
  const added = addedFailures(action.before, after);
19110
19884
  if (added.length === 0)
19111
19885
  return action;
19112
- writeFileSync6(action.path, action.original, "utf8");
19886
+ writeFileSync7(action.path, action.original, "utf8");
19113
19887
  return {
19114
19888
  ...action,
19115
19889
  kind: "refused",
@@ -19123,7 +19897,7 @@ function applyDerivation(plan2, options) {
19123
19897
  }
19124
19898
 
19125
19899
  // src/backfill/proposal-batch.ts
19126
- import { readFileSync as readFileSync24 } from "fs";
19900
+ import { readFileSync as readFileSync25 } from "fs";
19127
19901
  import { relative as relative9 } from "path";
19128
19902
 
19129
19903
  // src/demote/report.ts
@@ -19159,7 +19933,7 @@ function countVerdicts(rows) {
19159
19933
  function escapeCell(value) {
19160
19934
  return value.replace(/\s+/g, " ").trim().replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
19161
19935
  }
19162
- function tableLine(cells) {
19936
+ function tableLine2(cells) {
19163
19937
  return `| ${cells.join(" | ")} |`;
19164
19938
  }
19165
19939
  function splitTableRow(line) {
@@ -19214,7 +19988,7 @@ function summaryLine3(rows) {
19214
19988
  return `${files}: ${parts.join(", ")}.`;
19215
19989
  }
19216
19990
  function writeDemotionReport(report2) {
19217
- const rows = report2.rows.map((row2) => tableLine([
19991
+ const rows = report2.rows.map((row2) => tableLine2([
19218
19992
  escapeCell(row2.path),
19219
19993
  escapeCell(row2.hash),
19220
19994
  escapeCell(row2.verdict),
@@ -19229,8 +20003,8 @@ function writeDemotionReport(report2) {
19229
20003
  "",
19230
20004
  summaryLine3(report2.rows),
19231
20005
  "",
19232
- tableLine(DEMOTION_REPORT_COLUMNS),
19233
- tableLine(DEMOTION_REPORT_COLUMNS.map(() => "---")),
20006
+ tableLine2(DEMOTION_REPORT_COLUMNS),
20007
+ tableLine2(DEMOTION_REPORT_COLUMNS.map(() => "---")),
19234
20008
  ...rows,
19235
20009
  ""
19236
20010
  ].join(`
@@ -19422,7 +20196,7 @@ function readCandidate(root, entry) {
19422
20196
  return null;
19423
20197
  let text;
19424
20198
  try {
19425
- text = readFileSync24(entry.path, "utf8");
20199
+ text = readFileSync25(entry.path, "utf8");
19426
20200
  } catch {
19427
20201
  return null;
19428
20202
  }
@@ -19528,8 +20302,8 @@ function parseSessionAnswer(stdout) {
19528
20302
  }
19529
20303
 
19530
20304
  // src/backfill/propose.ts
19531
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync25, writeFileSync as writeFileSync7 } from "fs";
19532
- import { join as join40, resolve as resolve15 } from "path";
20305
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync8 } from "fs";
20306
+ import { join as join42, resolve as resolve16 } from "path";
19533
20307
  var PROPOSAL_ACTION_KINDS = [
19534
20308
  "applied",
19535
20309
  "unchanged",
@@ -19613,7 +20387,7 @@ async function runProposalPass(options) {
19613
20387
  const number = index + 1;
19614
20388
  const file = await runProposalBatch(number, batch, options);
19615
20389
  const path = proposalPath(options.backfillDir, number);
19616
- writeFileSync7(path, renderProposalFile(file), "utf8");
20390
+ writeFileSync8(path, renderProposalFile(file), "utf8");
19617
20391
  written.push({ path, file });
19618
20392
  }
19619
20393
  return { written, candidates: candidates.length };
@@ -19655,7 +20429,7 @@ function writeRow(path, row2, original, options) {
19655
20429
  return inert2(path, row2, "unchanged", "every field the row proposes is already what the file carries");
19656
20430
  }
19657
20431
  const before = checkLines(path, options);
19658
- writeFileSync7(path, writeFrontmatter(document, mergeFrontmatter(document.data, changes)), "utf8");
20432
+ writeFileSync8(path, writeFrontmatter(document, mergeFrontmatter(document.data, changes)), "utf8");
19659
20433
  const added = addedFailures(before, checkLines(path, options));
19660
20434
  if (added.length === 0) {
19661
20435
  return {
@@ -19667,7 +20441,7 @@ function writeRow(path, row2, original, options) {
19667
20441
  added: []
19668
20442
  };
19669
20443
  }
19670
- writeFileSync7(path, original, "utf8");
20444
+ writeFileSync8(path, original, "utf8");
19671
20445
  return {
19672
20446
  path,
19673
20447
  relative: row2.path,
@@ -19678,7 +20452,7 @@ function writeRow(path, row2, original, options) {
19678
20452
  };
19679
20453
  }
19680
20454
  function applyRow(root, row2, options) {
19681
- const path = join40(root, ...row2.path.split("/"));
20455
+ const path = join42(root, ...row2.path.split("/"));
19682
20456
  if (row2.status === UNANSWERED_ROW) {
19683
20457
  return inert2(path, row2, "unanswered", row2.note ?? "the row carries no answer");
19684
20458
  }
@@ -19687,7 +20461,7 @@ function applyRow(root, row2, options) {
19687
20461
  }
19688
20462
  let original;
19689
20463
  try {
19690
- original = readFileSync25(path, "utf8");
20464
+ original = readFileSync26(path, "utf8");
19691
20465
  } catch {
19692
20466
  return inert2(path, row2, "refused", "the file the row names is gone");
19693
20467
  }
@@ -19701,7 +20475,7 @@ function fileRefusal(file, root) {
19701
20475
  if (file.status !== REVIEWED_FILE) {
19702
20476
  return `the proposal file is still marked ${file.status}: review it and mark it ${REVIEWED_FILE}`;
19703
20477
  }
19704
- return resolve15(file.skills) === resolve15(root) ? null : `the proposal file names the skills directory ${file.skills}, not ${root}`;
20478
+ return resolve16(file.skills) === resolve16(root) ? null : `the proposal file names the skills directory ${file.skills}, not ${root}`;
19705
20479
  }
19706
20480
  function countProposalActions(actions) {
19707
20481
  const counts = Object.fromEntries(PROPOSAL_ACTION_KINDS.map((kind) => [kind, 0]));
@@ -19715,7 +20489,7 @@ function applyProposals(files, root, options) {
19715
20489
  for (const file of files) {
19716
20490
  const refusal13 = fileRefusal(file, root);
19717
20491
  for (const row2 of file.rows) {
19718
- const action = refusal13 === null ? applyRow(root, row2, options) : inert2(join40(root, ...row2.path.split("/")), row2, "refused", refusal13);
20492
+ const action = refusal13 === null ? applyRow(root, row2, options) : inert2(join42(root, ...row2.path.split("/")), row2, "refused", refusal13);
19719
20493
  actions.push(action);
19720
20494
  if (row2.trigger !== null && action.kind !== "refused" && action.kind !== "unanswered") {
19721
20495
  triggers[row2.name] = row2.trigger;
@@ -19726,13 +20500,13 @@ function applyProposals(files, root, options) {
19726
20500
  }
19727
20501
 
19728
20502
  // src/demote/select.ts
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";
20503
+ import { readdirSync as readdirSync6, readFileSync as readFileSync27, realpathSync as realpathSync6, statSync as statSync12 } from "fs";
20504
+ import { basename as basename8, dirname as dirname10, join as join43 } from "path";
19731
20505
  var SKILLS_DIRECTORY = "skills";
19732
20506
  var CLAUDE_DIRECTORY = ".claude";
19733
20507
  var AUTO_EXTRACTED_ORIGIN = "auto-extracted";
19734
- var DEMOTED_PATH = join41(".rafa", "demoted");
19735
- var INSTINCTS_PATH = join41(".rafa", "instincts");
20508
+ var DEMOTED_PATH = join43(".rafa", "demoted");
20509
+ var INSTINCTS_PATH = join43(".rafa", "instincts");
19736
20510
  var REPORT_FILE = "report.md";
19737
20511
  function realOrSelf(path) {
19738
20512
  try {
@@ -19751,18 +20525,18 @@ function samePath(left, right) {
19751
20525
  function resolveDemotionScope(dir, home) {
19752
20526
  if (basename8(dir) !== SKILLS_DIRECTORY)
19753
20527
  return null;
19754
- const claude = dirname9(dir);
20528
+ const claude = dirname10(dir);
19755
20529
  if (basename8(claude) !== CLAUDE_DIRECTORY)
19756
20530
  return null;
19757
- const base = dirname9(claude);
19758
- const demotedDir = join41(base, DEMOTED_PATH);
20531
+ const base = dirname10(claude);
20532
+ const demotedDir = join43(base, DEMOTED_PATH);
19759
20533
  return {
19760
20534
  scope: samePath(base, home) ? "user" : "project",
19761
20535
  dir,
19762
20536
  base,
19763
20537
  demotedDir,
19764
- instinctsDir: join41(base, INSTINCTS_PATH),
19765
- reportPath: join41(demotedDir, REPORT_FILE)
20538
+ instinctsDir: join43(base, INSTINCTS_PATH),
20539
+ reportPath: join43(demotedDir, REPORT_FILE)
19766
20540
  };
19767
20541
  }
19768
20542
  function entryKind(path) {
@@ -19788,7 +20562,7 @@ function readSelected(absolute, path, entries) {
19788
20562
  let text;
19789
20563
  let mtime;
19790
20564
  try {
19791
- text = readFileSync26(absolute, "utf8");
20565
+ text = readFileSync27(absolute, "utf8");
19792
20566
  mtime = statSync12(absolute).mtime.toISOString();
19793
20567
  } catch {
19794
20568
  return null;
@@ -19804,14 +20578,14 @@ function selectsLearned(text, scope) {
19804
20578
  return origin === AUTO_EXTRACTED_ORIGIN;
19805
20579
  }
19806
20580
  function selectLearned(scope) {
19807
- const dir = join41(scope.dir, LEARNED_DIRECTORY);
20581
+ const dir = join43(scope.dir, LEARNED_DIRECTORY);
19808
20582
  if (entryKind(dir) !== "directory")
19809
20583
  return [];
19810
20584
  const selected = [];
19811
20585
  for (const name2 of visibleNames(dir)) {
19812
20586
  if (!name2.endsWith(MARKDOWN_EXTENSION) || name2 === MARKDOWN_EXTENSION)
19813
20587
  continue;
19814
- const absolute = join41(dir, name2);
20588
+ const absolute = join43(dir, name2);
19815
20589
  if (entryKind(absolute) !== "file")
19816
20590
  continue;
19817
20591
  const file = readSelected(absolute, `${LEARNED_DIRECTORY}/${name2}`, undefined);
@@ -19825,10 +20599,10 @@ function selectSkillFiles(scope) {
19825
20599
  for (const name2 of visibleNames(scope.dir)) {
19826
20600
  if (name2 === LEARNED_DIRECTORY)
19827
20601
  continue;
19828
- const skillDir = join41(scope.dir, name2);
20602
+ const skillDir = join43(scope.dir, name2);
19829
20603
  if (entryKind(skillDir) !== "directory")
19830
20604
  continue;
19831
- const absolute = join41(skillDir, SKILL_FILE);
20605
+ const absolute = join43(skillDir, SKILL_FILE);
19832
20606
  if (entryKind(absolute) !== "file")
19833
20607
  continue;
19834
20608
  const file = readSelected(absolute, `${name2}/${SKILL_FILE}`, visibleNames(skillDir));
@@ -19876,7 +20650,7 @@ Usage: ${BACKFILL_USAGE}`);
19876
20650
  throw new CommandExit(1, `\u274C Expected one directory, and read "${extra}" as a second
19877
20651
  Usage: ${BACKFILL_USAGE}`);
19878
20652
  }
19879
- return resolve16(cwd(), dir);
20653
+ return resolve17(cwd(), dir);
19880
20654
  }
19881
20655
  function readConsumerRoot(value, cwd) {
19882
20656
  if (value === undefined || value === false)
@@ -19885,7 +20659,7 @@ function readConsumerRoot(value, cwd) {
19885
20659
  throw new CommandExit(1, `\u274C --project needs a path, and the line gave it none
19886
20660
  Usage: ${BACKFILL_USAGE}`);
19887
20661
  }
19888
- return resolve16(cwd(), value);
20662
+ return resolve17(cwd(), value);
19889
20663
  }
19890
20664
  function isDirectory3(path) {
19891
20665
  try {
@@ -20013,7 +20787,7 @@ function reviewedProposals(dir) {
20013
20787
  return read2.flatMap((entry) => entry.result.file === null ? [] : [entry.result.file]);
20014
20788
  }
20015
20789
  function rowPaths(files, root) {
20016
- return files.flatMap((file) => file.status !== REVIEWED_FILE ? [] : file.rows.filter((row2) => row2.status === ANSWERED_ROW).map((row2) => join42(root, ...row2.path.split("/"))));
20790
+ return files.flatMap((file) => file.status !== REVIEWED_FILE ? [] : file.rows.filter((row2) => row2.status === ANSWERED_ROW).map((row2) => join44(root, ...row2.path.split("/"))));
20017
20791
  }
20018
20792
  function runApply(context, scope, backfillDir, projectRoot3) {
20019
20793
  const options = checkOptions(context, projectRoot3);
@@ -20059,7 +20833,7 @@ async function runBackfill(context, seams) {
20059
20833
  const dir = readBackfillDirectory(context.args, seams.cwd);
20060
20834
  const projectRoot3 = readConsumerRoot(context.flags["project"], seams.cwd);
20061
20835
  const scope = backfillScopeOf(dir, projectOf11(context).home);
20062
- const backfillDir = join42(scope.base, BACKFILL_PATH);
20836
+ const backfillDir = join44(scope.base, BACKFILL_PATH);
20063
20837
  if (mode === "propose") {
20064
20838
  await runPropose(context, scope, backfillDir, seams);
20065
20839
  return;
@@ -20173,13 +20947,13 @@ function createSkillCheckCommand(seams = DEFAULT_CHECK_SEAMS) {
20173
20947
  var check_default3 = createSkillCheckCommand();
20174
20948
 
20175
20949
  // src/commands/skill/demote.ts
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";
20950
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync29, statSync as statSync15, writeFileSync as writeFileSync10 } from "fs";
20951
+ import { dirname as dirname12, resolve as resolve18 } from "path";
20178
20952
 
20179
20953
  // src/demote/apply.ts
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";
20181
- import { tmpdir as tmpdir2 } from "os";
20182
- import { dirname as dirname10, join as join43 } from "path";
20954
+ import { copyFileSync as copyFileSync6, existsSync as existsSync19, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync3, readdirSync as readdirSync7, readFileSync as readFileSync28, renameSync as renameSync3, rmSync as rmSync6, statSync as statSync14, unlinkSync, writeFileSync as writeFileSync9 } from "fs";
20955
+ import { tmpdir as tmpdir3 } from "os";
20956
+ import { dirname as dirname11, join as join45 } from "path";
20183
20957
  var DEMOTION_ACTION_KINDS = [
20184
20958
  "demoted",
20185
20959
  "kept",
@@ -20200,13 +20974,13 @@ function learnedSkillName(path) {
20200
20974
  return base.slice(0, -MARKDOWN_EXTENSION.length);
20201
20975
  }
20202
20976
  function sourcePath(scope, path) {
20203
- return join43(scope.dir, ...path.split("/"));
20977
+ return join45(scope.dir, ...path.split("/"));
20204
20978
  }
20205
20979
  function demotedPath(scope, path) {
20206
- return join43(scope.demotedDir, ...path.split("/"));
20980
+ return join45(scope.demotedDir, ...path.split("/"));
20207
20981
  }
20208
20982
  function promotedPath(scope, path) {
20209
- return join43(scope.dir, learnedSkillName(path), SKILL_FILE);
20983
+ return join45(scope.dir, learnedSkillName(path), SKILL_FILE);
20210
20984
  }
20211
20985
  function takenInstinctIds(dir) {
20212
20986
  let names;
@@ -20219,7 +20993,7 @@ function takenInstinctIds(dir) {
20219
20993
  }
20220
20994
  function readText4(path) {
20221
20995
  try {
20222
- return statSync14(path).isFile() ? readFileSync27(path, "utf8") : null;
20996
+ return statSync14(path).isFile() ? readFileSync28(path, "utf8") : null;
20223
20997
  } catch {
20224
20998
  return null;
20225
20999
  }
@@ -20247,7 +21021,7 @@ function planKept(scope, row2, file) {
20247
21021
  return inert3(row2.path, "procedure", "kept", "procedure-shaped: left where it is, for the backfill");
20248
21022
  }
20249
21023
  const to = promotedPath(scope, row2.path);
20250
- if (existsSync18(to) || existsSync18(dirname10(to))) {
21024
+ if (existsSync19(to) || existsSync19(dirname11(to))) {
20251
21025
  return inert3(row2.path, "procedure", "refused", `kept as a skill, but ${to} is already there`);
20252
21026
  }
20253
21027
  return {
@@ -20261,8 +21035,8 @@ function planKept(scope, row2, file) {
20261
21035
  };
20262
21036
  }
20263
21037
  function checkCandidate(dir, id2, text, pathDirs) {
20264
- const path = join43(dir, `${id2}${MARKDOWN_EXTENSION}`);
20265
- writeFileSync8(path, text, "utf8");
21038
+ const path = join45(dir, `${id2}${MARKDOWN_EXTENSION}`);
21039
+ writeFileSync9(path, text, "utf8");
20266
21040
  const report3 = checkFile(path, "instinct", { projectRoot: null, pathDirs });
20267
21041
  return failureLines2(report3.issues);
20268
21042
  }
@@ -20282,11 +21056,11 @@ function planDemoted(scope, row2, file, context) {
20282
21056
  if (failures.length > 0) {
20283
21057
  return inert3(row2.path, "observation", "refused", `the record ${id2} fails the checker \u2014 ${failures.join("; ")}`);
20284
21058
  }
20285
- const destination = join43(scope.instinctsDir, `${id2}${MARKDOWN_EXTENSION}`);
21059
+ const destination = join45(scope.instinctsDir, `${id2}${MARKDOWN_EXTENSION}`);
20286
21060
  const kept = demotedPath(scope, row2.path);
20287
- if (existsSync18(destination))
21061
+ if (existsSync19(destination))
20288
21062
  return inert3(row2.path, "observation", "refused", `${destination} is already there`);
20289
- if (existsSync18(kept))
21063
+ if (existsSync19(kept))
20290
21064
  return inert3(row2.path, "observation", "refused", `${kept} is already there`);
20291
21065
  context.taken.push(id2);
20292
21066
  return {
@@ -20329,17 +21103,17 @@ function countActions(actions) {
20329
21103
  return counts;
20330
21104
  }
20331
21105
  function planDemotion(report3, scope, seams = DEFAULT_APPLY_SEAMS) {
20332
- const temp = mkdtempSync2(join43(tmpdir2(), "rafa-demote-"));
21106
+ const temp = mkdtempSync3(join45(tmpdir3(), "rafa-demote-"));
20333
21107
  const taken = [...takenInstinctIds(scope.instinctsDir)];
20334
21108
  try {
20335
21109
  const actions = report3.rows.map((row2) => planRow(scope, row2, { temp, taken, seams }));
20336
21110
  return { scope, actions, counts: countActions(actions) };
20337
21111
  } finally {
20338
- rmSync5(temp, { recursive: true, force: true });
21112
+ rmSync6(temp, { recursive: true, force: true });
20339
21113
  }
20340
21114
  }
20341
21115
  function moveFile(move3) {
20342
- mkdirSync10(dirname10(move3.to), { recursive: true });
21116
+ mkdirSync10(dirname11(move3.to), { recursive: true });
20343
21117
  try {
20344
21118
  renameSync3(move3.from, move3.to);
20345
21119
  } catch {
@@ -20350,8 +21124,8 @@ function moveFile(move3) {
20350
21124
  function applyDemotion(plan2) {
20351
21125
  for (const action of plan2.actions) {
20352
21126
  if (action.write !== null) {
20353
- mkdirSync10(dirname10(action.write.path), { recursive: true });
20354
- writeFileSync8(action.write.path, action.write.text, "utf8");
21127
+ mkdirSync10(dirname11(action.write.path), { recursive: true });
21128
+ writeFileSync9(action.write.path, action.write.text, "utf8");
20355
21129
  }
20356
21130
  if (action.move !== null)
20357
21131
  moveFile(action.move);
@@ -20416,7 +21190,7 @@ Usage: ${DEMOTE_USAGE}`);
20416
21190
  throw new CommandExit(1, `\u274C Expected one directory, and read "${extra}" as a second
20417
21191
  Usage: ${DEMOTE_USAGE}`);
20418
21192
  }
20419
- return resolve17(cwd(), dir);
21193
+ return resolve18(cwd(), dir);
20420
21194
  }
20421
21195
  function isDirectory4(path) {
20422
21196
  try {
@@ -20448,9 +21222,9 @@ function verdictLine2(report3) {
20448
21222
  return `${String(report3.rows.length)} file(s): ${parts.join(", ")}`;
20449
21223
  }
20450
21224
  function previousReport(path, context) {
20451
- if (!existsSync19(path))
21225
+ if (!existsSync20(path))
20452
21226
  return null;
20453
- const parsed = parseDemotionReport(readFileSync28(path, "utf8"));
21227
+ const parsed = parseDemotionReport(readFileSync29(path, "utf8"));
20454
21228
  if (parsed.report !== null)
20455
21229
  return parsed.report;
20456
21230
  context.output.warn(`the report already at ${path} does not parse, so no override in it is carried forward:` + ` ${parsed.issues.map((issue2) => `line ${String(issue2.line)} ${issue2.code}`).join(", ")}`);
@@ -20459,8 +21233,8 @@ function previousReport(path, context) {
20459
21233
  function runWrite(context, scope) {
20460
21234
  const files = selectSources(scope);
20461
21235
  const report3 = buildDemotionReport(files, previousReport(scope.reportPath, context));
20462
- mkdirSync11(dirname11(scope.reportPath), { recursive: true });
20463
- writeFileSync9(scope.reportPath, writeDemotionReport(report3), "utf8");
21236
+ mkdirSync11(dirname12(scope.reportPath), { recursive: true });
21237
+ writeFileSync10(scope.reportPath, writeDemotionReport(report3), "utf8");
20464
21238
  const result = {
20465
21239
  applied: false,
20466
21240
  scope: scope.scope,
@@ -20483,10 +21257,10 @@ function runWrite(context, scope) {
20483
21257
  context.output.info(line);
20484
21258
  }
20485
21259
  function reviewedReport(scope) {
20486
- if (!existsSync19(scope.reportPath)) {
21260
+ if (!existsSync20(scope.reportPath)) {
20487
21261
  throw new CommandExit(1, `\u274C No report at ${scope.reportPath}; run "rafa skill demote ${scope.dir}" first, then review it`);
20488
21262
  }
20489
- const parsed = parseDemotionReport(readFileSync28(scope.reportPath, "utf8"));
21263
+ const parsed = parseDemotionReport(readFileSync29(scope.reportPath, "utf8"));
20490
21264
  if (parsed.report === null) {
20491
21265
  const lines2 = parsed.issues.map((issue2) => ` line ${String(issue2.line)} ${issue2.code}: ${issue2.message}`);
20492
21266
  throw new CommandExit(1, [`\u274C ${scope.reportPath} does not parse:`, ...lines2].join(`
@@ -20592,7 +21366,7 @@ var demote_default = createSkillDemoteCommand();
20592
21366
 
20593
21367
  // src/commands/skill/show.ts
20594
21368
  var DEFAULT_SKILL_SHOW_SEAMS = DEFAULT_SKILL_LIST_SEAMS;
20595
- var COMMAND_NAME9 = "rafa skill show";
21369
+ var COMMAND_NAME11 = "rafa skill show";
20596
21370
  var USAGE37 = "rafa skill show <name> [--full]";
20597
21371
  function unknownSkillMessage(name2, inventory) {
20598
21372
  const agent = findShown(inventory.records, "agent", name2);
@@ -20620,7 +21394,7 @@ async function runShow3(context, seams) {
20620
21394
  const full = readSwitch("full", context.flags["full"], `Type the name ahead of it: ${USAGE37}`);
20621
21395
  const name2 = expectOneArgument(context.args, USAGE37);
20622
21396
  const project = projectOf13(context);
20623
- const { inventory, settingSources } = await projectInventory(project, context, seams, COMMAND_NAME9);
21397
+ const { inventory, settingSources } = await projectInventory(project, context, seams, COMMAND_NAME11);
20624
21398
  const warnings = warningLines(inventory);
20625
21399
  for (const warning of warnings)
20626
21400
  context.output.warn(warning);
@@ -20704,6 +21478,10 @@ function awayLine(current, home) {
20704
21478
  return null;
20705
21479
  return `${AWAY_PREFIX}: working ${id2(placeNumber(current))} for ${id2(placeNumber(home))}`;
20706
21480
  }
21481
+ var WAITING_ON = "owner review";
21482
+ function waitingLine(issue2) {
21483
+ return `waiting on ${id2(issue2)} (${WAITING_ON})`;
21484
+ }
20707
21485
 
20708
21486
  // src/status/render.ts
20709
21487
  var STATUS_SECTION_TITLES = Object.freeze({
@@ -20770,6 +21548,7 @@ function boardBody(reading) {
20770
21548
  return [
20771
21549
  info2(`${INDENT9}${placeLine2(place2.view)}`),
20772
21550
  ...away === null ? [] : [info2(`${INDENT9}${away}`)],
21551
+ ...place2.waiting === undefined ? [] : [info2(`${INDENT9}${waitingLine(place2.waiting.issue)}`)],
20773
21552
  ...place2.notices.map((notice) => ({ level: "warn", text: `${INDENT9}${notice}` }))
20774
21553
  ];
20775
21554
  }
@@ -20813,7 +21592,7 @@ function statusData(sections) {
20813
21592
  }
20814
21593
 
20815
21594
  // src/status/sections.ts
20816
- import { existsSync as existsSync20 } from "fs";
21595
+ import { existsSync as existsSync21 } from "fs";
20817
21596
  var STATUS_NETWORK_TIMEOUT_MS = 5000;
20818
21597
  function isIdleWorktree(row2) {
20819
21598
  return !row2.blockers.some((blocker) => blocker.kind === "recent");
@@ -20860,9 +21639,9 @@ function boundedRunner(open, root, deadline) {
20860
21639
  }
20861
21640
  async function networkSection(what, deadline, work) {
20862
21641
  let timer;
20863
- const expired = new Promise((resolve18) => {
21642
+ const expired = new Promise((resolve19) => {
20864
21643
  timer = setTimeout(() => {
20865
- resolve18(unread3(`the ${what} was not read within the ${String(deadline.ms)}ms network deadline`));
21644
+ resolve19(unread3(`the ${what} was not read within the ${String(deadline.ms)}ms network deadline`));
20866
21645
  }, Math.max(0, deadline.left()));
20867
21646
  });
20868
21647
  try {
@@ -20952,13 +21731,52 @@ function nextOfPlace(current, epic, walk) {
20952
21731
  return epic?.members.some((member) => member.number === line.issue) === true ? line.issue : null;
20953
21732
  }
20954
21733
  function hasBoards(root, listing2) {
20955
- if (existsSync20(positionFilePath(root)))
21734
+ if (existsSync21(positionFilePath(root)))
20956
21735
  return true;
20957
21736
  return listing2.some((issue2) => issue2.state === "OPEN" && issue2.labels.includes(ROADMAP_LABEL));
20958
21737
  }
20959
- async function readPlace(root, walk, listing2) {
21738
+ var NOT_WAITING = { waiting: null, notes: [] };
21739
+ function ref6(number) {
21740
+ return `#${String(number)}`;
21741
+ }
21742
+ async function readGate(sources, pullRequest) {
21743
+ try {
21744
+ return await sources.ownerGate()(pullRequest);
21745
+ } catch (error) {
21746
+ return { state: "unknown", reason: `could not read the owner gate of ${ref6(pullRequest)}: ${messageOf(error)}`, owners: [] };
21747
+ }
21748
+ }
21749
+ async function openPullGate(sources, pullRequest) {
21750
+ let detail;
21751
+ try {
21752
+ detail = await sources.pulls.get(pullRequest);
21753
+ } catch (error) {
21754
+ return { state: "unknown", reason: `could not read pull request ${ref6(pullRequest)}: ${messageOf(error)}`, owners: [] };
21755
+ }
21756
+ return detail?.state === "open" ? readGate(sources, pullRequest) : null;
21757
+ }
21758
+ async function readWaiting(sources) {
21759
+ const hop2 = readHopRecord(sources.root);
21760
+ if (!hop2.set) {
21761
+ return hop2.reason === "absent" ? NOT_WAITING : { waiting: null, notes: [`${hop2.detail}, so no waiting pull request is read`] };
21762
+ }
21763
+ const { record } = hop2;
21764
+ const { pullRequest } = record;
21765
+ if (record.state !== "waiting" || pullRequest === null)
21766
+ return NOT_WAITING;
21767
+ const placed = readPositionFile(sources.root);
21768
+ if (!placed.set || staleAgainst(record, placed.position))
21769
+ return NOT_WAITING;
21770
+ const gate = await openPullGate(sources, pullRequest);
21771
+ if (gate === null || gate.state === "approved" || gate.state === "not-gated")
21772
+ return NOT_WAITING;
21773
+ const waiting = { issue: record.target ?? pullRequest, pullRequest, gate: gate.state, reason: gate.reason };
21774
+ return { waiting, notes: [] };
21775
+ }
21776
+ async function readPlace(sources, walk, listing2) {
21777
+ const { root } = sources;
20960
21778
  if (!hasBoards(root, listing2))
20961
- return null;
21779
+ return { place: null, notes: [] };
20962
21780
  const resolved = await resolvePlace({ root, listing: listing2, defaultBoard: () => Promise.resolve(walk.roadmap) });
20963
21781
  const { current, home } = resolved;
20964
21782
  const epic = current.epic === null ? undefined : readEpics({ issues: listing2, claims: new Set, today: new Date }).epics.find((one) => one.number === current.epic);
@@ -20968,21 +21786,23 @@ async function readPlace(root, walk, listing2) {
20968
21786
  next: nextOfPlace(current, epic, walk)
20969
21787
  };
20970
21788
  const notices = resolved.notices.filter((notice) => notice.kind === "lost" || notice.reason !== "absent").map((notice) => notice.message);
20971
- return { current, home, view, notices };
21789
+ const place2 = { current, home, view, notices };
21790
+ const { waiting, notes } = await readWaiting(sources);
21791
+ return waiting === null ? { place: place2, notes } : { place: { ...place2, waiting }, notes };
20972
21792
  }
20973
- async function placeOrNote(root, walk, listing2) {
21793
+ async function placeOrNote(sources, walk, listing2) {
20974
21794
  try {
20975
- return { place: await readPlace(root, walk, await listing2()), notes: [] };
21795
+ return await readPlace(sources, walk, await listing2());
20976
21796
  } catch (error) {
20977
21797
  return { place: null, notes: [`the current place could not be read: ${messageOf(error)}`] };
20978
21798
  }
20979
21799
  }
20980
- async function readBoard(root, board, listing2, blocked) {
21800
+ async function readBoard(sources, board, listing2, blocked) {
20981
21801
  const [walk, report3] = await Promise.all([board.next(), blocked]);
20982
21802
  const { line } = walk;
20983
21803
  const next = line === null ? null : { line, ready: await board.isReady(line.issue), blocked: await board.blocking(line.issue) };
20984
21804
  const listed2 = report3.problem === null ? [] : [`the issues labelled ${SPEC_BLOCKED_LABEL} could not be read: ${report3.problem}`];
20985
- const { place: place2, notes } = await placeOrNote(root, walk, listing2);
21805
+ const { place: place2, notes } = await placeOrNote(sources, walk, listing2);
20986
21806
  const reading = {
20987
21807
  roadmap: walk.roadmap,
20988
21808
  next,
@@ -21013,6 +21833,12 @@ async function readStatusSections(input, seams = {}) {
21013
21833
  const pulls = (seams.pullRequests ?? ((runner) => createGhPullRequests({ gh: runner })))(gh);
21014
21834
  const listing2 = once2((seams.listing ?? ((runner) => createGhBoardListing({ gh: runner })))(gh));
21015
21835
  const board = (seams.board ?? ghNextBoard)({ gh, git, configured: config.roadmapIssue, listing: listing2 });
21836
+ const openGate = seams.ownerGate ?? nextOwnerGate;
21837
+ const place2 = {
21838
+ root,
21839
+ pulls,
21840
+ ownerGate: () => openGate({ gh, root, pulls, configured: config.roadmapIssue, listing: listing2 })
21841
+ };
21016
21842
  const sources = {
21017
21843
  base: config.prBase ?? DEFAULT_BASE_BRANCH,
21018
21844
  plans: plansDirAt(root, config.planDir),
@@ -21030,7 +21856,7 @@ async function readStatusSections(input, seams = {}) {
21030
21856
  const blockedIssues = seams.blockedIssues ?? ((runner) => readBlockedIssues({ gh: runner }));
21031
21857
  deadline.start();
21032
21858
  const pull = pullSection(sources, branch, provider, deadline);
21033
- const boardSection = isGh ? networkSection("board", deadline, () => readBoard(root, board, listing2, blockedIssues(gh))) : Promise.resolve(unread3(notGhProblem("board", provider)));
21859
+ const boardSection = isGh ? networkSection("board", deadline, () => readBoard(place2, board, listing2, blockedIssues(gh))) : Promise.resolve(unread3(notGhProblem("board", provider)));
21034
21860
  const housekeeping = readHousekeeping(input, cleanup, now);
21035
21861
  return {
21036
21862
  branch,
@@ -21042,7 +21868,7 @@ async function readStatusSections(input, seams = {}) {
21042
21868
  }
21043
21869
 
21044
21870
  // src/commands/status.ts
21045
- var COMMAND_NAME10 = "rafa status";
21871
+ var COMMAND_NAME12 = "rafa status";
21046
21872
  var STATUS_USAGE = "rafa status";
21047
21873
  var STATUS_ARGUMENT_EXIT = 2;
21048
21874
  var DEFAULT_STATUS_SEAMS = Object.freeze({});
@@ -21054,13 +21880,13 @@ function projectOf14(context) {
21054
21880
  function expectNoWord(args) {
21055
21881
  if (args.length === 0)
21056
21882
  return;
21057
- throw new CommandExit(STATUS_ARGUMENT_EXIT, `\u274C ${COMMAND_NAME10}: expected no argument, got ${String(args.length)}: ${args.join(" ")}
21883
+ throw new CommandExit(STATUS_ARGUMENT_EXIT, `\u274C ${COMMAND_NAME12}: expected no argument, got ${String(args.length)}: ${args.join(" ")}
21058
21884
  Usage: ${STATUS_USAGE}`);
21059
21885
  }
21060
21886
  async function runStatus2(context, seams) {
21061
21887
  expectNoWord(context.args);
21062
21888
  const project = projectOf14(context);
21063
- const config = resolveProjectConfig(project, COMMAND_NAME10, (message) => {
21889
+ const config = resolveProjectConfig(project, COMMAND_NAME12, (message) => {
21064
21890
  context.output.warn(message);
21065
21891
  });
21066
21892
  const read3 = seams.read ?? ((input) => readStatusSections(input));
@@ -21126,7 +21952,7 @@ var CORE_SUBJECTS = Object.freeze([
21126
21952
  { name: "loop", summary: "start a plan; stop, pause, resume, show and list its sessions" },
21127
21953
  { name: "issue", summary: "the tracker: list, show, create, comment on and move issues; mark one ready, unblock it and check its references" },
21128
21954
  { name: "pr", summary: "the pull request of a branch: one line, in full or in the browser; list, wait on, merge and triage them" },
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" },
21955
+ { 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; merge another device's store; move an NDJSON store to SQLite" },
21130
21956
  { name: "module", summary: "list the configured modules; run an action a module provides" },
21131
21957
  { name: "agent", summary: "copy an agent definition into the project; list what a session sees" },
21132
21958
  { name: "skill", summary: "check a skills directory; list each tier; demote and backfill it" },
@@ -21152,7 +21978,7 @@ var CORE_COMMANDS = Object.freeze([
21152
21978
  show_default4,
21153
21979
  create_default,
21154
21980
  comment_default,
21155
- move_default2,
21981
+ move_default3,
21156
21982
  ready_default,
21157
21983
  unblock_default,
21158
21984
  check_default2,
@@ -21161,7 +21987,7 @@ var CORE_COMMANDS = Object.freeze([
21161
21987
  view_default,
21162
21988
  list_default9,
21163
21989
  wait_default,
21164
- merge_default,
21990
+ merge_default2,
21165
21991
  triage_default,
21166
21992
  collect_default,
21167
21993
  report_default,
@@ -21169,6 +21995,8 @@ var CORE_COMMANDS = Object.freeze([
21169
21995
  copy_default,
21170
21996
  schema_default,
21171
21997
  migrate_default,
21998
+ merge_default,
21999
+ move_default,
21172
22000
  list_default8,
21173
22001
  exec_default,
21174
22002
  vendor_default,
@@ -21193,7 +22021,7 @@ var CORE_COMMANDS = Object.freeze([
21193
22021
  new_default,
21194
22022
  defer_default,
21195
22023
  promote_default,
21196
- move_default,
22024
+ move_default2,
21197
22025
  close_default,
21198
22026
  cancel_default,
21199
22027
  status_default3,