@open-tomato/rafa 0.28.0 → 0.31.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.
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  CONFIG_DEFAULTS,
4
4
  CONFIG_FILE,
5
+ ConfigError,
5
6
  FINDING_DOMAINS,
6
7
  FINDING_KINDS,
7
8
  FINDING_SIGNALS,
@@ -10,20 +11,24 @@ import {
10
11
  configFilePath,
11
12
  createTextOutput,
12
13
  isStampableStub,
13
- readRafaBlocks
14
- } from "./index-rv7y8y1j.js";
14
+ parseConfigText,
15
+ readRafaBlocks,
16
+ resolveConfig
17
+ } from "./index-t6q4k1tq.js";
15
18
  import {
16
19
  bless,
17
20
  merge
18
21
  } from "./index-d6d0942q.js";
19
22
  import {
23
+ CommandExit,
20
24
  commandSpelling
21
- } from "./index-4j1e3h26.js";
25
+ } from "./index-e10rwa4g.js";
22
26
  import {
23
27
  BUSY_TIMEOUT_MAX_MS,
24
28
  BUSY_TIMEOUT_MIN_MS,
25
29
  RAFA_INSTINCTS_PATH,
26
30
  STORE_BACKENDS,
31
+ SYNC_STRATEGIES,
27
32
  bundledBinDirectory,
28
33
  describeValue,
29
34
  isBusyTimeoutMs,
@@ -31,7 +36,7 @@ import {
31
36
  messageOf,
32
37
  resolveInstinctScopes,
33
38
  tierExists
34
- } from "./index-sf2brh4w.js";
39
+ } from "./index-dnwcx1cs.js";
35
40
  import {
36
41
  actionHash,
37
42
  triggerKey
@@ -244,7 +249,7 @@ import { Database } from "bun:sqlite";
244
249
 
245
250
  // package.json
246
251
  var name = "@open-tomato/rafa";
247
- var version = "0.28.0";
252
+ var version = "0.31.0";
248
253
 
249
254
  // src/cli/version.ts
250
255
  var RAFA_VERSION = version;
@@ -254,7 +259,7 @@ function versionLine() {
254
259
 
255
260
  // src/effort/store/development-build.ts
256
261
  import { tmpdir as tmpdir2 } from "os";
257
- import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute3 } from "path";
262
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5 } from "path";
258
263
 
259
264
  // src/loop/sessions.ts
260
265
  import {
@@ -266,7 +271,7 @@ import {
266
271
  rmSync as rmSync3,
267
272
  writeFileSync as writeFileSync3
268
273
  } from "fs";
269
- import { basename, dirname as dirname5, join as join7 } from "path";
274
+ import { basename, dirname as dirname5, isAbsolute as isAbsolute4, join as join7 } from "path";
270
275
 
271
276
  // src/next/hop-record.ts
272
277
  import { randomUUID as randomUUID2 } from "crypto";
@@ -280,7 +285,73 @@ import { dirname as dirname3, join as join5 } from "path";
280
285
 
281
286
  // src/project/scope.ts
282
287
  import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
283
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4 } from "path";
288
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4 } from "path";
289
+
290
+ // src/project/worktree-root.ts
291
+ import { isAbsolute as isAbsolute2 } from "path";
292
+
293
+ // src/pr/git.ts
294
+ import { spawnSync } from "child_process";
295
+ function createGitRunner(cwd) {
296
+ return (args) => {
297
+ const result = spawnSync("git", [...args], {
298
+ cwd,
299
+ encoding: "utf8",
300
+ env: { ...process.env, LC_ALL: "C" }
301
+ });
302
+ if (result.error !== undefined) {
303
+ return { ok: false, stdout: "", stderr: `could not run git in ${cwd}: ${messageOf(result.error)}` };
304
+ }
305
+ return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
306
+ };
307
+ }
308
+ function gitSaid(result) {
309
+ return [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream !== "").join(`
310
+ `);
311
+ }
312
+
313
+ // src/project/worktree-root.ts
314
+ var NOT_A_REPOSITORY = "fatal: not a git repository (or any ";
315
+ var WORKTREE_LIST_ARGS = Object.freeze(["worktree", "list", "--porcelain", "-z"]);
316
+ var WORKTREE_PREFIX = "worktree ";
317
+ var BARE_LINE = "bare";
318
+
319
+ class WorktreeRootError extends Error {
320
+ constructor(message, options) {
321
+ super(`rafa worktree root: ${message}`, options);
322
+ this.name = "WorktreeRootError";
323
+ }
324
+ }
325
+ function firstRecordOf(stdout) {
326
+ const lines = stdout.split("\x00");
327
+ const end = lines.indexOf("");
328
+ return end === -1 ? lines : lines.slice(0, end);
329
+ }
330
+ function readListing(dir, stdout) {
331
+ const record = firstRecordOf(stdout);
332
+ const head = record[0];
333
+ if (head === undefined || !head.startsWith(WORKTREE_PREFIX)) {
334
+ throw new WorktreeRootError(`git worktree list in ${dir} printed no worktree line first`);
335
+ }
336
+ if (record.includes(BARE_LINE))
337
+ return null;
338
+ return head.slice(WORKTREE_PREFIX.length);
339
+ }
340
+ function readFailure(dir, result) {
341
+ if (result.stderr.startsWith(NOT_A_REPOSITORY))
342
+ return null;
343
+ throw new WorktreeRootError(`git worktree list failed in ${dir}: ${gitSaid(result)}`);
344
+ }
345
+ function mainCheckoutOf(dir, seams = {}) {
346
+ if (!isAbsolute2(dir)) {
347
+ throw new WorktreeRootError(`directory ${JSON.stringify(dir)} is not an absolute path`);
348
+ }
349
+ const git = (seams.openGit ?? createGitRunner)(dir);
350
+ const result = git(WORKTREE_LIST_ARGS);
351
+ return result.ok ? readListing(dir, result.stdout) : readFailure(dir, result);
352
+ }
353
+
354
+ // src/project/scope.ts
284
355
  var SCOPE_DIR = dirname2(CONFIG_FILE);
285
356
  var INIT_COMMAND = "rafa init";
286
357
  var DISK_FILE_SYSTEM = Object.freeze({
@@ -297,17 +368,19 @@ class ScopeError extends Error {
297
368
  function scopeAt(base) {
298
369
  return { dir: join4(base, SCOPE_DIR), configFile: configFilePath(base) };
299
370
  }
300
- function initHint(start, passedUserConfig = null) {
371
+ function initHint(start, passedUserConfig = null, probedMainCheckout = null) {
301
372
  const passed = passedUserConfig === null ? [] : [`${passedUserConfig} is the user scope's config and marks no project`];
373
+ const main = probedMainCheckout === null ? [] : [`nor in ${probedMainCheckout}, the main checkout of the repository holding it`];
302
374
  return [
303
375
  `not inside a rafa project: no ${CONFIG_FILE} in ${start} or any directory above it`,
376
+ ...main,
304
377
  ...passed,
305
378
  `run \`${INIT_COMMAND}\` to set one up`
306
379
  ].join(`
307
380
  `);
308
381
  }
309
382
  function requireAbsolute(label, path) {
310
- if (!isAbsolute2(path)) {
383
+ if (!isAbsolute3(path)) {
311
384
  throw new ScopeError(`${label} is ${describeValue(path)}, expected an absolute path`);
312
385
  }
313
386
  }
@@ -341,17 +414,36 @@ function walkUp(realStart, realHome, fs) {
341
414
  }
342
415
  return { root: null, passedUserConfig };
343
416
  }
417
+ function mainCheckoutAt(realStart, mainCheckout) {
418
+ try {
419
+ return mainCheckout(realStart);
420
+ } catch (error) {
421
+ if (!(error instanceof WorktreeRootError))
422
+ throw error;
423
+ throw new ScopeError(`cannot read the main checkout of ${realStart} (${error.message})`, { cause: error });
424
+ }
425
+ }
426
+ function fallBack(realStart, realHome, fs, mainCheckout) {
427
+ const main = mainCheckoutAt(realStart, mainCheckout);
428
+ if (main === null || main === realHome || selfAndAncestors(realStart).includes(main)) {
429
+ return { root: null, probed: null };
430
+ }
431
+ return fs.exists(configFilePath(main)) ? { root: main, probed: main } : { root: null, probed: main };
432
+ }
344
433
  function resolveScope(start, seams) {
345
- const { home, fs = DISK_FILE_SYSTEM } = seams;
434
+ const { home, fs = DISK_FILE_SYSTEM, mainCheckout = mainCheckoutOf } = seams;
346
435
  requireAbsolute("start directory", start);
347
436
  requireAbsolute("home directory", home);
348
437
  const user = scopeAt(home);
349
- const walk = walkUp(realStartOf(start, fs), realHomeOf(home, fs), fs);
350
- if (walk.root === null) {
438
+ const realStart = realStartOf(start, fs);
439
+ const realHome = realHomeOf(home, fs);
440
+ const walk = walkUp(realStart, realHome, fs);
441
+ const fallback = walk.root === null ? fallBack(realStart, realHome, fs, mainCheckout) : { root: walk.root, probed: null };
442
+ if (fallback.root === null) {
351
443
  const passed = walk.passedUserConfig ? user.configFile : null;
352
- return { found: false, start, home, user, hint: initHint(start, passed) };
444
+ return { found: false, start, home, user, hint: initHint(start, passed, fallback.probed) };
353
445
  }
354
- return { found: true, root: walk.root, home, project: scopeAt(walk.root), user };
446
+ return { found: true, root: fallback.root, home, project: scopeAt(fallback.root), user };
355
447
  }
356
448
 
357
449
  // src/project/position.ts
@@ -612,6 +704,12 @@ function hopProblem(fields) {
612
704
  const hop2 = field(fields, "hop");
613
705
  return asHopRecord(hop2) === null ? `hop is ${describeValue2(hop2)}, expected a hop record` : null;
614
706
  }
707
+ function worktreeProblem(fields) {
708
+ if (!Object.hasOwn(fields, "worktree"))
709
+ return null;
710
+ const worktree = field(fields, "worktree");
711
+ return isText(worktree) && isAbsolute4(worktree) ? null : `worktree is ${describeValue2(worktree)}, expected an absolute path`;
712
+ }
615
713
  function sessionIdProblem(sessionId, file) {
616
714
  if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
617
715
  return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
@@ -636,7 +734,8 @@ function recordProblems(fields, file) {
636
734
  typeof startedAt === "string" && !Number.isNaN(Date.parse(startedAt)) ? null : `startedAt is ${describeValue2(startedAt)}, expected a timestamp`,
637
735
  SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
638
736
  taskProblem(field(fields, "task")),
639
- hopProblem(fields)
737
+ hopProblem(fields),
738
+ worktreeProblem(fields)
640
739
  ];
641
740
  return problems.filter((problem) => problem !== null);
642
741
  }
@@ -647,6 +746,7 @@ function freezeHop(value) {
647
746
  function freezeRecord(fields) {
648
747
  const task = field(fields, "task");
649
748
  const hop2 = field(fields, "hop");
749
+ const worktree = field(fields, "worktree");
650
750
  return Object.freeze({
651
751
  sessionId: field(fields, "sessionId"),
652
752
  planStub: field(fields, "planStub"),
@@ -656,7 +756,8 @@ function freezeRecord(fields) {
656
756
  startedAt: field(fields, "startedAt"),
657
757
  state: field(fields, "state"),
658
758
  task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
659
- ...hop2 === undefined ? {} : { hop: freezeHop(hop2) }
759
+ ...hop2 === undefined ? {} : { hop: freezeHop(hop2) },
760
+ ...worktree === undefined ? {} : { worktree }
660
761
  });
661
762
  }
662
763
  function parseSessionRecord(text, file) {
@@ -850,7 +951,7 @@ function isOwnedStore(path, env, tempDir) {
850
951
  if (isUnderTempDir(path, tempDir))
851
952
  return true;
852
953
  const override = env[EFFORT_DIR_VARIABLE];
853
- if (override === undefined || override === "" || !isAbsolute3(override))
954
+ if (override === undefined || override === "" || !isAbsolute5(override))
854
955
  return false;
855
956
  return storeProjectRoot(path) === null && isUnderTempDir(path, override);
856
957
  }
@@ -1482,26 +1583,6 @@ import { createHmac } from "crypto";
1482
1583
  import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync } from "fs";
1483
1584
  import { hostname, platform as currentPlatform } from "os";
1484
1585
 
1485
- // src/pr/git.ts
1486
- import { spawnSync } from "child_process";
1487
- function createGitRunner(cwd) {
1488
- return (args) => {
1489
- const result = spawnSync("git", [...args], {
1490
- cwd,
1491
- encoding: "utf8",
1492
- env: { ...process.env, LC_ALL: "C" }
1493
- });
1494
- if (result.error !== undefined) {
1495
- return { ok: false, stdout: "", stderr: `could not run git in ${cwd}: ${messageOf(result.error)}` };
1496
- }
1497
- return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1498
- };
1499
- }
1500
- function gitSaid(result) {
1501
- return [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream !== "").join(`
1502
- `);
1503
- }
1504
-
1505
1586
  // src/schema/project-id.ts
1506
1587
  import { spawnSync as spawnSync2 } from "child_process";
1507
1588
  var PROJECT_ID_LENGTH = 12;
@@ -1778,17 +1859,1390 @@ function openSqliteStore(repoRoot) {
1778
1859
  };
1779
1860
  }
1780
1861
 
1862
+ // src/effort/sync/file.ts
1863
+ import { existsSync as existsSync12 } from "fs";
1864
+ import { join as join12, resolve as resolve5 } from "path";
1865
+
1866
+ // src/effort/store/fix-schema.ts
1867
+ import { existsSync as existsSync7 } from "fs";
1868
+ import { Database as Database2 } from "bun:sqlite";
1869
+
1870
+ // src/effort/store/rebuild-aside.ts
1871
+ import { existsSync as existsSync6, renameSync as renameSync4, rmSync as rmSync4 } from "fs";
1872
+
1873
+ class RebuildRefusal extends Error {
1874
+ constructor(message) {
1875
+ super(message);
1876
+ this.name = "RebuildRefusal";
1877
+ }
1878
+ }
1879
+ function quoted(name2) {
1880
+ return `"${name2.replaceAll('"', '""')}"`;
1881
+ }
1882
+ function count(db, sql) {
1883
+ return db.query(sql).get()?.n ?? 0;
1884
+ }
1885
+ function refuseInFlight(path) {
1886
+ for (const suffix of ["-journal", "-wal"]) {
1887
+ if (existsSync6(`${path}${suffix}`)) {
1888
+ throw new RebuildRefusal(`${path}${suffix} is there, so a write is in flight or was interrupted; stop whatever uses the store` + " and open it once with rafa before repairing it");
1889
+ }
1890
+ }
1891
+ }
1892
+ function checkedCounts(db, tables, original, added = {}) {
1893
+ return tables.map((table) => {
1894
+ const rows = count(db, `SELECT count(*) AS n FROM main.${quoted(table)}`);
1895
+ const originalRows = count(db, `SELECT count(*) AS n FROM ${original}.${quoted(table)}`);
1896
+ const addedRows = added[table] ?? 0;
1897
+ if (rows !== originalRows + addedRows) {
1898
+ const plus = addedRows === 0 ? "" : ` plus ${String(addedRows)} added`;
1899
+ throw new RebuildRefusal(`the rebuilt ${table} holds ${String(rows)} rows, the store ${String(originalRows)}${plus}`);
1900
+ }
1901
+ return { table, rows };
1902
+ });
1903
+ }
1904
+ function refuseCorrupt(db) {
1905
+ const answer = db.query("PRAGMA main.integrity_check").get()?.integrity_check;
1906
+ if (answer !== "ok")
1907
+ throw new RebuildRefusal(`the rebuilt store failed integrity_check: ${answer ?? "no answer"}`);
1908
+ }
1909
+ function removeParallel(parallelPath) {
1910
+ rmSync4(parallelPath, { force: true });
1911
+ rmSync4(`${parallelPath}-journal`, { force: true });
1912
+ }
1913
+ function swapIn(path, parallelPath, backupPath) {
1914
+ renameSync4(path, backupPath);
1915
+ try {
1916
+ renameSync4(parallelPath, path);
1917
+ } catch (error) {
1918
+ renameSync4(backupPath, path);
1919
+ throw error;
1920
+ }
1921
+ }
1922
+ function refuseTaken(paths) {
1923
+ for (const taken of paths) {
1924
+ if (existsSync6(taken))
1925
+ throw new RebuildRefusal(`${taken} is already there; move it aside or pass another stamp`);
1926
+ }
1927
+ }
1928
+ function rebuildAside(options) {
1929
+ const { path, parallelPath, backupPath, dryRun, build } = options;
1930
+ refuseInFlight(path);
1931
+ refuseTaken([parallelPath, backupPath]);
1932
+ let built;
1933
+ try {
1934
+ built = build(parallelPath);
1935
+ } catch (error) {
1936
+ removeParallel(parallelPath);
1937
+ throw error;
1938
+ }
1939
+ if (dryRun) {
1940
+ removeParallel(parallelPath);
1941
+ return { built, backupPath: null };
1942
+ }
1943
+ swapIn(path, parallelPath, backupPath);
1944
+ return { built, backupPath };
1945
+ }
1946
+
1947
+ // src/effort/store/fix-schema.ts
1948
+ var REPAIRABLE_REASONS = [
1949
+ "pre-log-unreleased",
1950
+ "gate-mismatch",
1951
+ "edited",
1952
+ "unknown-breaks-readers",
1953
+ "unknown-breaks-writers"
1954
+ ];
1955
+ var LIVE = "live";
1956
+ function tableNames(db, schema) {
1957
+ return db.query(`SELECT name FROM ${schema}.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ? ORDER BY name`).all(MIGRATION_LOG_TABLE).map((row) => row.name);
1958
+ }
1959
+ function columnNames(db, schema, table) {
1960
+ return db.query("SELECT name FROM pragma_table_info(?, ?)").all(table, schema).map((row) => row.name);
1961
+ }
1962
+ function readOnlySchema(path) {
1963
+ const db = new Database2(path, { readonly: true });
1964
+ try {
1965
+ return readStoreSchema(db, path);
1966
+ } finally {
1967
+ db.close();
1968
+ }
1969
+ }
1970
+ function refuseRemoved(db, known, live) {
1971
+ for (const table of known) {
1972
+ if (!live.has(table)) {
1973
+ throw new RebuildRefusal(`the store holds no table ${table}, which this rafa writes; its newer schema is not additive`);
1974
+ }
1975
+ const liveColumns = new Set(columnNames(db, LIVE, table));
1976
+ const missing = columnNames(db, "main", table).filter((column) => !liveColumns.has(column));
1977
+ if (missing.length > 0) {
1978
+ throw new RebuildRefusal(`the store holds no column ${missing.map((column) => `${table}.${column}`).join(", ")}, which this rafa` + " writes; its newer schema is not additive");
1979
+ }
1980
+ }
1981
+ }
1982
+ function copyKnown(db, known) {
1983
+ db.transaction(() => {
1984
+ for (const table of known) {
1985
+ const columns = columnNames(db, "main", table).map(quoted).join(", ");
1986
+ db.run(`INSERT INTO main.${quoted(table)} (${columns}) SELECT ${columns} FROM ${LIVE}.${quoted(table)}`);
1987
+ }
1988
+ })();
1989
+ }
1990
+ function leftBehind(db, known, live) {
1991
+ const leftTables = live.filter((table) => !known.has(table)).map((table) => ({ table, rows: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)}`) }));
1992
+ const leftColumns = live.filter((table) => known.has(table)).flatMap((table) => {
1993
+ const kept = new Set(columnNames(db, "main", table));
1994
+ return columnNames(db, LIVE, table).filter((column) => !kept.has(column)).map((column) => ({
1995
+ table,
1996
+ column,
1997
+ values: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)} WHERE ${quoted(column)} IS NOT NULL`)
1998
+ }));
1999
+ });
2000
+ return { leftTables, leftColumns };
2001
+ }
2002
+ function refuseUnusable(db, parallelPath, migrations) {
2003
+ const plan = planSchema(readStoreSchema(db, parallelPath), sqliteCatalogue(migrations), "write", "open");
2004
+ if (plan.verdict === "refuse")
2005
+ throw new RebuildRefusal(`the rebuilt store is refused as ${plan.reason}: ${plan.message}`);
2006
+ const needs = writeNames(plan);
2007
+ if (needs.length > 0)
2008
+ throw new RebuildRefusal(`the rebuilt store still needs ${needs.join(", ")}`);
2009
+ }
2010
+ function buildParallel(path, parallelPath, migrations, stamp) {
2011
+ const db = new Database2(parallelPath, { create: true, readwrite: true });
2012
+ try {
2013
+ bringForward(db, parallelPath, "write", "open", { migrations, ...stamp, builtAside: true });
2014
+ db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
2015
+ const known = tableNames(db, "main");
2016
+ const live = tableNames(db, LIVE);
2017
+ refuseRemoved(db, known, new Set(live));
2018
+ copyKnown(db, known);
2019
+ const kept = checkedCounts(db, known, LIVE);
2020
+ const left = leftBehind(db, new Set(known), live);
2021
+ db.run(`DETACH DATABASE ${LIVE}`);
2022
+ refuseCorrupt(db);
2023
+ refuseUnusable(db, parallelPath, migrations);
2024
+ return { kept, ...left };
2025
+ } finally {
2026
+ db.close();
2027
+ }
2028
+ }
2029
+ function asidePaths(path, version2, stamp) {
2030
+ return { parallelPath: `${path}.fix-${stamp}`, backupPath: `${path}.v${String(version2)}-${stamp}.bak` };
2031
+ }
2032
+ function isRepairable(reason) {
2033
+ return REPAIRABLE_REASONS.includes(reason);
2034
+ }
2035
+ function refuseDevelopmentSwap(path, reason, identity) {
2036
+ if (identity.kind !== "development")
2037
+ return;
2038
+ throw new RebuildRefusal(`${path} needs a rebuild (${reason}), and this rafa is a development build (${identity.checkout}), whose` + " migrations may be a branch's; a development build never swaps a rebuild in. Nothing was changed, and" + " --dry-run runs from here. Run the swap from an installed rafa. Next safe step: rafa effort fix-schema");
2039
+ }
2040
+ function fixStoreSchema(options) {
2041
+ const { path, dryRun, stamp } = options;
2042
+ guardTestProcess(path);
2043
+ const migrations = options.migrations ?? SQLITE_MIGRATIONS;
2044
+ const identity = options.identity ?? readRuntimeIdentity();
2045
+ const appliedBy = appliedByName(identity);
2046
+ const known = migrations.map(({ id }) => id);
2047
+ const nothing = { known, appliedBy, pending: [], kept: [], leftTables: [], leftColumns: [], backupPath: null };
2048
+ if (!existsSync7(path)) {
2049
+ return { path, status: "missing", storeVersion: null, logged: false, reason: null, refusal: null, unknown: [], ...nothing };
2050
+ }
2051
+ refuseInFlight(path);
2052
+ const store = readOnlySchema(path);
2053
+ const knownIds = new Set(known);
2054
+ const found = {
2055
+ path,
2056
+ storeVersion: store.userVersion,
2057
+ logged: store.log !== null,
2058
+ unknown: (store.log ?? []).filter(({ id }) => !knownIds.has(id))
2059
+ };
2060
+ const plan = planSchema(store, sqliteCatalogue(migrations), "write", "open");
2061
+ if (plan.verdict === "use") {
2062
+ const pending = writeNames(plan);
2063
+ const status2 = pending.length === 0 ? "current" : "behind";
2064
+ return { ...found, ...nothing, status: status2, reason: null, refusal: null, pending };
2065
+ }
2066
+ if (!isRepairable(plan.reason)) {
2067
+ throw new RebuildRefusal(`a rebuild at this rafa's migrations does not repair ${plan.reason}. ${plan.message}`);
2068
+ }
2069
+ if (!dryRun)
2070
+ refuseDevelopmentSwap(path, plan.reason, identity);
2071
+ const { built, backupPath } = rebuildAside({
2072
+ path,
2073
+ ...asidePaths(path, store.userVersion, stamp),
2074
+ dryRun,
2075
+ build: (parallelPath) => buildParallel(path, parallelPath, migrations, {
2076
+ appliedBy,
2077
+ now: options.now ?? (() => new Date)
2078
+ })
2079
+ });
2080
+ const status = dryRun ? "would-rebuild" : "rebuilt";
2081
+ const refusal = withoutWayOut(plan.message, plan.nextStep);
2082
+ return { ...found, ...nothing, status, reason: plan.reason, refusal, ...built, backupPath };
2083
+ }
2084
+ function withoutWayOut(message, nextStep) {
2085
+ const ending = ` Next safe step: ${nextStep}`;
2086
+ return message.endsWith(ending) ? message.slice(0, -ending.length) : message;
2087
+ }
2088
+
2089
+ // src/commands/plan/plan-files.ts
2090
+ import { statSync as statSync2 } from "fs";
2091
+ import { resolve as resolve3 } from "path";
2092
+
2093
+ // src/config-load.ts
2094
+ import { existsSync as existsSync8, readFileSync as readFileSync7, realpathSync as realpathSync5 } from "fs";
2095
+ import { isAbsolute as isAbsolute6 } from "path";
2096
+ function readConfigFile(root) {
2097
+ const path = configFilePath(root);
2098
+ if (!existsSync8(path))
2099
+ return null;
2100
+ let text;
2101
+ try {
2102
+ text = readFileSync7(path, "utf8");
2103
+ } catch (error) {
2104
+ const problem = `${path}: cannot be read (${messageOf(error)})`;
2105
+ throw new ConfigError([problem], { cause: error });
2106
+ }
2107
+ return parseConfigText(text, path);
2108
+ }
2109
+ function isSameDirectory(first, second) {
2110
+ try {
2111
+ return realpathSync5(first) === realpathSync5(second);
2112
+ } catch {
2113
+ return false;
2114
+ }
2115
+ }
2116
+ function readUserConfigFile({ root, home }) {
2117
+ if (!isAbsolute6(home)) {
2118
+ throw new ConfigError([
2119
+ `home directory is ${describeValue(home)}, expected an absolute path`
2120
+ ]);
2121
+ }
2122
+ return isSameDirectory(root, home) ? null : readConfigFile(home);
2123
+ }
2124
+ function printWarning(message) {
2125
+ activeOutput().warn(message);
2126
+ }
2127
+ function loadConfig(roots, cli = {}, warn = printWarning) {
2128
+ const user = readUserConfigFile(roots);
2129
+ const file = readConfigFile(roots.root);
2130
+ const resolved = resolveConfig({ cli, file, user });
2131
+ for (const warning of resolved.warnings)
2132
+ warn(warning);
2133
+ setActiveStoreSettings({ busyTimeoutMs: resolved.config.effortBusyTimeoutMs });
2134
+ return resolved;
2135
+ }
2136
+
2137
+ // src/commands/plan/plan-files.ts
2138
+ function plansDirAt(root, planDir) {
2139
+ return { label: planDir, path: resolve3(root, planDir) };
2140
+ }
2141
+ function requireProject(context, command) {
2142
+ if (context.project === null)
2143
+ throw new Error(`${command} runs inside a project, and was handed none`);
2144
+ return context.project;
2145
+ }
2146
+ function resolvePlansDir(project, command, warn) {
2147
+ return plansDirAt(project.root, resolveProjectConfig(project, command, warn).planDir);
2148
+ }
2149
+ function resolveProjectConfig(project, command, warn) {
2150
+ try {
2151
+ return loadConfig({ root: project.root, home: project.home }, {}, warn).config;
2152
+ } catch (error) {
2153
+ if (!(error instanceof ConfigError))
2154
+ throw error;
2155
+ throw new CommandExit(1, [
2156
+ `\u274C ${command}: the config cannot be used:`,
2157
+ ...error.problems.map((problem) => ` ${problem}`)
2158
+ ].join(`
2159
+ `));
2160
+ }
2161
+ }
2162
+ var CHECKBOXES = {
2163
+ unchecked: "[ ]",
2164
+ blocked: "[BLOCKED]",
2165
+ done: "[x]"
2166
+ };
2167
+ function countTasks(tasks) {
2168
+ const done = tasks.filter((task) => task.status === "done").length;
2169
+ const blocked = tasks.filter((task) => task.status === "blocked").length;
2170
+ return { total: tasks.length, done, blocked, open: tasks.length - done - blocked };
2171
+ }
2172
+ function formatCounts(counts) {
2173
+ return `${counts.done}/${counts.total} done, ${counts.blocked} blocked, ${counts.open} open`;
2174
+ }
2175
+ function checkbox(status) {
2176
+ return CHECKBOXES[status];
2177
+ }
2178
+ function planFileName(stub, tracker) {
2179
+ return tracker ? `PLAN_TRACKER-${stub}.md` : `PLAN-${stub}.md`;
2180
+ }
2181
+ function stubOfPlanFile(name2) {
2182
+ const stub = /^PLAN-(.+)\.md$/.exec(name2)?.[1];
2183
+ return stub !== undefined && isStampableStub(stub) ? stub : null;
2184
+ }
2185
+ function isFile(path) {
2186
+ return statSync2(path, { throwIfNoEntry: false })?.isFile() ?? false;
2187
+ }
2188
+ function issueLine(file, issue) {
2189
+ return `${file}:${issue.line}: ${issue.reason}: ${issue.text}`;
2190
+ }
2191
+ function plural(count2, noun) {
2192
+ return count2 === 1 ? `${count2} ${noun}` : `${count2} ${noun}s`;
2193
+ }
2194
+ function expectNoArgument(args, usage) {
2195
+ if (args.length === 0)
2196
+ return;
2197
+ throw new CommandExit(1, `\u274C Expected no argument, got ${args.length}: ${args.join(" ")}
2198
+ Usage: ${usage}`);
2199
+ }
2200
+ function expectOneArgument(args, usage) {
2201
+ const [only] = args;
2202
+ if (args.length === 1 && only !== undefined)
2203
+ return only;
2204
+ const got = args.length === 0 ? "none" : `${args.length}: ${args.join(" ")}`;
2205
+ throw new CommandExit(1, `\u274C Expected one argument, got ${got}
2206
+ Usage: ${usage}`);
2207
+ }
2208
+ function expectAtMostOneArgument(args, usage) {
2209
+ const [only] = args;
2210
+ if (only === undefined)
2211
+ return null;
2212
+ if (args.length === 1)
2213
+ return only;
2214
+ throw new CommandExit(1, `\u274C Expected at most one argument, got ${args.length}: ${args.join(" ")}
2215
+ Usage: ${usage}`);
2216
+ }
2217
+ function readSwitch(name2, value, hint) {
2218
+ if (value === undefined || value === false || value === "false")
2219
+ return false;
2220
+ if (value === true || value === "true")
2221
+ return true;
2222
+ throw new CommandExit(1, `\u274C --${name2} takes no value, and read "${value}" as one. ${hint}`);
2223
+ }
2224
+
2225
+ // src/commands/effort/fix-schema.ts
2226
+ var COMMAND_NAME = "rafa effort fix-schema";
2227
+ var FIX_SCHEMA_USAGE = "rafa effort fix-schema [--dry-run]";
2228
+ var DRY_RUN_FLAG = "dry-run";
2229
+ function fileStamp(date) {
2230
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
2231
+ }
2232
+ function refuseLiveLoop(root, isAlive) {
2233
+ let records;
2234
+ try {
2235
+ records = readSessions(root, isAlive === undefined ? {} : { isAlive });
2236
+ } catch (error) {
2237
+ throw new CommandExit(1, `\u274C ${COMMAND_NAME}: the loop's run records cannot be read, so a live loop cannot be ruled out: ${String(error)}`);
2238
+ }
2239
+ const live = records.find((record) => record.state === "running" || record.state === "paused");
2240
+ if (live === undefined)
2241
+ return;
2242
+ throw new CommandExit(1, `\u274C ${COMMAND_NAME}: loop session ${live.sessionId} on ${live.branch} is ${live.state} and writes the store;` + ` stop it with \`rafa loop stop -s ${live.sessionId}\` first. \`--dry-run\` runs beside it.`);
2243
+ }
2244
+ function migrationNames(result) {
2245
+ return result.unknown.map(({ id, appliedBy }) => `${id} (applied by ${appliedBy})`).join(", ");
2246
+ }
2247
+ function rebuildLines(result) {
2248
+ const rows = result.kept.reduce((sum, table) => sum + table.rows, 0);
2249
+ const kept = result.kept.map((table) => `${table.table} ${String(table.rows)}`).join(", ");
2250
+ const left = [
2251
+ ...result.unknown.map(({ id, appliedBy }) => `migration ${id} (applied by ${appliedBy})`),
2252
+ ...result.leftTables.map((table) => `table ${table.table} (${String(table.rows)} rows)`),
2253
+ ...result.leftColumns.map((column) => `column ${column.table}.${column.column} (${String(column.values)} values)`)
2254
+ ];
2255
+ const newer = result.unknown.length === 0 ? [] : [`A rafa that knows ${result.unknown.map(({ id }) => id).join(", ")} uses the store as it is and keeps that data` + " in it; install one instead where you can."];
2256
+ return [
2257
+ `Refused as ${String(result.reason)}: ${String(result.refusal)}`,
2258
+ `Rebuilds it at the ${String(result.known.length)} migrations this rafa knows, logged as applied by ${result.appliedBy}.`,
2259
+ `Keeps ${String(result.kept.length)} tables, ${String(rows)} rows: ${kept}.`,
2260
+ left.length === 0 ? "Leaves nothing behind." : `Leaves behind, kept only in the backup: ${left.join("; ")}.`,
2261
+ ...newer
2262
+ ];
2263
+ }
2264
+ function unknownAdditiveLines(result) {
2265
+ return result.unknown.length === 0 ? [] : [`It logs ${migrationNames(result)}, which this rafa does not know; each is additive, so this rafa uses the store` + " through them."];
2266
+ }
2267
+ function renderFixSchema(result) {
2268
+ switch (result.status) {
2269
+ case "missing":
2270
+ return [`No effort store at ${result.path}: nothing to repair.`];
2271
+ case "current":
2272
+ return [
2273
+ `\u2705 ${result.path} is current: this rafa reads and writes it as it is. Nothing to repair.`,
2274
+ ...unknownAdditiveLines(result)
2275
+ ];
2276
+ case "behind":
2277
+ return [
2278
+ `${result.path} is behind this rafa's migrations; the next command that opens it records` + ` ${result.pending.join(", ")}. Nothing to repair.`,
2279
+ ...unknownAdditiveLines(result)
2280
+ ];
2281
+ case "would-rebuild":
2282
+ return [
2283
+ ...rebuildLines(result),
2284
+ "\uD83D\uDD0D Dry run: the rebuild was built, checked (row counts, integrity_check, this rafa's schema plan) and" + " deleted. Run without `--dry-run` to swap it in."
2285
+ ];
2286
+ case "rebuilt":
2287
+ return [
2288
+ ...rebuildLines(result),
2289
+ `\u2705 Rebuilt at this rafa's migrations. The original is kept whole at ${String(result.backupPath)};` + " rename it back to undo."
2290
+ ];
2291
+ }
2292
+ }
2293
+ function runFixSchema(context, seams) {
2294
+ expectNoArgument(context.args, FIX_SCHEMA_USAGE);
2295
+ const dryRun = readSwitch(DRY_RUN_FLAG, context.flags[DRY_RUN_FLAG], `Usage: ${FIX_SCHEMA_USAGE}`);
2296
+ if (context.project === null)
2297
+ throw new Error(`${COMMAND_NAME} runs inside a project, and was handed none`);
2298
+ const root = context.project.root;
2299
+ if (!dryRun)
2300
+ refuseLiveLoop(root, seams.isAlive);
2301
+ const now = seams.now ?? (() => new Date);
2302
+ let result;
2303
+ try {
2304
+ result = fixStoreSchema({
2305
+ path: sqliteStorePath(root),
2306
+ dryRun,
2307
+ stamp: fileStamp(now()),
2308
+ now,
2309
+ ...seams.identity === undefined ? {} : { identity: seams.identity }
2310
+ });
2311
+ } catch (error) {
2312
+ if (error instanceof RebuildRefusal)
2313
+ throw new CommandExit(1, `\u274C ${COMMAND_NAME}: ${error.message}`);
2314
+ throw error;
2315
+ }
2316
+ if (context.outputMode === "json") {
2317
+ context.output.result(result);
2318
+ return;
2319
+ }
2320
+ for (const line of renderFixSchema(result))
2321
+ context.output.info(line);
2322
+ }
2323
+ function createFixSchemaCommand(seams = {}) {
2324
+ const command = {
2325
+ name: "effort fix-schema",
2326
+ subject: "effort",
2327
+ action: "fix-schema",
2328
+ summary: "rebuild an effort store this rafa refuses at the migrations it knows, keeping the original",
2329
+ description: "Repairs `.rafa/effort/effort.sqlite`, or the store under `RAFA_EFFORT_DIR`, when this rafa refuses" + " it for a reason a rebuild repairs: no migration log past the legacy entries (pre-log-unreleased), a log" + " with a schema version it does not match (gate-mismatch), a logged checksum this rafa holds otherwise" + " (edited), or a logged migration this rafa does not know that breaks older readers or writers" + " (unknown-breaks-readers, unknown-breaks-writers). Installing a rafa that knows the store's migrations" + " comes first where one exists. It builds a parallel store beside it at this rafa's migrations, with a" + " migration log naming this rafa, copies every table and column this rafa knows, checks the row counts," + " SQLite's integrity_check and the schema plan, and lists what only the newer schema holds, unknown" + " migrations included. Then it renames the original to `effort.sqlite.v<user_version>-<stamp>.bak`," + " whole, and moves the rebuild into its place. A store this rafa uses, current or behind, or no store, is" + " left alone. It refuses, changing nothing, while a loop session under the project is running or paused," + " from a development build, while a journal beside the store shows a write in flight, when the newer" + " schema lacks a table or column this rafa writes, and on a refusal a rebuild does not repair. With" + " `--output=json` the outcome is the data of the terminal result event. Starts no session.",
2330
+ args: [],
2331
+ flags: [
2332
+ {
2333
+ name: DRY_RUN_FLAG,
2334
+ description: "Build and check the rebuild, print what it keeps and leaves behind, then delete it. The" + " live store is only read, so this runs beside a live loop and from a development build, and can be" + " repeated.",
2335
+ type: "boolean"
2336
+ }
2337
+ ],
2338
+ examples: [
2339
+ {
2340
+ cmd: "rafa effort fix-schema --dry-run",
2341
+ note: "Prints what a rebuild would keep and leave behind, changing nothing."
2342
+ },
2343
+ {
2344
+ cmd: "rafa effort fix-schema",
2345
+ note: "Swaps the rebuild in and prints where the original was kept."
2346
+ }
2347
+ ],
2348
+ outputs: ["text", "json"],
2349
+ run: async (context) => {
2350
+ runFixSchema(context, seams);
2351
+ await Promise.resolve();
2352
+ }
2353
+ };
2354
+ return Object.freeze(command);
2355
+ }
2356
+ var fix_schema_default = createFixSchemaCommand();
2357
+
2358
+ // src/effort/store/copy.ts
2359
+ import { copyFileSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync2, rmSync as rmSync5, statSync as statSync3 } from "fs";
2360
+ import { join as join10, resolve as resolve4 } from "path";
2361
+ import { Database as Database3 } from "bun:sqlite";
2362
+ var NDJSON_KINDS = { sessions: true, commits: true };
2363
+ var EFFORT_COPY_FILE_NAMES = [
2364
+ SQLITE_STORE_FILE_NAME,
2365
+ ...Object.keys(NDJSON_KINDS).map((kind) => effortStoreFileName(kind))
2366
+ ];
2367
+
2368
+ class EffortCopyRefusal extends Error {
2369
+ name = "EffortCopyRefusal";
2370
+ }
2371
+
2372
+ class EffortCopyFailure extends Error {
2373
+ name = "EffortCopyFailure";
2374
+ }
2375
+ function refuseUsedTarget(target) {
2376
+ if (!existsSync9(target))
2377
+ return;
2378
+ let entries;
2379
+ try {
2380
+ if (!statSync3(target).isDirectory()) {
2381
+ throw new EffortCopyRefusal(`${target} is a file, not a directory; name an empty or absent directory with --to=<dir>.`);
2382
+ }
2383
+ entries = readdirSync2(target);
2384
+ } catch (error) {
2385
+ if (error instanceof EffortCopyRefusal)
2386
+ throw error;
2387
+ throw new EffortCopyFailure(`${target} cannot be read: ${String(error)}`);
2388
+ }
2389
+ if (entries.length === 0)
2390
+ return;
2391
+ throw new EffortCopyRefusal(`${target} is not empty (${String(entries.length)} entries); nothing was copied.` + " Name an empty or absent directory with --to=<dir>.");
2392
+ }
2393
+ function vacuumInto(source, target) {
2394
+ const db = new Database3(source, { readonly: true });
2395
+ try {
2396
+ db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
2397
+ db.run("VACUUM INTO ?", [target]);
2398
+ } finally {
2399
+ db.close();
2400
+ }
2401
+ }
2402
+ function copyOne(name2, source, target) {
2403
+ if (name2 === SQLITE_STORE_FILE_NAME) {
2404
+ vacuumInto(join10(source, name2), join10(target, name2));
2405
+ return;
2406
+ }
2407
+ copyFileSync(join10(source, name2), join10(target, name2), constants.COPYFILE_EXCL);
2408
+ }
2409
+ function undo(target, written, madeDir) {
2410
+ try {
2411
+ if (madeDir === undefined) {
2412
+ for (const name2 of written)
2413
+ rmSync5(join10(target, name2), { force: true });
2414
+ } else {
2415
+ rmSync5(madeDir, { recursive: true, force: true });
2416
+ }
2417
+ return "";
2418
+ } catch (error) {
2419
+ return ` Removing what it made failed too: ${String(error)}`;
2420
+ }
2421
+ }
2422
+ function copyEffortStore(request) {
2423
+ const source = resolve4(request.source);
2424
+ const target = resolve4(request.target);
2425
+ guardTestProcess(source);
2426
+ guardTestProcess(target);
2427
+ const files = EFFORT_COPY_FILE_NAMES.filter((name2) => existsSync9(join10(source, name2)));
2428
+ if (files.length === 0) {
2429
+ throw new EffortCopyRefusal(`no effort store at ${source}: it holds none of ${EFFORT_COPY_FILE_NAMES.join(", ")}. Nothing to copy.`);
2430
+ }
2431
+ refuseUsedTarget(target);
2432
+ const written = [];
2433
+ let madeDir;
2434
+ try {
2435
+ madeDir = mkdirSync6(target, { recursive: true });
2436
+ for (const name2 of files) {
2437
+ written.push(name2);
2438
+ copyOne(name2, source, target);
2439
+ }
2440
+ } catch (error) {
2441
+ const left = undo(target, written, madeDir);
2442
+ throw new EffortCopyFailure(`copying ${source} to ${target} failed: ${String(error)}.${left}`);
2443
+ }
2444
+ return Object.freeze({ source, directory: target, files: Object.freeze(files) });
2445
+ }
2446
+
2447
+ // src/effort/store/merge-store.ts
2448
+ import { randomUUID as randomUUID4 } from "crypto";
2449
+ import { existsSync as existsSync11, mkdtempSync, rmSync as rmSync6 } from "fs";
2450
+ import { tmpdir as tmpdir3 } from "os";
2451
+ import { dirname as dirname10, join as join11 } from "path";
2452
+ import { Database as Database5, SQLiteError as SQLiteError2 } from "bun:sqlite";
2453
+
2454
+ // src/effort/commits.ts
2455
+ import { execFileSync } from "child_process";
2456
+ var COMMIT_RECORD_MARKER = "@@ralph-commit@@";
2457
+ var HEADER_PLACEHOLDERS = ["%H", "%aI", "%P", "%an", "%S", "%s"];
2458
+ var FIELD_SEPARATOR = "\t";
2459
+ var COMMIT_LOG_FORMAT = [
2460
+ COMMIT_RECORD_MARKER,
2461
+ ...HEADER_PLACEHOLDERS
2462
+ ].join("%x09");
2463
+ var MS_PER_MINUTE = 60000;
2464
+ var ELAPSED_DECIMALS = 3;
2465
+ function normaliseBranch(source) {
2466
+ if (source.length === 0 || source === "HEAD")
2467
+ return null;
2468
+ return source.startsWith("refs/heads/") ? source.slice("refs/heads/".length) : source;
2469
+ }
2470
+ function parseStatCount(field2) {
2471
+ return /^\d+$/.test(field2) ? Number(field2) : null;
2472
+ }
2473
+ function parseCommitHeader(line) {
2474
+ const parts = line.split(FIELD_SEPARATOR);
2475
+ if (parts[0] !== COMMIT_RECORD_MARKER)
2476
+ return null;
2477
+ if (parts.length <= HEADER_PLACEHOLDERS.length)
2478
+ return null;
2479
+ const [sha, timestamp, parents, author, source] = parts.slice(1);
2480
+ if (sha === undefined || sha.length === 0)
2481
+ return null;
2482
+ return {
2483
+ sha,
2484
+ timestamp: timestamp ?? "",
2485
+ subject: parts.slice(HEADER_PLACEHOLDERS.length).join(FIELD_SEPARATOR),
2486
+ author: author ?? "",
2487
+ branch: normaliseBranch(source ?? ""),
2488
+ filesChanged: 0,
2489
+ insertions: 0,
2490
+ deletions: 0,
2491
+ parentCount: countParents(parents ?? ""),
2492
+ minutesSincePrevious: null
2493
+ };
2494
+ }
2495
+ function countParents(parents) {
2496
+ return parents.trim().length === 0 ? 0 : parents.trim().split(/ +/).length;
2497
+ }
2498
+ function applyStatLine(row, line) {
2499
+ const parts = line.split(FIELD_SEPARATOR);
2500
+ if (parts.length < 3)
2501
+ return false;
2502
+ const added = parts[0] ?? "";
2503
+ const removed = parts[1] ?? "";
2504
+ const isBinary = added === "-" && removed === "-";
2505
+ const addedCount = parseStatCount(added);
2506
+ const removedCount = parseStatCount(removed);
2507
+ if (!isBinary && (addedCount === null || removedCount === null)) {
2508
+ return false;
2509
+ }
2510
+ row.filesChanged += 1;
2511
+ row.insertions += addedCount ?? 0;
2512
+ row.deletions += removedCount ?? 0;
2513
+ return true;
2514
+ }
2515
+ function minutesBetween(earlier, later) {
2516
+ const from = Date.parse(earlier);
2517
+ const to = Date.parse(later);
2518
+ if (Number.isNaN(from) || Number.isNaN(to))
2519
+ return null;
2520
+ const minutes = (to - from) / MS_PER_MINUTE;
2521
+ return Number(minutes.toFixed(ELAPSED_DECIMALS));
2522
+ }
2523
+ function assignElapsedMinutes(rows) {
2524
+ const ordered = rows.map((row, index) => ({ index, epoch: Date.parse(row.timestamp) })).filter((entry) => !Number.isNaN(entry.epoch)).sort((a, b) => a.epoch - b.epoch);
2525
+ for (let position = 1;position < ordered.length; position += 1) {
2526
+ const entry = ordered[position];
2527
+ const previous = ordered[position - 1];
2528
+ if (entry === undefined || previous === undefined)
2529
+ continue;
2530
+ const row = rows[entry.index];
2531
+ const earlier = rows[previous.index];
2532
+ if (row === undefined || earlier === undefined)
2533
+ continue;
2534
+ row.minutesSincePrevious = minutesBetween(earlier.timestamp, row.timestamp);
2535
+ }
2536
+ }
2537
+ function parseCommitLog(text) {
2538
+ const rows = [];
2539
+ let lineCount = 0;
2540
+ let unparsedLineCount = 0;
2541
+ let open = null;
2542
+ for (const rawLine of text.split(/\r?\n/)) {
2543
+ const line = rawLine.trimEnd();
2544
+ if (line.trim().length === 0)
2545
+ continue;
2546
+ lineCount += 1;
2547
+ const header = parseCommitHeader(line);
2548
+ if (header !== null) {
2549
+ rows.push(header);
2550
+ open = header;
2551
+ continue;
2552
+ }
2553
+ if (open === null || !applyStatLine(open, line)) {
2554
+ unparsedLineCount += 1;
2555
+ }
2556
+ }
2557
+ assignElapsedMinutes(rows);
2558
+ return { rows, lineCount, unparsedLineCount };
2559
+ }
2560
+ function commitLogArgs(options = {}) {
2561
+ const args = [
2562
+ "log",
2563
+ "--source",
2564
+ `--format=${COMMIT_LOG_FORMAT}`,
2565
+ "--numstat"
2566
+ ];
2567
+ if (options.since !== undefined)
2568
+ args.push(`--since=${options.since}`);
2569
+ if (options.maxCount !== undefined) {
2570
+ args.push(`--max-count=${options.maxCount}`);
2571
+ }
2572
+ if (options.revisionRange !== undefined)
2573
+ args.push(options.revisionRange);
2574
+ return args;
2575
+ }
2576
+ function readCommitLog(options = {}) {
2577
+ const stdout = execFileSync("git", commitLogArgs(options), {
2578
+ cwd: options.cwd ?? process.cwd(),
2579
+ encoding: "utf8",
2580
+ maxBuffer: 64 * 1024 * 1024
2581
+ });
2582
+ return parseCommitLog(stdout);
2583
+ }
2584
+
2585
+ // src/effort/store/merge-commit-gaps.ts
2586
+ var GAP_FIELD = "minutesSincePrevious";
2587
+ var UPDATE_GAP = "UPDATE commits SET row_json = ? WHERE seq = ?";
2588
+ function parsedObject2(text) {
2589
+ try {
2590
+ const value = JSON.parse(text);
2591
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2592
+ } catch {
2593
+ return null;
2594
+ }
2595
+ }
2596
+ function withoutGap(rowJson) {
2597
+ const row = parsedObject2(rowJson);
2598
+ if (row === null)
2599
+ return rowJson;
2600
+ const rest = Object.fromEntries(Object.entries(row).filter(([key]) => key !== GAP_FIELD));
2601
+ return JSON.stringify(rest);
2602
+ }
2603
+ function readCommits(db) {
2604
+ return db.query("SELECT seq, sha, row_json FROM commits").all().map(({ seq, sha, row_json: rowJson }) => {
2605
+ const row = parsedObject2(rowJson);
2606
+ const timestamp = typeof row?.timestamp === "string" ? row.timestamp : null;
2607
+ return { seq, sha, row, timestamp, epoch: timestamp === null ? Number.NaN : Date.parse(timestamp) };
2608
+ });
2609
+ }
2610
+ function timeOrder(entries) {
2611
+ return entries.filter((entry) => !Number.isNaN(entry.epoch)).sort((left, right) => left.epoch - right.epoch || compareText(left.sha, right.sha));
2612
+ }
2613
+ function compareText(left, right) {
2614
+ if (left === right)
2615
+ return 0;
2616
+ return left < right ? -1 : 1;
2617
+ }
2618
+ function storedGap(entry) {
2619
+ const value = entry.row?.[GAP_FIELD];
2620
+ return typeof value === "number" ? value : null;
2621
+ }
2622
+ function namedRows(inserted, positions, order) {
2623
+ const position = positions.get(inserted.seq);
2624
+ if (position === undefined)
2625
+ return [inserted.seq];
2626
+ const next = order[position + 1];
2627
+ return next === undefined ? [inserted.seq] : [inserted.seq, next.seq];
2628
+ }
2629
+ function recomputedGap(entry, positions, order) {
2630
+ const position = positions.get(entry.seq);
2631
+ if (position === undefined || position === 0)
2632
+ return null;
2633
+ const previous = order[position - 1];
2634
+ if (previous?.timestamp == null || entry.timestamp === null)
2635
+ return null;
2636
+ return minutesBetween(previous.timestamp, entry.timestamp);
2637
+ }
2638
+ function recomputeCommitGaps(db, inserted) {
2639
+ return db.transaction(() => {
2640
+ const entries = readCommits(db);
2641
+ const bySeq = new Map(entries.map((entry) => [entry.seq, entry]));
2642
+ const order = timeOrder(entries);
2643
+ const positions = new Map(order.map((entry, position) => [entry.seq, position]));
2644
+ const named = new Set(inserted.flatMap((seq) => {
2645
+ const entry = bySeq.get(seq);
2646
+ return entry === undefined ? [] : namedRows(entry, positions, order);
2647
+ }));
2648
+ 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));
2649
+ return targets.flatMap((entry) => {
2650
+ const before = storedGap(entry);
2651
+ const after = recomputedGap(entry, positions, order);
2652
+ if (before === after && entry.row !== null && Object.hasOwn(entry.row, GAP_FIELD))
2653
+ return [];
2654
+ db.query(UPDATE_GAP).run(JSON.stringify({ ...entry.row, [GAP_FIELD]: after }), entry.seq);
2655
+ return [{ seq: entry.seq, sha: entry.sha, before, after }];
2656
+ });
2657
+ })();
2658
+ }
2659
+
2660
+ // src/effort/store/merge-rules.ts
2661
+ var SET_ONCE = "set-once";
2662
+ var RECOMPUTED = "recomputed";
2663
+ var APPEND_ONLY = {};
2664
+ function appendOnly(...identity) {
2665
+ return { scope: "merged", identity, edited: APPEND_ONLY };
2666
+ }
2667
+ var MERGE_RULES = {
2668
+ sessions: appendOnly("session_id"),
2669
+ commits: { scope: "merged", identity: ["sha"], edited: { row_json: RECOMPUTED } },
2670
+ findings: { scope: "merged", identity: ["id"], edited: { tracker_ref: SET_ONCE } },
2671
+ blockers: appendOnly("id"),
2672
+ out_of_scope_bugs: appendOnly("id"),
2673
+ report_absences: appendOnly("id"),
2674
+ task_reports: appendOnly("id"),
2675
+ preflight: appendOnly("run_id", "position"),
2676
+ dispatches: appendOnly("session_id"),
2677
+ changes: appendOnly("id"),
2678
+ skill_invocations: appendOnly("session_id", "name", "sidechain"),
2679
+ plan_ci: appendOnly("pr", "head_sha", "read_at"),
2680
+ schema_migrations: { scope: "local" },
2681
+ store_meta: { scope: "local" },
2682
+ merges: { scope: "local" },
2683
+ merge_conflicts: { scope: "local" }
2684
+ };
2685
+
2686
+ // src/effort/store/merge-union.ts
2687
+ import { SQLiteError } from "bun:sqlite";
2688
+ class UnionSchemaMismatch extends Error {
2689
+ constructor(message) {
2690
+ super(message);
2691
+ this.name = "UnionSchemaMismatch";
2692
+ }
2693
+ }
2694
+ var UNIQUE_REFUSAL = "SQLITE_CONSTRAINT_UNIQUE";
2695
+ function mergedTables() {
2696
+ return Object.entries(MERGE_RULES).flatMap(([table, rule]) => rule.scope === "merged" ? [{ table, identity: rule.identity }] : []);
2697
+ }
2698
+ function carriedColumns(db, table) {
2699
+ return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name: name2 }) => name2).filter((name2) => name2 !== "seq");
2700
+ }
2701
+ function refuseColumnMismatch(local, other, tables) {
2702
+ const faults = tables.flatMap(({ table }) => {
2703
+ const here = new Set(carriedColumns(local, table));
2704
+ const there = new Set(carriedColumns(other, table));
2705
+ const onlyHere = [...here].filter((column) => !there.has(column)).map((column) => `${table}.${column} only here`);
2706
+ const onlyThere = [...there].filter((column) => !here.has(column)).map((column) => `${table}.${column} only there`);
2707
+ return [...onlyHere, ...onlyThere];
2708
+ });
2709
+ if (faults.length > 0) {
2710
+ throw new UnionSchemaMismatch(`the two stores hold different columns (${faults.join(", ")}); bring both to the same migrations first`);
2711
+ }
2712
+ }
2713
+ function valueOf(row, column) {
2714
+ return row[column] ?? null;
2715
+ }
2716
+ function byOrigin(local, table, row) {
2717
+ const store = valueOf(row, "origin_store");
2718
+ const seq = valueOf(row, "origin_seq");
2719
+ if (store === null || seq === null)
2720
+ return null;
2721
+ return local.query(`SELECT seq FROM ${quoted(table)} WHERE origin_store = ? AND origin_seq = ?`).get(store, seq)?.seq ?? null;
2722
+ }
2723
+ function byIdentity(local, { table, identity }, row) {
2724
+ const where = identity.map((column) => `${quoted(column)} IS ?`).join(" AND ");
2725
+ return local.query(`SELECT seq FROM ${quoted(table)} WHERE ${where} ORDER BY seq LIMIT 1`).get(...identity.map((column) => valueOf(row, column)))?.seq ?? null;
2726
+ }
2727
+ function insertRow(local, table, columns, row, incomingSeq) {
2728
+ const columnList = columns.map(quoted).join(", ");
2729
+ const placeholders = columns.map(() => "?").join(", ");
2730
+ const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${quoted(table)})`;
2731
+ try {
2732
+ const inserted = local.query(`INSERT INTO ${quoted(table)} (seq, ${columnList}) VALUES (${nextSeq}, ${placeholders}) RETURNING seq`).get(...columns.map((column) => valueOf(row, column)));
2733
+ if (inserted === null)
2734
+ throw new Error(`the insert into ${table} returned no seq`);
2735
+ return { kind: "added", localSeq: inserted.seq };
2736
+ } catch (error) {
2737
+ if (!(error instanceof SQLiteError) || error.code !== UNIQUE_REFUSAL)
2738
+ throw error;
2739
+ return { kind: "collided", collision: { incomingSeq, reason: error.message } };
2740
+ }
2741
+ }
2742
+ function placeRow(local, merged, columns, row) {
2743
+ const incomingSeq = Number(valueOf(row, "seq"));
2744
+ const origin = byOrigin(local, merged.table, row);
2745
+ if (origin !== null)
2746
+ return { kind: "matched", match: { localSeq: origin, incomingSeq, by: "origin" } };
2747
+ const identity = byIdentity(local, merged, row);
2748
+ if (identity !== null)
2749
+ return { kind: "matched", match: { localSeq: identity, incomingSeq, by: "identity" } };
2750
+ return insertRow(local, merged.table, columns, row, incomingSeq);
2751
+ }
2752
+ function unionTable(local, other, merged) {
2753
+ const columns = carriedColumns(local, merged.table);
2754
+ const incoming = other.query(`SELECT seq, ${columns.map(quoted).join(", ")} FROM ${quoted(merged.table)} ORDER BY seq`).all();
2755
+ const outcomes = incoming.map((row) => placeRow(local, merged, columns, row));
2756
+ return {
2757
+ table: merged.table,
2758
+ added: outcomes.flatMap((outcome) => outcome.kind === "added" ? [outcome.localSeq] : []),
2759
+ matched: outcomes.flatMap((outcome) => outcome.kind === "matched" ? [outcome.match] : []),
2760
+ collided: outcomes.flatMap((outcome) => outcome.kind === "collided" ? [outcome.collision] : [])
2761
+ };
2762
+ }
2763
+ function unionStores(local, other) {
2764
+ const tables = mergedTables();
2765
+ refuseColumnMismatch(local, other, tables);
2766
+ return local.transaction(() => tables.map((merged) => unionTable(local, other, merged)))();
2767
+ }
2768
+
2769
+ // src/effort/store/merge-conflicts.ts
2770
+ var SET_ONCE_FILLS = {
2771
+ "findings.tracker_ref": "UPDATE findings SET tracker_ref = ? WHERE seq = ? AND tracker_ref IS NULL"
2772
+ };
2773
+ var RECOMPUTED_COMPARISONS = {
2774
+ "commits.row_json": withoutGap
2775
+ };
2776
+ var NOT_CONTENT = new Set(["seq", "origin_store", "origin_seq"]);
2777
+ var INSERT_CONFLICT = `INSERT INTO merge_conflicts (merge_id, table_name, local_seq, field, incoming, recorded_at)
2778
+ VALUES (?, ?, ?, ?, ?, ?)`;
2779
+ function rowAt(db, table, columns, seq, side) {
2780
+ const row = db.query(`SELECT seq, ${columns.map(quoted).join(", ")} FROM ${quoted(table)} WHERE seq = ?`).get(seq);
2781
+ if (row === null)
2782
+ throw new Error(`the union matched ${table} row ${seq} ${side}, which that store does not hold`);
2783
+ return row;
2784
+ }
2785
+ function sameValue(left, right) {
2786
+ if (left instanceof Uint8Array && right instanceof Uint8Array)
2787
+ return Buffer.from(left).equals(right);
2788
+ return (left ?? null) === (right ?? null);
2789
+ }
2790
+ function fillStatement(table, field2) {
2791
+ const statement = SET_ONCE_FILLS[`${table}.${field2}`];
2792
+ if (statement === undefined) {
2793
+ throw new Error(`${table}.${field2} is set-once in MERGE_RULES and has no statement in SET_ONCE_FILLS`);
2794
+ }
2795
+ return statement;
2796
+ }
2797
+ function settleSetOnce(local, table, field2, here, there) {
2798
+ const incoming = there[field2] ?? null;
2799
+ if (incoming === null)
2800
+ return { skipped: true, filled: [], conflictFields: [] };
2801
+ if ((here[field2] ?? null) !== null)
2802
+ return { skipped: false, filled: [], conflictFields: [field2] };
2803
+ local.query(fillStatement(table, field2)).run(incoming, Number(here.seq));
2804
+ return { skipped: true, filled: [field2], conflictFields: [] };
2805
+ }
2806
+ function settleRecomputed(table, field2, here, there) {
2807
+ const comparable = RECOMPUTED_COMPARISONS[`${table}.${field2}`];
2808
+ if (comparable === undefined) {
2809
+ throw new Error(`${table}.${field2} is recomputed in MERGE_RULES and has no entry in RECOMPUTED_COMPARISONS`);
2810
+ }
2811
+ const hereValue = here[field2];
2812
+ const thereValue = there[field2];
2813
+ const same = typeof hereValue === "string" && typeof thereValue === "string" && comparable(hereValue) === comparable(thereValue);
2814
+ return same ? { skipped: true, filled: [], conflictFields: [] } : { skipped: false, filled: [], conflictFields: [null] };
2815
+ }
2816
+ function settlePair(local, table, edited, here, there) {
2817
+ const differing = Object.keys(here).filter((column) => !NOT_CONTENT.has(column) && !sameValue(here[column], there[column]));
2818
+ if (differing.length === 0)
2819
+ return { skipped: true, filled: [], conflictFields: [] };
2820
+ if (differing.some((column) => !Object.hasOwn(edited, column)))
2821
+ return { skipped: false, filled: [], conflictFields: [null] };
2822
+ const outcomes = differing.map((field2) => {
2823
+ if (edited[field2] === SET_ONCE)
2824
+ return settleSetOnce(local, table, field2, here, there);
2825
+ if (edited[field2] === RECOMPUTED)
2826
+ return settleRecomputed(table, field2, here, there);
2827
+ throw new Error(`${table}.${field2} has the rule ${edited[field2]}, which the merge does not apply`);
2828
+ });
2829
+ return {
2830
+ skipped: outcomes.every((outcome) => outcome.skipped),
2831
+ filled: outcomes.flatMap((outcome) => outcome.filled),
2832
+ conflictFields: outcomes.flatMap((outcome) => outcome.conflictFields)
2833
+ };
2834
+ }
2835
+ function settleTable(local, other, union, trail) {
2836
+ const rule = MERGE_RULES[union.table];
2837
+ if (rule?.scope !== "merged")
2838
+ throw new Error(`${union.table} is not a merged table in MERGE_RULES`);
2839
+ const columns = carriedColumns(local, union.table);
2840
+ const settled = union.matched.map((match) => {
2841
+ const here = rowAt(local, union.table, columns, match.localSeq, "here");
2842
+ const there = rowAt(other, union.table, columns, match.incomingSeq, "in the other store");
2843
+ const outcome = settlePair(local, union.table, rule.edited, here, there);
2844
+ const conflicts = outcome.conflictFields.map((field2) => {
2845
+ local.query(INSERT_CONFLICT).run(trail.mergeId, union.table, match.localSeq, field2, JSON.stringify(there), trail.recordedAt);
2846
+ return { localSeq: match.localSeq, incomingSeq: match.incomingSeq, field: field2 };
2847
+ });
2848
+ return { match, outcome, conflicts };
2849
+ });
2850
+ return {
2851
+ table: union.table,
2852
+ skipped: settled.flatMap(({ match, outcome }) => outcome.skipped ? [match.localSeq] : []),
2853
+ filled: settled.flatMap(({ match, outcome }) => outcome.filled.map((field2) => ({ localSeq: match.localSeq, field: field2 }))),
2854
+ conflicts: settled.flatMap(({ conflicts }) => conflicts)
2855
+ };
2856
+ }
2857
+ function settleMatches(local, other, unions, trail) {
2858
+ return local.transaction(() => unions.map((union) => settleTable(local, other, union, trail)))();
2859
+ }
2860
+
2861
+ // src/effort/store/migrate.ts
2862
+ import { existsSync as existsSync10 } from "fs";
2863
+ import { Database as Database4 } from "bun:sqlite";
2864
+ var LIVE2 = "live";
2865
+ function asidePaths2(path, first, stamp) {
2866
+ return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
2867
+ }
2868
+ function liveLoop(path, options, nothingDone = "Nothing was migrated.") {
2869
+ const root = storeProjectRoot(path);
2870
+ if (root === null)
2871
+ return;
2872
+ let records;
2873
+ try {
2874
+ records = readSessions(root, { isAlive: options.isAlive ?? isPidAlive });
2875
+ } catch (error) {
2876
+ throw new RebuildRefusal(`the loop's run records cannot be read, so a live loop on ${path} cannot be ruled out` + ` (${messageOf(error)}). ${nothingDone}`);
2877
+ }
2878
+ return records.find((record) => record.state === "running" || record.state === "paused");
2879
+ }
2880
+ function refuseLiveLoop2(path, breaking, options) {
2881
+ const loop = liveLoop(path, options);
2882
+ if (loop === undefined)
2883
+ return;
2884
+ const named = `loop ${loop.sessionId} (pid ${String(loop.pid)}, plan ${loop.planStub ?? loop.plan}) is running on this store`;
2885
+ const [first] = breaking;
2886
+ const opening = first === undefined ? `REFUSED \u2014 ${named}, and swapping the migrated store in under it would lose what it writes.` : `REFUSED \u2014 migration ${first} breaks older runtimes, and ${named}.`;
2887
+ throw new RebuildRefusal(`${opening} Nothing was migrated. Finish or stop that loop, then run it again.`);
2888
+ }
2889
+ function buildMigrated(path, parallelPath, migrations, stamp) {
2890
+ vacuumInto(path, parallelPath);
2891
+ const db = new Database4(parallelPath, { readwrite: true });
2892
+ try {
2893
+ const brought = bringForward(db, parallelPath, "write", "migrate", { migrations, ...stamp, builtAside: true });
2894
+ db.run(`ATTACH DATABASE ? AS ${LIVE2}`, [path]);
2895
+ const live = new Set(tableNames(db, LIVE2));
2896
+ const counted = checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE2);
2897
+ db.run(`DETACH DATABASE ${LIVE2}`);
2898
+ refuseCorrupt(db);
2899
+ refuseUnusable(db, parallelPath, migrations);
2900
+ return { userVersion: brought.userVersion, counted };
2901
+ } finally {
2902
+ db.close();
2903
+ }
2904
+ }
2905
+ function migrateStore(options) {
2906
+ const { path, dryRun, stamp } = options;
2907
+ guardTestProcess(path);
2908
+ const migrations = options.migrations ?? SQLITE_MIGRATIONS;
2909
+ const appliedBy = appliedByName(options.identity ?? readRuntimeIdentity());
2910
+ const nothing = { applying: [], breaking: [], unknown: [], userVersion: null, counted: [], backupPath: null };
2911
+ if (!existsSync10(path)) {
2912
+ return { path, status: "missing", storeVersion: null, logged: false, appliedBy, ...nothing };
2913
+ }
2914
+ refuseInFlight(path);
2915
+ const store = readOnlySchema(path);
2916
+ const found = { path, storeVersion: store.userVersion, logged: store.log !== null, appliedBy };
2917
+ const plan = planSchema(store, sqliteCatalogue(migrations), "write", "migrate");
2918
+ if (plan.verdict === "refuse")
2919
+ throw new RebuildRefusal(plan.message);
2920
+ const applying = writeNames(plan);
2921
+ const planned = {
2922
+ applying,
2923
+ breaking: plan.pending.filter(({ breaks }) => breaks.length > 0).map(({ id }) => id),
2924
+ unknown: plan.unknown
2925
+ };
2926
+ if (applying.length === 0)
2927
+ return { ...found, ...nothing, ...planned, status: "current" };
2928
+ if (!dryRun) {
2929
+ refuseUnownedDevelopmentWrite(path, applying, options);
2930
+ refuseLiveLoop2(path, planned.breaking, options);
2931
+ }
2932
+ const first = plan.pending[0]?.id ?? ADOPTION_NAME;
2933
+ const { built, backupPath } = rebuildAside({
2934
+ path,
2935
+ ...asidePaths2(path, first, stamp),
2936
+ dryRun,
2937
+ build: (parallelPath) => buildMigrated(path, parallelPath, migrations, {
2938
+ appliedBy,
2939
+ now: options.now ?? (() => new Date)
2940
+ })
2941
+ });
2942
+ const status = dryRun ? "would-migrate" : "migrated";
2943
+ return { ...found, ...planned, ...built, status, backupPath };
2944
+ }
2945
+
2946
+ // src/effort/store/merge-store.ts
2947
+ var MOVE_TO_SQLITE = "rafa effort move --to=sqlite";
2948
+
2949
+ class MergeRefusal extends RebuildRefusal {
2950
+ reason;
2951
+ constructor(reason, message) {
2952
+ super(message);
2953
+ this.name = "MergeRefusal";
2954
+ this.reason = reason;
2955
+ }
2956
+ }
2957
+ var LIVE3 = "live";
2958
+ var MERGES_TABLE = "merges";
2959
+ var INSERT_MERGE = `INSERT INTO merges (id, other_store, merged_at, rows_added, rows_skipped, rows_in_conflict)
2960
+ VALUES (?, ?, ?, ?, ?, ?)`;
2961
+ var NOTHING_MERGED = "Nothing was merged.";
2962
+ function refuseNdjson(backend) {
2963
+ if (backend !== "ndjson")
2964
+ return;
2965
+ 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}`);
2966
+ }
2967
+ function refuseMissing(path, which) {
2968
+ if (existsSync11(path))
2969
+ return;
2970
+ throw new MergeRefusal("missing", `REFUSED \u2014 ${which} ${path} is not there. ${NOTHING_MERGED}`);
2971
+ }
2972
+ function refuseDamaged(path) {
2973
+ let answers;
2974
+ try {
2975
+ const db = new Database5(path, { readonly: true });
2976
+ try {
2977
+ answers = db.query("PRAGMA integrity_check").all().map((row) => row.integrity_check);
2978
+ } finally {
2979
+ db.close();
2980
+ }
2981
+ } catch (error) {
2982
+ if (!(error instanceof SQLiteError2))
2983
+ throw error;
2984
+ 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.");
2985
+ }
2986
+ if (answers.length === 1 && answers[0] === "ok")
2987
+ return;
2988
+ 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.");
2989
+ }
2990
+ function writablePlan(path, migrations) {
2991
+ const plan = planSchema(readOnlySchema(path), sqliteCatalogue(migrations), "write", "open");
2992
+ if (plan.verdict === "refuse")
2993
+ throw new MergeRefusal("schema", `REFUSED \u2014 ${path}: ${plan.message}`);
2994
+ return plan;
2995
+ }
2996
+ function recordedMeta(path) {
2997
+ const db = new Database5(path, { readonly: true });
2998
+ try {
2999
+ const hasTable = db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
3000
+ return hasTable ? readStoreMeta(db) : null;
3001
+ } finally {
3002
+ db.close();
3003
+ }
3004
+ }
3005
+ function refuseOtherProject(options) {
3006
+ const other = recordedMeta(options.otherPath);
3007
+ const here = recordedMeta(options.path)?.projectRootCommit ?? (options.readProject ?? readProjectIdentity)(dirname10(options.path)).rootCommit;
3008
+ const there = other?.projectRootCommit ?? null;
3009
+ if (here !== null && there !== null && here !== there) {
3010
+ 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}`);
3011
+ }
3012
+ return other?.storeId ?? null;
3013
+ }
3014
+ function refuseLiveLoop3(path, options) {
3015
+ const loop = liveLoop(path, options, NOTHING_MERGED);
3016
+ if (loop === undefined)
3017
+ return;
3018
+ 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.");
3019
+ }
3020
+ function openOther(otherPath, plan, stamp) {
3021
+ const needs = writeNames(plan);
3022
+ if (needs.length === 0) {
3023
+ const db = new Database5(otherPath, { readonly: true });
3024
+ return { db, broughtForward: [], release: () => db.close() };
3025
+ }
3026
+ const dir = mkdtempSync(join11(tmpdir3(), "rafa-merge-other-"));
3027
+ const release = (db) => {
3028
+ db?.close();
3029
+ rmSync6(dir, { recursive: true, force: true });
3030
+ };
3031
+ try {
3032
+ const copyPath = join11(dir, "other.sqlite");
3033
+ vacuumInto(otherPath, copyPath);
3034
+ const db = new Database5(copyPath, { readwrite: true });
3035
+ try {
3036
+ bringForward(db, copyPath, "write", "open", { ...stamp, builtAside: true });
3037
+ } catch (error) {
3038
+ release(db);
3039
+ throw error;
3040
+ }
3041
+ return { db, broughtForward: needs, release: () => release(db) };
3042
+ } catch (error) {
3043
+ release();
3044
+ throw error;
3045
+ }
3046
+ }
3047
+ function mergeInto(db, other, mergeId, otherStore, mergedAt) {
3048
+ return db.transaction(() => {
3049
+ const unions = unionStores(db, other);
3050
+ const settlements = settleMatches(db, other, unions, { mergeId, recordedAt: mergedAt });
3051
+ const gaps = recomputeCommitGaps(db, unions.find(({ table }) => table === "commits")?.added ?? []);
3052
+ const tables = unions.map((union, index) => {
3053
+ const settled = settlements[index];
3054
+ return {
3055
+ table: union.table,
3056
+ added: union.added.length,
3057
+ skipped: settled?.skipped.length ?? 0,
3058
+ inConflict: settled?.conflicts.length ?? 0,
3059
+ filled: settled?.filled.length ?? 0,
3060
+ collided: union.collided.length
3061
+ };
3062
+ });
3063
+ const sum = (field2) => tables.reduce((total, entry) => total + entry[field2], 0);
3064
+ const totals = { rowsAdded: sum("added"), rowsSkipped: sum("skipped"), rowsInConflict: sum("inConflict") };
3065
+ db.query(INSERT_MERGE).run(mergeId, otherStore, mergedAt, totals.rowsAdded, totals.rowsSkipped, totals.rowsInConflict);
3066
+ return { tables, ...totals, gapsRewritten: gaps.length };
3067
+ })();
3068
+ }
3069
+ function addedRows(merged) {
3070
+ return {
3071
+ ...Object.fromEntries(merged.tables.map(({ table, added }) => [table, added])),
3072
+ [MERGES_TABLE]: 1,
3073
+ merge_conflicts: merged.rowsInConflict
3074
+ };
3075
+ }
3076
+ function buildMerged(path, parallelPath, other, merge2) {
3077
+ vacuumInto(path, parallelPath);
3078
+ const db = new Database5(parallelPath, { readwrite: true });
3079
+ try {
3080
+ bringForward(db, parallelPath, "write", "open", { ...merge2.stamp, builtAside: true });
3081
+ const merged = mergeInto(db, other, merge2.id, merge2.otherStore, merge2.stamp.now().toISOString());
3082
+ db.run(`ATTACH DATABASE ? AS ${LIVE3}`, [path]);
3083
+ const live = new Set(tableNames(db, LIVE3));
3084
+ checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE3, addedRows(merged));
3085
+ db.run(`DETACH DATABASE ${LIVE3}`);
3086
+ refuseCorrupt(db);
3087
+ refuseUnusable(db, parallelPath, merge2.stamp.migrations);
3088
+ return merged;
3089
+ } finally {
3090
+ db.close();
3091
+ }
3092
+ }
3093
+ function refuseUnmergeable(options, migrations) {
3094
+ const { path, otherPath } = options;
3095
+ refuseNdjson(options.backend);
3096
+ refuseMissing(path, "this store");
3097
+ refuseMissing(otherPath, "the other store");
3098
+ refuseInFlight(path);
3099
+ refuseInFlight(otherPath);
3100
+ refuseDamaged(otherPath);
3101
+ const otherPlan = writablePlan(otherPath, migrations);
3102
+ const localPlan = writablePlan(path, migrations);
3103
+ const otherStore = refuseOtherProject(options);
3104
+ if (!options.dryRun) {
3105
+ refuseUnownedDevelopmentWrite(path, [...writeNames(localPlan), MERGES_TABLE], options);
3106
+ refuseLiveLoop3(path, options);
3107
+ }
3108
+ return { otherPlan, otherStore };
3109
+ }
3110
+ function mergeStore(options) {
3111
+ const { path, otherPath, dryRun, stamp } = options;
3112
+ guardTestProcess(path);
3113
+ guardTestProcess(otherPath);
3114
+ const migrations = options.migrations ?? SQLITE_MIGRATIONS;
3115
+ const { otherPlan, otherStore } = refuseUnmergeable(options, migrations);
3116
+ const mergeStamp = {
3117
+ migrations,
3118
+ appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
3119
+ now: options.now ?? (() => new Date)
3120
+ };
3121
+ const mergeId = (options.newMergeId ?? randomUUID4)();
3122
+ const other = openOther(otherPath, otherPlan, mergeStamp);
3123
+ try {
3124
+ const { built, backupPath } = rebuildAside({
3125
+ path,
3126
+ parallelPath: `${path}.merge-${stamp}`,
3127
+ backupPath: `${path}.before-merge-${stamp}.bak`,
3128
+ dryRun,
3129
+ build: (parallelPath) => buildMerged(path, parallelPath, other.db, { id: mergeId, otherStore, stamp: mergeStamp })
3130
+ });
3131
+ const status = dryRun ? "would-merge" : "merged";
3132
+ return { path, otherPath, status, mergeId, otherStore, otherBroughtForward: other.broughtForward, ...built, backupPath };
3133
+ } finally {
3134
+ other.release();
3135
+ }
3136
+ }
3137
+
3138
+ // src/effort/sync/file.ts
3139
+ var REFUSAL = "effort sync (file)";
3140
+ var NOTHING_COPIED = "Nothing was copied.";
3141
+
3142
+ class FileSyncRefusal extends Error {
3143
+ name = "FileSyncRefusal";
3144
+ }
3145
+ function requirePath(field2, value) {
3146
+ if (typeof value === "string" && value !== "")
3147
+ return value;
3148
+ throw new TypeError(`${REFUSAL}: ${field2} is ${describeValue(value)}, expected a path`);
3149
+ }
3150
+ function refuseUncarriable(backend, source) {
3151
+ if (backend === "ndjson") {
3152
+ throw new FileSyncRefusal(`${REFUSAL}: this project keeps its sessions and commits as NDJSON (store: ndjson), and the file carried` + ` is effort.sqlite, which would hold none of them. ${NOTHING_COPIED} Next safe step: ${MOVE_TO_SQLITE}`);
3153
+ }
3154
+ if (!existsSync12(join12(source, SQLITE_STORE_FILE_NAME))) {
3155
+ throw new FileSyncRefusal(`${REFUSAL}: no ${SQLITE_STORE_FILE_NAME} in ${source}. ${NOTHING_COPIED}`);
3156
+ }
3157
+ }
3158
+ function mergeSeams(options) {
3159
+ const { identity, env, tempDir, isAlive, migrations, newMergeId, readProject } = options;
3160
+ const seams = { identity, env, tempDir, isAlive, migrations, newMergeId, readProject };
3161
+ return Object.fromEntries(Object.entries(seams).filter(([, value]) => value !== undefined));
3162
+ }
3163
+ function createFileSync(options) {
3164
+ const { repoRoot, backend } = options;
3165
+ const now = options.now ?? (() => new Date);
3166
+ const storeDir = () => effortStoreDir(repoRoot, options.env);
3167
+ const push = async ({ to }) => {
3168
+ const target = resolve5(repoRoot, requirePath("to", to));
3169
+ const source = storeDir();
3170
+ refuseUncarriable(backend, source);
3171
+ const copied = copyEffortStore({ source, target });
3172
+ return Object.freeze({ status: "pushed", path: join12(copied.directory, SQLITE_STORE_FILE_NAME) });
3173
+ };
3174
+ const pull = async ({ from, dryRun }) => {
3175
+ const otherPath = resolve5(repoRoot, requirePath("from", from));
3176
+ const path = join12(storeDir(), SQLITE_STORE_FILE_NAME);
3177
+ const merge2 = mergeStore({ ...mergeSeams(options), path, otherPath, backend, dryRun, stamp: fileStamp(now()), now });
3178
+ return Object.freeze({ status: "pulled", merge: merge2 });
3179
+ };
3180
+ return Object.freeze({ kind: "file", push, pull });
3181
+ }
3182
+
3183
+ // src/effort/sync/select.ts
3184
+ var REFUSAL2 = "effort sync";
3185
+ var MODULE_SYNC_STRATEGIES = Object.freeze(["git", "service", "p2p"]);
3186
+ var MODULE_DIRECTORY_PLACEHOLDER = "<module directory>";
3187
+ var MODULE_NAME_PLACEHOLDER = "<module name>";
3188
+ var NOTHING_TO_SYNC = Object.freeze({ status: "nothing-to-sync" });
3189
+ function createLocalSync() {
3190
+ return Object.freeze({
3191
+ kind: "local",
3192
+ push: async () => NOTHING_TO_SYNC,
3193
+ pull: async () => NOTHING_TO_SYNC
3194
+ });
3195
+ }
3196
+ function listed(registered) {
3197
+ return registered.length === 0 ? "none" : registered.join(", ");
3198
+ }
3199
+ function moduleMissingMessage(kind, registered) {
3200
+ return [
3201
+ `${REFUSAL2}: effort.sync is "${kind}", and no module registers a sync adapter of that kind` + ` (registered: ${listed(registered)}). Core ships no ${kind} strategy; load a module that provides` + " one with these lines in .rafa/config.yaml:",
3202
+ "modules:",
3203
+ ` - path: ${MODULE_DIRECTORY_PLACEHOLDER}`,
3204
+ "allowList:",
3205
+ ` - ${MODULE_NAME_PLACEHOLDER}`
3206
+ ].join(`
3207
+ `);
3208
+ }
3209
+
3210
+ class SyncModuleMissing extends Error {
3211
+ name = "SyncModuleMissing";
3212
+ kind;
3213
+ constructor(kind, registered) {
3214
+ super(moduleMissingMessage(kind, registered));
3215
+ this.kind = kind;
3216
+ }
3217
+ }
3218
+ function isModuleStrategy(kind) {
3219
+ return MODULE_SYNC_STRATEGIES.includes(kind);
3220
+ }
3221
+ function selectSync(repoRoot, config, registry = CORE_ADAPTER_REGISTRY) {
3222
+ const kind = config.effortSync;
3223
+ const adapter = typeof kind === "string" ? registry.find("sync", kind) : undefined;
3224
+ if (adapter !== undefined) {
3225
+ return adapter.create(config.store === undefined ? { repoRoot } : { repoRoot, store: config.store });
3226
+ }
3227
+ const registered = registry.kinds("sync");
3228
+ if (isModuleStrategy(kind))
3229
+ throw new SyncModuleMissing(kind, registered);
3230
+ const known = SYNC_STRATEGIES.includes(kind);
3231
+ const expected = known ? `registered: ${listed(registered)}` : `expected one of: ${SYNC_STRATEGIES.join(", ")}`;
3232
+ throw new TypeError(`${REFUSAL2}: effort.sync is ${describeValue(kind)}, and no sync adapter is registered under it; ${expected}`);
3233
+ }
3234
+
1781
3235
  // src/adapters/learning/local.ts
1782
3236
  import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
1783
- import { join as join12 } from "path";
3237
+ import { join as join15 } from "path";
1784
3238
 
1785
3239
  // src/commands/instinct/instinct-records.ts
1786
- import { readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
1787
- import { join as join11 } from "path";
3240
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
3241
+ import { join as join14 } from "path";
1788
3242
 
1789
3243
  // src/check/layout.ts
1790
- import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1791
- import { join as join10, relative as relative2 } from "path";
3244
+ import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3245
+ import { join as join13, relative as relative2 } from "path";
1792
3246
  var SKILL_FILE = "SKILL.md";
1793
3247
  var MARKDOWN_EXTENSION = ".md";
1794
3248
  var LAYOUT_SEVERITY = Object.freeze({
@@ -1806,7 +3260,7 @@ function layoutIssue(code, message) {
1806
3260
  }
1807
3261
  function entryKind(path) {
1808
3262
  try {
1809
- const stats = statSync2(path);
3263
+ const stats = statSync4(path);
1810
3264
  if (stats.isFile())
1811
3265
  return "file";
1812
3266
  if (stats.isDirectory())
@@ -1817,7 +3271,7 @@ function entryKind(path) {
1817
3271
  }
1818
3272
  }
1819
3273
  function visibleNames(dir) {
1820
- return readdirSync2(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
3274
+ return readdirSync3(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
1821
3275
  }
1822
3276
  function isMarkdown(name2) {
1823
3277
  return name2.endsWith(MARKDOWN_EXTENSION);
@@ -1905,7 +3359,7 @@ function scanGroupMember(root, dir, name2) {
1905
3359
  const children = visibleNames(dir);
1906
3360
  if (children.length === 0)
1907
3361
  return [dotfileOnlyEntry("skill", root, dir)];
1908
- const skillPath = join10(dir, SKILL_FILE);
3362
+ const skillPath = join13(dir, SKILL_FILE);
1909
3363
  if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
1910
3364
  return [nestedGroupEntry(root, skillPath, name2)];
1911
3365
  }
@@ -1914,7 +3368,7 @@ function scanGroupMember(root, dir, name2) {
1914
3368
  function scanSkillGroup(root, dir, children) {
1915
3369
  const entries = [];
1916
3370
  for (const child of children) {
1917
- const path = join10(dir, child);
3371
+ const path = join13(dir, child);
1918
3372
  const found = entryKind(path);
1919
3373
  if (found === null) {
1920
3374
  entries.push(unreadableEntry("skill", root, path));
@@ -1935,7 +3389,7 @@ function scanSkillDirectory(root, dir, name2) {
1935
3389
  const children = visibleNames(dir);
1936
3390
  if (children.length === 0)
1937
3391
  return [dotfileOnlyEntry("skill", root, dir)];
1938
- const skillPath = join10(dir, SKILL_FILE);
3392
+ const skillPath = join13(dir, SKILL_FILE);
1939
3393
  if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
1940
3394
  return [skillFileEntry(skillPath, name2)];
1941
3395
  }
@@ -1944,7 +3398,7 @@ function scanSkillDirectory(root, dir, name2) {
1944
3398
  function scanSkillRoot(root) {
1945
3399
  const entries = [];
1946
3400
  for (const name2 of visibleNames(root)) {
1947
- const path = join10(root, name2);
3401
+ const path = join13(root, name2);
1948
3402
  const found = entryKind(path);
1949
3403
  if (found === null) {
1950
3404
  entries.push(unreadableEntry("skill", root, path));
@@ -1965,7 +3419,7 @@ function scanInstinctSubdirectory(root, dir) {
1965
3419
  return [dotfileOnlyEntry("instinct", root, dir)];
1966
3420
  const entries = [];
1967
3421
  for (const child of children) {
1968
- const path = join10(dir, child);
3422
+ const path = join13(dir, child);
1969
3423
  const found = entryKind(path);
1970
3424
  if (found === null) {
1971
3425
  entries.push(unreadableEntry("instinct", root, path));
@@ -1983,7 +3437,7 @@ function scanInstinctSubdirectory(root, dir) {
1983
3437
  function scanInstinctRoot(root) {
1984
3438
  const entries = [];
1985
3439
  for (const name2 of visibleNames(root)) {
1986
- const path = join10(root, name2);
3440
+ const path = join13(root, name2);
1987
3441
  const found = entryKind(path);
1988
3442
  if (found === null) {
1989
3443
  entries.push(unreadableEntry("instinct", root, path));
@@ -2436,7 +3890,7 @@ function unreadableIssue(message) {
2436
3890
  function readRecord(path, scope, id) {
2437
3891
  let text;
2438
3892
  try {
2439
- text = readFileSync7(path, "utf8");
3893
+ text = readFileSync8(path, "utf8");
2440
3894
  } catch (error) {
2441
3895
  const message = error instanceof Error ? error.message : String(error);
2442
3896
  return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
@@ -2444,15 +3898,15 @@ function readRecord(path, scope, id) {
2444
3898
  const parsed = parseInstinct(text);
2445
3899
  return { scope, path, id, instinct: parsed.instinct, issues: parsed.issues };
2446
3900
  }
2447
- function isFile(path) {
3901
+ function isFile2(path) {
2448
3902
  try {
2449
- return statSync3(path).isFile();
3903
+ return statSync5(path).isFile();
2450
3904
  } catch {
2451
3905
  return false;
2452
3906
  }
2453
3907
  }
2454
3908
  function recordFiles(dir) {
2455
- return readdirSync3(dir).filter((name2) => isRecordFile(name2) && isFile(join11(dir, name2))).sort((left, right) => left.localeCompare(right));
3909
+ return readdirSync4(dir).filter((name2) => isRecordFile(name2) && isFile2(join14(dir, name2))).sort((left, right) => left.localeCompare(right));
2456
3910
  }
2457
3911
  function readScope(scope, dir) {
2458
3912
  if (!tierExists(dir))
@@ -2461,7 +3915,7 @@ function readScope(scope, dir) {
2461
3915
  scope,
2462
3916
  dir,
2463
3917
  exists: true,
2464
- records: recordFiles(dir).map((name2) => readRecord(join11(dir, name2), scope, recordId(name2)))
3918
+ records: recordFiles(dir).map((name2) => readRecord(join14(dir, name2), scope, recordId(name2)))
2465
3919
  };
2466
3920
  }
2467
3921
  function readScopes(seams) {
@@ -2480,7 +3934,7 @@ function instinctProject(context, name2) {
2480
3934
  }
2481
3935
 
2482
3936
  // src/adapters/learning/held.ts
2483
- function compareText(left, right) {
3937
+ function compareText2(left, right) {
2484
3938
  if (left < right) {
2485
3939
  return -1;
2486
3940
  }
@@ -2490,7 +3944,7 @@ function compareText(left, right) {
2490
3944
  return 0;
2491
3945
  }
2492
3946
  function byAge(left, right) {
2493
- return compareText(left.createdAt, right.createdAt) || compareText(left.id, right.id);
3947
+ return compareText2(left.createdAt, right.createdAt) || compareText2(left.id, right.id);
2494
3948
  }
2495
3949
  function toHeldRecord(instinct) {
2496
3950
  return {
@@ -2764,7 +4218,7 @@ function heldChanges(dir, held, result, described) {
2764
4218
  if (issue2 !== undefined) {
2765
4219
  throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue2.field}: ${issue2.message}`);
2766
4220
  }
2767
- const path = join12(dir, file);
4221
+ const path = join15(dir, file);
2768
4222
  if (held.unreadable.has(path)) {
2769
4223
  throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
2770
4224
  }
@@ -2794,18 +4248,18 @@ function pushLogLines(sourceId, described, result) {
2794
4248
  });
2795
4249
  }
2796
4250
  function localInstinctsDir(repoRoot) {
2797
- return join12(repoRoot, RAFA_INSTINCTS_PATH);
4251
+ return join15(repoRoot, RAFA_INSTINCTS_PATH);
2798
4252
  }
2799
4253
  function userInstinctsDir(home) {
2800
- return join12(home, RAFA_INSTINCTS_PATH);
4254
+ return join15(home, RAFA_INSTINCTS_PATH);
2801
4255
  }
2802
4256
  function createLocalLearning(options) {
2803
4257
  const { instinctsDir, minConfidence } = options;
2804
4258
  const userDir = userInstinctsDir(options.home);
2805
4259
  const now = options.now ?? (() => new Date().toISOString());
2806
4260
  const warn = options.warn ?? warnThroughActiveOutput;
2807
- const instinctsPath = join12(instinctsDir, INSTINCTS_FILE);
2808
- const flagsPath = join12(instinctsDir, FLAGS_FILE);
4261
+ const instinctsPath = join15(instinctsDir, INSTINCTS_FILE);
4262
+ const flagsPath = join15(instinctsDir, FLAGS_FILE);
2809
4263
  function readHeld(scope, dir) {
2810
4264
  const { records } = readScope(scope, dir);
2811
4265
  const unreadable = new Set;
@@ -2922,9 +4376,9 @@ function createJsonOutput({
2922
4376
  }
2923
4377
 
2924
4378
  // src/adapters/planner/claude.ts
2925
- import { existsSync as existsSync6 } from "fs";
4379
+ import { existsSync as existsSync13 } from "fs";
2926
4380
  import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2927
- import { posix, resolve as resolve3 } from "path";
4381
+ import { posix, resolve as resolve6 } from "path";
2928
4382
 
2929
4383
  // src/board/spec-review.ts
2930
4384
  var SPEC_REVIEW_KIND = "spec-review";
@@ -2960,8 +4414,8 @@ function describeValue3(value) {
2960
4414
  function messageOf2(error) {
2961
4415
  return error instanceof Error ? error.message : String(error);
2962
4416
  }
2963
- function counted(count, noun) {
2964
- return count === 1 ? `1 ${noun}` : `${String(count)} ${noun}s`;
4417
+ function counted(count2, noun) {
4418
+ return count2 === 1 ? `1 ${noun}` : `${String(count2)} ${noun}s`;
2965
4419
  }
2966
4420
  function valueAt(mapping, key) {
2967
4421
  return Object.hasOwn(mapping, key) ? mapping[key] ?? null : null;
@@ -3187,17 +4641,18 @@ function guardSpend() {
3187
4641
  if (refusal !== null)
3188
4642
  throw new UndeclaredSpendError(refusal);
3189
4643
  }
3190
- async function spawnClaude(args, prompt) {
4644
+ async function spawnClaude(args, prompt, options = {}) {
3191
4645
  guardSpend();
3192
4646
  if (activeOutputMode() === "json") {
3193
- const { exitCode } = await spawnCaptured(args, prompt);
4647
+ const { exitCode } = await spawnCaptured(args, prompt, options);
3194
4648
  return exitCode;
3195
4649
  }
3196
4650
  const proc = Bun.spawn([CLAUDE_BIN, ...args], {
3197
4651
  stdin: new TextEncoder().encode(prompt),
3198
4652
  stdout: "inherit",
3199
4653
  stderr: "inherit",
3200
- env: sessionSpawnEnv(process.env)
4654
+ env: sessionSpawnEnv(process.env),
4655
+ ...cwdOption(options)
3201
4656
  });
3202
4657
  liveSessions.add(proc);
3203
4658
  try {
@@ -3206,8 +4661,11 @@ async function spawnClaude(args, prompt) {
3206
4661
  liveSessions.delete(proc);
3207
4662
  }
3208
4663
  }
3209
- function runClaude(prompt, settingSources, flags = [], spawn = spawnClaude, served = []) {
3210
- return spawn(claudeArgs(settingSources, flags, served), prompt);
4664
+ function runClaude(prompt, settingSources, flags = [], spawn = spawnClaude, served = [], options = {}) {
4665
+ return spawn(claudeArgs(settingSources, flags, served), prompt, options);
4666
+ }
4667
+ function cwdOption(options) {
4668
+ return options.cwd === undefined ? {} : { cwd: options.cwd };
3211
4669
  }
3212
4670
  function writeWholeLines(text, write) {
3213
4671
  const lines = text.split(`
@@ -3251,7 +4709,7 @@ async function spawnCaptured(args, prompt, options = {}) {
3251
4709
  stdout: "pipe",
3252
4710
  stderr: "inherit",
3253
4711
  env: sessionSpawnEnv(process.env),
3254
- ...options.cwd === undefined ? {} : { cwd: options.cwd }
4712
+ ...cwdOption(options)
3255
4713
  });
3256
4714
  liveSessions.add(proc);
3257
4715
  let stdout;
@@ -3263,8 +4721,8 @@ async function spawnCaptured(args, prompt, options = {}) {
3263
4721
  }
3264
4722
  return { exitCode: await proc.exited, stdout };
3265
4723
  }
3266
- function runClaudeCaptured(prompt, settingSources, flags = [], spawn = spawnClaudeCaptured, served = []) {
3267
- return spawn(claudeArgs(settingSources, flags, served), prompt);
4724
+ function runClaudeCaptured(prompt, settingSources, flags = [], spawn = spawnClaudeCaptured, served = [], options = {}) {
4725
+ return spawn(claudeArgs(settingSources, flags, served), prompt, options);
3268
4726
  }
3269
4727
 
3270
4728
  // src/adapters/planner/claude.ts
@@ -3285,23 +4743,24 @@ class ClaudePlannerError extends Error {
3285
4743
  }
3286
4744
  function createClaudePlanner(options) {
3287
4745
  const { repoRoot, planDir, settingSources, buildPrompt, spawn = spawnClaudeCaptured } = options;
4746
+ const root = resolve6(repoRoot);
3288
4747
  const create = async ({ specPath, stub }) => {
3289
4748
  const planPath = planFilePath(planDir, `PLAN-${stub}.md`);
3290
4749
  const prerequisitesPath = planFilePath(planDir, `PREREQUISITES-${stub}.md`);
3291
- const isWritten = (path) => existsSync6(resolve3(repoRoot, path));
4750
+ const isWritten = (path) => existsSync13(resolve6(root, path));
3292
4751
  if (isWritten(planPath)) {
3293
4752
  throw new ClaudePlannerError(`${planPath} already exists`, FAILURE_EXIT_CODE);
3294
4753
  }
3295
- const specContent = await readFile2(resolve3(repoRoot, specPath), "utf8");
3296
- await mkdir2(resolve3(repoRoot, planDir), { recursive: true });
3297
- const session = await runClaudeCaptured(buildPrompt(specContent, stub), settingSources, [], spawn);
4754
+ const specContent = await readFile2(resolve6(root, specPath), "utf8");
4755
+ await mkdir2(resolve6(root, planDir), { recursive: true });
4756
+ const session = await spawn(claudeArgs(settingSources), buildPrompt(specContent, stub), { cwd: root });
3298
4757
  const review = parseSpecReview(session.stdout);
3299
4758
  const { exitCode } = session;
3300
4759
  if (exitCode !== 0) {
3301
4760
  throw new ClaudePlannerError(`Plan generation failed (exit ${exitCode}).`, exitCode, review);
3302
4761
  }
3303
4762
  if (!isWritten(planPath)) {
3304
- throw new ClaudePlannerError(`The session finished but ${planPath} was not created \u2014 inspect the output above.`, FAILURE_EXIT_CODE, review);
4763
+ throw new ClaudePlannerError(`The session finished but ${planPath} was not created under ${root} \u2014 inspect the output above.`, FAILURE_EXIT_CODE, review);
3305
4764
  }
3306
4765
  return {
3307
4766
  planPath,
@@ -3404,8 +4863,8 @@ async function withDeadline(work, timeoutMs) {
3404
4863
  if (timeoutMs === undefined)
3405
4864
  return work;
3406
4865
  let timer;
3407
- const deadline = new Promise((resolve4) => {
3408
- timer = setTimeout(() => resolve4(TIMED_OUT), timeoutMs);
4866
+ const deadline = new Promise((resolve7) => {
4867
+ timer = setTimeout(() => resolve7(TIMED_OUT), timeoutMs);
3409
4868
  });
3410
4869
  try {
3411
4870
  return await Promise.race([work, deadline]);
@@ -3710,7 +5169,7 @@ function createGithubTracker(options) {
3710
5169
 
3711
5170
  // src/adapters/tracker/local.ts
3712
5171
  import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3713
- import { join as join13 } from "path";
5172
+ import { join as join16 } from "path";
3714
5173
  var PREFIX3 = "local tracker";
3715
5174
  var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
3716
5175
  var ISSUE_NUMBER2 = /^[1-9]\d*$/;
@@ -3767,7 +5226,7 @@ function issueNumberOfFile(name2) {
3767
5226
  return name2.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name2.slice(0, -ISSUE_EXTENSION.length)) : null;
3768
5227
  }
3769
5228
  function issuePath(issuesDir, number) {
3770
- return join13(issuesDir, `${number}${ISSUE_EXTENSION}`);
5229
+ return join16(issuesDir, `${number}${ISSUE_EXTENSION}`);
3771
5230
  }
3772
5231
  async function issueNumbers(issuesDir) {
3773
5232
  let names;
@@ -3781,7 +5240,7 @@ async function issueNumbers(issuesDir) {
3781
5240
  return names.map(issueNumberOfFile).filter((number) => number !== null).sort((a, b) => a - b);
3782
5241
  }
3783
5242
  function localIssuesDir(repoRoot) {
3784
- return join13(repoRoot, ".rafa", "issues");
5243
+ return join16(repoRoot, ".rafa", "issues");
3785
5244
  }
3786
5245
  function renderLocalIssue(record) {
3787
5246
  const { draft } = record;
@@ -3960,40 +5419,41 @@ function createLocalTracker(options) {
3960
5419
  }
3961
5420
 
3962
5421
  // src/adapters/registry.ts
3963
- var REFUSAL = "adapter registry";
5422
+ var REFUSAL3 = "adapter registry";
3964
5423
  var PORT_VERSIONS = Object.freeze({
3965
5424
  tracker: 1,
3966
5425
  store: 1,
3967
5426
  learning: 1,
3968
5427
  output: 1,
3969
- planner: 1
5428
+ planner: 1,
5429
+ sync: 1
3970
5430
  });
3971
5431
  var SERVED_VERSIONS = new Map(Object.entries(PORT_VERSIONS));
3972
5432
  var PORT_TYPE_LIST = [...SERVED_VERSIONS.keys()].join(", ");
3973
5433
  function checkPortType(port) {
3974
5434
  if (!SERVED_VERSIONS.has(port)) {
3975
- throw new TypeError(`${REFUSAL}: port is ${describeValue(port)}, expected one of: ${PORT_TYPE_LIST}`);
5435
+ throw new TypeError(`${REFUSAL3}: port is ${describeValue(port)}, expected one of: ${PORT_TYPE_LIST}`);
3976
5436
  }
3977
5437
  }
3978
5438
  function accept(adapter, held) {
3979
5439
  if (typeof adapter !== "object" || adapter === null) {
3980
- throw new TypeError(`${REFUSAL}: an adapter is ${describeValue(adapter)}, expected a mapping`);
5440
+ throw new TypeError(`${REFUSAL3}: an adapter is ${describeValue(adapter)}, expected a mapping`);
3981
5441
  }
3982
5442
  const { port, kind, portVersion, create } = adapter;
3983
5443
  checkPortType(port);
3984
5444
  if (typeof kind !== "string" || kind === "") {
3985
- throw new TypeError(`${REFUSAL}: a ${port} adapter has kind ${describeValue(kind)}, expected a non-empty name`);
5445
+ throw new TypeError(`${REFUSAL3}: a ${port} adapter has kind ${describeValue(kind)}, expected a non-empty name`);
3986
5446
  }
3987
5447
  const key = `${port}/${kind}`;
3988
5448
  if (typeof create !== "function") {
3989
- throw new TypeError(`${REFUSAL}: ${key} has create ${describeValue(create)}, expected a function`);
5449
+ throw new TypeError(`${REFUSAL3}: ${key} has create ${describeValue(create)}, expected a function`);
3990
5450
  }
3991
5451
  const served = SERVED_VERSIONS.get(port);
3992
5452
  if (portVersion !== served) {
3993
- throw new Error(`${REFUSAL}: ${key} implements ${port} port version ${describeValue(portVersion)},` + ` and core serves ${port} port version ${served}`);
5453
+ throw new Error(`${REFUSAL3}: ${key} implements ${port} port version ${describeValue(portVersion)},` + ` and core serves ${port} port version ${served}`);
3994
5454
  }
3995
5455
  if (held.some((other) => other.port === port && other.kind === kind)) {
3996
- throw new Error(`${REFUSAL}: ${key} is already registered`);
5456
+ throw new Error(`${REFUSAL3}: ${key} is already registered`);
3997
5457
  }
3998
5458
  return Object.freeze({ port, kind, portVersion, create });
3999
5459
  }
@@ -4004,7 +5464,8 @@ function indexByPort(adapters) {
4004
5464
  store: only("store"),
4005
5465
  learning: only("learning"),
4006
5466
  output: only("output"),
4007
- planner: only("planner")
5467
+ planner: only("planner"),
5468
+ sync: only("sync")
4008
5469
  };
4009
5470
  }
4010
5471
  function createAdapterRegistry(adapters = []) {
@@ -4019,8 +5480,8 @@ function createAdapterRegistry(adapters = []) {
4019
5480
  if (adapter !== undefined)
4020
5481
  return adapter;
4021
5482
  const registered = [...byPort[port].keys()];
4022
- const listed = registered.length === 0 ? "none" : registered.join(", ");
4023
- throw new Error(`${REFUSAL}: no ${port} adapter is registered as ${describeValue(kind)};` + ` registered: ${listed}`);
5483
+ const listed2 = registered.length === 0 ? "none" : registered.join(", ");
5484
+ throw new Error(`${REFUSAL3}: no ${port} adapter is registered as ${describeValue(kind)};` + ` registered: ${listed2}`);
4024
5485
  },
4025
5486
  kinds: (port) => {
4026
5487
  checkPortType(port);
@@ -4087,16 +5548,33 @@ var CORE_ADAPTERS = [
4087
5548
  portVersion: PORT_VERSIONS.planner,
4088
5549
  create: ({ repoRoot, settingSources, planPrompt, planDir, claude }) => {
4089
5550
  if (!Array.isArray(settingSources)) {
4090
- throw new TypeError(`${REFUSAL}: planner/claude has settingSources ${describeValue(settingSources)} in its context,` + " expected a list of setting sources");
5551
+ throw new TypeError(`${REFUSAL3}: planner/claude has settingSources ${describeValue(settingSources)} in its context,` + " expected a list of setting sources");
4091
5552
  }
4092
5553
  if (typeof planPrompt !== "function") {
4093
- throw new TypeError(`${REFUSAL}: planner/claude has planPrompt ${describeValue(planPrompt)} in its context,` + " expected a function");
5554
+ throw new TypeError(`${REFUSAL3}: planner/claude has planPrompt ${describeValue(planPrompt)} in its context,` + " expected a function");
4094
5555
  }
4095
5556
  if (typeof planDir !== "string") {
4096
- throw new TypeError(`${REFUSAL}: planner/claude has planDir ${describeValue(planDir)} in its context,` + " expected a directory path");
5557
+ throw new TypeError(`${REFUSAL3}: planner/claude has planDir ${describeValue(planDir)} in its context,` + " expected a directory path");
4097
5558
  }
4098
5559
  return createClaudePlanner({ repoRoot, planDir, settingSources, buildPrompt: planPrompt, spawn: claude });
4099
5560
  }
5561
+ },
5562
+ {
5563
+ port: "sync",
5564
+ kind: "local",
5565
+ portVersion: PORT_VERSIONS.sync,
5566
+ create: () => createLocalSync()
5567
+ },
5568
+ {
5569
+ port: "sync",
5570
+ kind: "file",
5571
+ portVersion: PORT_VERSIONS.sync,
5572
+ create: ({ repoRoot, store }) => {
5573
+ if (!STORE_BACKENDS.includes(store)) {
5574
+ throw new TypeError(`${REFUSAL3}: sync/file has store ${describeValue(store)} in its context,` + ` expected one of: ${STORE_BACKENDS.join(", ")}`);
5575
+ }
5576
+ return createFileSync({ repoRoot, backend: store });
5577
+ }
4100
5578
  }
4101
5579
  ];
4102
5580
  var CORE_ADAPTER_REGISTRY = createAdapterRegistry(CORE_ADAPTERS);
@@ -4118,4 +5596,4 @@ function selectEffortStore(repoRoot, config) {
4118
5596
  return adapter.create({ repoRoot });
4119
5597
  }
4120
5598
 
4121
- export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, positionAt, rehome, hop, goHome, HOP_FILE, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, SessionConflictError, SessionStateError, isSessionId, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, beginSession, updateSession, readRuntimeIdentity, DEVELOPMENT_NEXT_STEP, ADOPTION_NAME, DEVELOPMENT_MARK, appliedByName, DevelopmentBuildRefusedError, storeProjectRoot, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, MIGRATION_LOG_TABLE, SchemaRefusedError, bringForward, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, setActiveStoreSettings, activeStoreSettings, createGitRunner, gitSaid, normalizeRemote, gitRemoteUrl, readProjectIdentity, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, sessionSpawnEnv, getClaudeUsagePercent, checkUsage, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };
5599
+ export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, createGitRunner, gitSaid, NOT_A_REPOSITORY, mainCheckoutOf, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, positionAt, rehome, hop, goHome, HOP_FILE, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, SessionConflictError, SessionStateError, isSessionId, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, beginSession, updateSession, DEVELOPMENT_NEXT_STEP, DEVELOPMENT_MARK, DevelopmentBuildRefusedError, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, SchemaRefusedError, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, activeStoreSettings, normalizeRemote, gitRemoteUrl, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, RebuildRefusal, readConfigFile, loadConfig, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, stubOfPlanFile, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, fileStamp, fix_schema_default, EffortCopyRefusal, EffortCopyFailure, copyEffortStore, minutesBetween, readCommitLog, UnionSchemaMismatch, migrateStore, MOVE_TO_SQLITE, MergeRefusal, mergeStore, createFileSync, SyncModuleMissing, selectSync, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, sessionSpawnEnv, getClaudeUsagePercent, checkUsage, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, spawnClaude, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };