@open-tomato/rafa 0.34.1 → 0.36.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.
@@ -15,7 +15,7 @@ import {
15
15
  parseConfigText,
16
16
  readRafaBlocks,
17
17
  resolveConfig
18
- } from "./index-vp0xghk2.js";
18
+ } from "./index-jfas157k.js";
19
19
  import {
20
20
  bless,
21
21
  merge
@@ -255,11 +255,11 @@ function openNdjsonStore(repoRoot) {
255
255
  // src/effort/store/sqlite.ts
256
256
  import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
257
257
  import { dirname as dirname10, join as join9 } from "path";
258
- import { Database } from "bun:sqlite";
258
+ import { Database as Database2 } from "bun:sqlite";
259
259
 
260
260
  // package.json
261
261
  var name = "@open-tomato/rafa";
262
- var version = "0.34.1";
262
+ var version = "0.36.0";
263
263
 
264
264
  // src/cli/version.ts
265
265
  var RAFA_VERSION = version;
@@ -656,6 +656,7 @@ var DEFAULT_PHASE = "task";
656
656
  var RECORD_EXTENSION = ".json";
657
657
  var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
658
658
  var SESSION_STEP_KINDS = Object.freeze(["baseline", "task", "stage", "pre-wrap-up"]);
659
+ var SESSION_STEP_REASONS = Object.freeze(["declared", "trigger", "fallback", "no-module-tests", "stage"]);
659
660
 
660
661
  class SessionRecordError extends Error {
661
662
  file;
@@ -805,10 +806,17 @@ function freezeHop(value) {
805
806
  function freezeFailures(failures) {
806
807
  return Object.freeze(failures.map((failure) => Object.freeze({ file: failure.file, name: failure.name })));
807
808
  }
809
+ function isSessionStepReason(value) {
810
+ return SESSION_STEP_REASONS.includes(value);
811
+ }
812
+ function reasonEntry(reason) {
813
+ return isSessionStepReason(reason) ? { reason } : {};
814
+ }
808
815
  function freezeStep(step) {
809
816
  return Object.freeze({
810
817
  kind: step.kind,
811
818
  scope: typeof step.scope === "string" ? step.scope : Object.freeze([...step.scope]),
819
+ ...reasonEntry(field(step, "reason")),
812
820
  command: Object.freeze([...step.command]),
813
821
  exitCode: step.exitCode,
814
822
  summary: step.summary,
@@ -1469,6 +1477,20 @@ var SQLITE_MIGRATIONS = [
1469
1477
  recorded_at TEXT NOT NULL CHECK (recorded_at <> '')
1470
1478
  );
1471
1479
  `
1480
+ },
1481
+ {
1482
+ id: "store-meta-generation",
1483
+ breaks: [],
1484
+ sql: `
1485
+ ALTER TABLE store_meta ADD COLUMN generation TEXT CHECK (generation <> '');
1486
+ `
1487
+ },
1488
+ {
1489
+ id: "session-worktree",
1490
+ breaks: [],
1491
+ sql: `
1492
+ ALTER TABLE sessions ADD COLUMN worktree TEXT;
1493
+ `
1472
1494
  }
1473
1495
  ];
1474
1496
  var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
@@ -1700,13 +1722,55 @@ function activeStoreSettings() {
1700
1722
  }
1701
1723
 
1702
1724
  // src/effort/store/store-meta.ts
1703
- import { randomUUID as randomUUID3 } from "crypto";
1725
+ import { randomUUID as randomUUID4 } from "crypto";
1704
1726
  import { dirname as dirname9 } from "path";
1727
+ import { Database } from "bun:sqlite";
1728
+
1729
+ // src/effort/store/store-generation.ts
1730
+ import { randomUUID as randomUUID3 } from "crypto";
1731
+ import { readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1732
+ var STORE_GENERATION_SUFFIX = ".generation";
1733
+ function storeGenerationPath(storePath) {
1734
+ return `${storePath}${STORE_GENERATION_SUFFIX}`;
1735
+ }
1736
+ function isMissingFile(error) {
1737
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1738
+ }
1739
+ function readStoreGeneration(storePath) {
1740
+ let text;
1741
+ try {
1742
+ text = readFileSync6(storeGenerationPath(storePath), "utf8");
1743
+ } catch (error) {
1744
+ if (isMissingFile(error))
1745
+ return null;
1746
+ throw error;
1747
+ }
1748
+ const generation = text.trim();
1749
+ return generation === "" ? null : generation;
1750
+ }
1751
+ function assertWritableGeneration(generation) {
1752
+ if (generation === "" || generation !== generation.trim() || /[\r\n]/.test(generation)) {
1753
+ throw new Error(`store generation must be non-empty, unpadded and on one line, got ${JSON.stringify(generation)}`);
1754
+ }
1755
+ }
1756
+ function writeStoreGeneration(storePath, generation) {
1757
+ assertWritableGeneration(generation);
1758
+ const file = storeGenerationPath(storePath);
1759
+ const temporary = `${file}.${String(process.pid)}.${randomUUID3()}.tmp`;
1760
+ try {
1761
+ writeFileSync4(temporary, `${generation}
1762
+ `, { encoding: "utf8", flag: "wx" });
1763
+ renameSync4(temporary, file);
1764
+ } catch (error) {
1765
+ rmSync4(temporary, { force: true });
1766
+ throw error;
1767
+ }
1768
+ }
1705
1769
 
1706
1770
  // src/effort/store/store-identity.ts
1707
1771
  import { spawnSync as spawnSync3 } from "child_process";
1708
1772
  import { createHmac } from "crypto";
1709
- import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync } from "fs";
1773
+ import { readFileSync as readFileSync7, realpathSync as realpathSync4, statSync } from "fs";
1710
1774
  import { hostname, platform as currentPlatform } from "os";
1711
1775
 
1712
1776
  // src/schema/project-id.ts
@@ -1744,7 +1808,7 @@ function hostIdFrom(source, value) {
1744
1808
  }
1745
1809
  function readMachineIdFile() {
1746
1810
  try {
1747
- const value = readFileSync6(MACHINE_ID_PATH, "utf8").trim();
1811
+ const value = readFileSync7(MACHINE_ID_PATH, "utf8").trim();
1748
1812
  return value === "" ? null : value;
1749
1813
  } catch {
1750
1814
  return null;
@@ -1780,14 +1844,19 @@ function readStoreFileFacts(path) {
1780
1844
  return { storePath, fileDev: stats.dev, fileIno: stats.ino };
1781
1845
  }
1782
1846
  function observeStore(path, readHost = readHostId) {
1783
- return { hostId: readHost(), ...readStoreFileFacts(path) };
1847
+ const fileFacts = readStoreFileFacts(path);
1848
+ return { hostId: readHost(), ...fileFacts, generation: readStoreGeneration(fileFacts.storePath) };
1849
+ }
1850
+ function staleGeneration(recorded, facts) {
1851
+ return recorded.generation !== null && recorded.generation !== facts.generation;
1784
1852
  }
1785
1853
  function movedFacts(recorded, facts) {
1786
1854
  const sameFile = BigInt(recorded.fileDev) === facts.fileDev && BigInt(recorded.fileIno) === facts.fileIno;
1787
1855
  const moved = [
1788
1856
  ["host", recorded.hostId !== facts.hostId],
1789
1857
  ["path", recorded.storePath !== facts.storePath],
1790
- ["file", !sameFile]
1858
+ ["file", !sameFile],
1859
+ ["generation", staleGeneration(recorded, facts)]
1791
1860
  ];
1792
1861
  return moved.filter(([, differs]) => differs).map(([reason]) => reason);
1793
1862
  }
@@ -1827,8 +1896,12 @@ function toStoredInteger(value) {
1827
1896
  function fromStoredInteger(text) {
1828
1897
  return BigInt.asUintN(STORED_INTEGER_BITS, BigInt(text));
1829
1898
  }
1899
+ function hasGenerationColumn(db) {
1900
+ return db.query("SELECT count(*) AS n FROM pragma_table_info('store_meta') WHERE name = 'generation'").get()?.n === 1;
1901
+ }
1830
1902
  function readStoreMeta(db) {
1831
- const row = db.query("SELECT store_id, project_root_commit, project_remote, host_id, store_path," + " CAST(file_dev AS TEXT) AS file_dev, CAST(file_ino AS TEXT) AS file_ino, minted_at" + " FROM store_meta WHERE id = 1").get();
1903
+ const generation = hasGenerationColumn(db) ? "generation" : "NULL AS generation";
1904
+ const row = db.query("SELECT store_id, project_root_commit, project_remote, host_id, store_path," + " CAST(file_dev AS TEXT) AS file_dev, CAST(file_ino AS TEXT) AS file_ino, minted_at," + ` ${generation} FROM store_meta WHERE id = 1`).get();
1832
1905
  if (row === null)
1833
1906
  return null;
1834
1907
  return {
@@ -1839,7 +1912,8 @@ function readStoreMeta(db) {
1839
1912
  storePath: row.store_path,
1840
1913
  fileDev: fromStoredInteger(row.file_dev),
1841
1914
  fileIno: fromStoredInteger(row.file_ino),
1842
- mintedAt: row.minted_at
1915
+ mintedAt: row.minted_at,
1916
+ generation: row.generation
1843
1917
  };
1844
1918
  }
1845
1919
  function projectFor(storePath, recorded, seams) {
@@ -1850,14 +1924,30 @@ function projectFor(storePath, recorded, seams) {
1850
1924
  return null;
1851
1925
  return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
1852
1926
  }
1853
- function writeStoreMeta(db, facts, project, seams) {
1854
- const storeId = (seams.newStoreId ?? randomUUID3)();
1927
+ function writeStoreMeta(db, facts, project, generation, seams) {
1928
+ const storeId = (seams.newStoreId ?? randomUUID4)();
1855
1929
  const mintedAt = (seams.now?.() ?? new Date).toISOString();
1856
- db.query("INSERT OR REPLACE INTO store_meta (id, store_id, project_root_commit, project_remote," + " host_id, store_path, file_dev, file_ino, minted_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?)").run(storeId, project.rootCommit, project.remote, facts.hostId, facts.storePath, toStoredInteger(facts.fileDev), toStoredInteger(facts.fileIno), mintedAt);
1930
+ db.query("INSERT OR REPLACE INTO store_meta (id, store_id, project_root_commit, project_remote," + " host_id, store_path, file_dev, file_ino, minted_at, generation) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(storeId, project.rootCommit, project.remote, facts.hostId, facts.storePath, toStoredInteger(facts.fileDev), toStoredInteger(facts.fileIno), mintedAt, generation);
1857
1931
  return storeId;
1858
1932
  }
1859
- function unminted(decision) {
1860
- return decision.action === "keep" ? { action: "keep", storeId: decision.storeId } : { action: "none" };
1933
+ function settleUnderLock(db, observe, prefetched, seams) {
1934
+ const current = readStoreMeta(db);
1935
+ const facts = observe();
1936
+ const decision = decideStoreIdentity("write", current, () => facts);
1937
+ if (decision.action === "none")
1938
+ return { action: "none" };
1939
+ const generation = (seams.newGeneration ?? randomUUID4)();
1940
+ if (decision.action === "keep") {
1941
+ db.query("UPDATE store_meta SET generation = ? WHERE id = 1").run(generation);
1942
+ writeStoreGeneration(facts.storePath, generation);
1943
+ return { action: "keep", storeId: decision.storeId };
1944
+ }
1945
+ const project = prefetched ?? projectFor(facts.storePath, current, seams);
1946
+ if (project === null)
1947
+ return { action: "no-project", reasons: decision.reasons };
1948
+ const storeId = writeStoreMeta(db, facts, project, generation, seams);
1949
+ writeStoreGeneration(facts.storePath, generation);
1950
+ return { action: "mint", storeId, reasons: decision.reasons };
1861
1951
  }
1862
1952
  function settleStoreIdentity(db, path, access, seams = {}) {
1863
1953
  if (access === "read")
@@ -1865,19 +1955,72 @@ function settleStoreIdentity(db, path, access, seams = {}) {
1865
1955
  const observe = () => observeStore(path, seams.readHostId ?? readHostId);
1866
1956
  const recorded = readStoreMeta(db);
1867
1957
  const first = decideStoreIdentity(access, recorded, observe);
1868
- if (first.action !== "mint")
1869
- return unminted(first);
1870
- const project = projectFor(first.facts.storePath, recorded, seams);
1871
- if (project === null)
1958
+ const prefetched = first.action === "mint" ? projectFor(first.facts.storePath, recorded, seams) : undefined;
1959
+ if (first.action === "mint" && prefetched === null)
1872
1960
  return { action: "no-project", reasons: first.reasons };
1873
- const mintUnderLock = db.transaction(() => {
1874
- const second = decideStoreIdentity(access, readStoreMeta(db), observe);
1875
- if (second.action !== "mint")
1876
- return unminted(second);
1877
- const storeId = writeStoreMeta(db, second.facts, project, seams);
1878
- return { action: "mint", storeId, reasons: second.reasons };
1879
- });
1880
- return mintUnderLock.immediate();
1961
+ const underLock = db.transaction(() => settleUnderLock(db, observe, prefetched ?? undefined, seams));
1962
+ return underLock.immediate();
1963
+ }
1964
+ function hasStoreMetaTable(db) {
1965
+ return db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
1966
+ }
1967
+ function decideLiveWrite(livePath, readHost) {
1968
+ const live = new Database(livePath, { readonly: true });
1969
+ let recorded;
1970
+ try {
1971
+ if (!hasStoreMetaTable(live))
1972
+ return { action: "no-table" };
1973
+ recorded = readStoreMeta(live);
1974
+ } finally {
1975
+ live.close();
1976
+ }
1977
+ if (recorded === null)
1978
+ return { action: "no-row" };
1979
+ const facts = observeStore(livePath, readHost);
1980
+ const decision = decideStoreIdentity("write", recorded, () => facts);
1981
+ if (decision.action === "keep")
1982
+ return { action: "keep", storeId: decision.storeId };
1983
+ const reasons = decision.action === "mint" ? decision.reasons : [];
1984
+ return { action: "mint", reasons };
1985
+ }
1986
+ function holdsRowOf(db, storeId) {
1987
+ return hasStoreMetaTable(db) && readStoreMeta(db)?.storeId === storeId;
1988
+ }
1989
+ function spoilGeneration(db, newGeneration) {
1990
+ if (!hasStoreMetaTable(db) || !hasGenerationColumn(db))
1991
+ return null;
1992
+ const rows = db.query("SELECT count(*) AS n FROM store_meta WHERE id = 1").get()?.n;
1993
+ if (rows !== 1)
1994
+ return null;
1995
+ const generation = newGeneration();
1996
+ db.query("UPDATE store_meta SET generation = ? WHERE id = 1").run(generation);
1997
+ return generation;
1998
+ }
1999
+ function writeCarried(db, storeId, parallel, newGeneration) {
2000
+ const dev = toStoredInteger(parallel.fileDev);
2001
+ const ino = toStoredInteger(parallel.fileIno);
2002
+ if (hasGenerationColumn(db)) {
2003
+ const generation = newGeneration();
2004
+ db.query("UPDATE store_meta SET file_dev = ?, file_ino = ?, generation = ? WHERE id = 1").run(dev, ino, generation);
2005
+ return { action: "carried", storeId, generation };
2006
+ }
2007
+ db.query("UPDATE store_meta SET file_dev = ?, file_ino = ? WHERE id = 1").run(dev, ino);
2008
+ return { action: "carried", storeId, generation: null };
2009
+ }
2010
+ function carryStoreIdentity(livePath, parallelPath, seams = {}) {
2011
+ const parallel = readStoreFileFacts(parallelPath);
2012
+ const live = decideLiveWrite(livePath, seams.readHostId ?? readHostId);
2013
+ const newGeneration = seams.newGeneration ?? randomUUID4;
2014
+ const db = new Database(parallelPath, { readwrite: true });
2015
+ try {
2016
+ if (live.action === "keep" && holdsRowOf(db, live.storeId)) {
2017
+ return writeCarried(db, live.storeId, parallel, newGeneration);
2018
+ }
2019
+ const spoiled = spoilGeneration(db, newGeneration);
2020
+ return live.action === "keep" ? { action: "other-row", spoiled } : { ...live, spoiled };
2021
+ } finally {
2022
+ db.close();
2023
+ }
1881
2024
  }
1882
2025
 
1883
2026
  // src/effort/store/sqlite.ts
@@ -1886,8 +2029,12 @@ function sqliteStorePath(repoRoot) {
1886
2029
  return join9(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
1887
2030
  }
1888
2031
  var KIND_TABLES = {
1889
- sessions: { name: "sessions", keyColumn: "session_id" },
1890
- commits: { name: "commits", keyColumn: "sha" }
2032
+ sessions: {
2033
+ name: "sessions",
2034
+ keyColumn: "session_id",
2035
+ fieldColumns: [{ column: "worktree", value: (row) => row.worktree ?? null }]
2036
+ },
2037
+ commits: { name: "commits", keyColumn: "sha", fieldColumns: [] }
1891
2038
  };
1892
2039
  var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
1893
2040
  function hasKey(entry) {
@@ -1898,6 +2045,7 @@ function refused(index, length, reason) {
1898
2045
  }
1899
2046
  function batchEntries(kind, rows) {
1900
2047
  const keyOf = EFFORT_KEY_PROJECTIONS[kind];
2048
+ const fieldColumns = KIND_TABLES[kind].fieldColumns;
1901
2049
  const projected = rows.map((row) => ({ key: keyOf(row), row }));
1902
2050
  const keyed = projected.filter(hasKey);
1903
2051
  if (keyed.length < projected.length) {
@@ -1909,13 +2057,17 @@ function batchEntries(kind, rows) {
1909
2057
  const reason = "carries a key with a lone UTF-16 surrogate," + " which SQLite text cannot hold";
1910
2058
  throw refused(unholdableAt, rows.length, reason);
1911
2059
  }
1912
- return keyed.map(({ key, row }) => ({ key, body: JSON.stringify(row) }));
2060
+ return keyed.map(({ key, row }) => ({
2061
+ key,
2062
+ body: JSON.stringify(row),
2063
+ fields: fieldColumns.map(({ value }) => value(row))
2064
+ }));
1913
2065
  }
1914
2066
  function withSqliteStore(path, access, create, use, identity = {}) {
1915
2067
  guardTestProcess(path);
1916
2068
  if (create)
1917
2069
  mkdirSync5(dirname10(path), { recursive: true });
1918
- const db = new Database(path, { readwrite: true, create });
2070
+ const db = new Database2(path, { readwrite: true, create });
1919
2071
  try {
1920
2072
  db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
1921
2073
  bringForward(db, path, access, "open");
@@ -1926,8 +2078,11 @@ function withSqliteStore(path, access, create, use, identity = {}) {
1926
2078
  }
1927
2079
  }
1928
2080
  function insertBatch(db, table, entries) {
1929
- const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json, ${STAMPED_COLUMNS})` + ` VALUES (?, ?, ${stampedValues(table.name)})` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
1930
- const insertAll = db.transaction(() => entries.reduce((appended, { key, body }) => appended + insert.run(key, body).changes, 0));
2081
+ const fieldNames = table.fieldColumns.map(({ column }) => column);
2082
+ const columns = [table.keyColumn, "row_json", ...fieldNames].join(", ");
2083
+ const placeholders = ["?", "?", ...fieldNames.map(() => "?")].join(", ");
2084
+ const insert = db.query(`INSERT INTO ${table.name} (${columns}, ${STAMPED_COLUMNS})` + ` VALUES (${placeholders}, ${stampedValues(table.name)})` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
2085
+ const insertAll = db.transaction(() => entries.reduce((appended, { key, body, fields }) => appended + insert.run(key, body, ...fields).changes, 0));
1931
2086
  return insertAll.immediate();
1932
2087
  }
1933
2088
  function writeSqliteStore(path, rowCount, empty, insert) {
@@ -1990,15 +2145,105 @@ import { existsSync as existsSync12 } from "fs";
1990
2145
  import { join as join12, resolve as resolve5 } from "path";
1991
2146
 
1992
2147
  // src/effort/store/fix-schema.ts
1993
- import { existsSync as existsSync7 } from "fs";
1994
- import { Database as Database2 } from "bun:sqlite";
2148
+ import { existsSync as existsSync8 } from "fs";
2149
+ import { Database as Database4 } from "bun:sqlite";
1995
2150
 
1996
2151
  // src/effort/store/rebuild-aside.ts
1997
- import { existsSync as existsSync6, renameSync as renameSync4, rmSync as rmSync4 } from "fs";
2152
+ import { closeSync, existsSync as existsSync7, fsyncSync, openSync, renameSync as renameSync5, rmSync as rmSync6 } from "fs";
2153
+
2154
+ // src/effort/store/copy.ts
2155
+ import { copyFileSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync2, rmSync as rmSync5, statSync as statSync2 } from "fs";
2156
+ import { join as join10, resolve as resolve3 } from "path";
2157
+ import { Database as Database3 } from "bun:sqlite";
2158
+ var NDJSON_KINDS = { sessions: true, commits: true };
2159
+ var EFFORT_COPY_FILE_NAMES = [
2160
+ SQLITE_STORE_FILE_NAME,
2161
+ ...Object.keys(NDJSON_KINDS).map((kind) => effortStoreFileName(kind))
2162
+ ];
2163
+
2164
+ class EffortCopyRefusal extends Error {
2165
+ name = "EffortCopyRefusal";
2166
+ }
1998
2167
 
2168
+ class EffortCopyFailure extends Error {
2169
+ name = "EffortCopyFailure";
2170
+ }
2171
+ function refuseUsedTarget(target) {
2172
+ if (!existsSync6(target))
2173
+ return;
2174
+ let entries;
2175
+ try {
2176
+ if (!statSync2(target).isDirectory()) {
2177
+ throw new EffortCopyRefusal(`${target} is a file, not a directory; name an empty or absent directory with --to=<dir>.`);
2178
+ }
2179
+ entries = readdirSync2(target);
2180
+ } catch (error) {
2181
+ if (error instanceof EffortCopyRefusal)
2182
+ throw error;
2183
+ throw new EffortCopyFailure(`${target} cannot be read: ${String(error)}`);
2184
+ }
2185
+ if (entries.length === 0)
2186
+ return;
2187
+ throw new EffortCopyRefusal(`${target} is not empty (${String(entries.length)} entries); nothing was copied.` + " Name an empty or absent directory with --to=<dir>.");
2188
+ }
2189
+ function vacuumInto(source, target) {
2190
+ const db = new Database3(source, { readonly: true });
2191
+ try {
2192
+ db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
2193
+ db.run("VACUUM INTO ?", [target]);
2194
+ } finally {
2195
+ db.close();
2196
+ }
2197
+ }
2198
+ function copyOne(name2, source, target) {
2199
+ if (name2 === SQLITE_STORE_FILE_NAME) {
2200
+ vacuumInto(join10(source, name2), join10(target, name2));
2201
+ return;
2202
+ }
2203
+ copyFileSync(join10(source, name2), join10(target, name2), constants.COPYFILE_EXCL);
2204
+ }
2205
+ function undo(target, written, madeDir) {
2206
+ try {
2207
+ if (madeDir === undefined) {
2208
+ for (const name2 of written)
2209
+ rmSync5(join10(target, name2), { force: true });
2210
+ } else {
2211
+ rmSync5(madeDir, { recursive: true, force: true });
2212
+ }
2213
+ return "";
2214
+ } catch (error) {
2215
+ return ` Removing what it made failed too: ${String(error)}`;
2216
+ }
2217
+ }
2218
+ function copyEffortStore(request) {
2219
+ const source = resolve3(request.source);
2220
+ const target = resolve3(request.target);
2221
+ guardTestProcess(source);
2222
+ guardTestProcess(target);
2223
+ const files = EFFORT_COPY_FILE_NAMES.filter((name2) => existsSync6(join10(source, name2)));
2224
+ if (files.length === 0) {
2225
+ throw new EffortCopyRefusal(`no effort store at ${source}: it holds none of ${EFFORT_COPY_FILE_NAMES.join(", ")}. Nothing to copy.`);
2226
+ }
2227
+ refuseUsedTarget(target);
2228
+ const written = [];
2229
+ let madeDir;
2230
+ try {
2231
+ madeDir = mkdirSync6(target, { recursive: true });
2232
+ for (const name2 of files) {
2233
+ written.push(name2);
2234
+ copyOne(name2, source, target);
2235
+ }
2236
+ } catch (error) {
2237
+ const left = undo(target, written, madeDir);
2238
+ throw new EffortCopyFailure(`copying ${source} to ${target} failed: ${String(error)}.${left}`);
2239
+ }
2240
+ return Object.freeze({ source, directory: target, files: Object.freeze(files) });
2241
+ }
2242
+
2243
+ // src/effort/store/rebuild-aside.ts
1999
2244
  class RebuildRefusal extends Error {
2000
- constructor(message) {
2001
- super(message);
2245
+ constructor(message, options) {
2246
+ super(message, options);
2002
2247
  this.name = "RebuildRefusal";
2003
2248
  }
2004
2249
  }
@@ -2010,7 +2255,7 @@ function count(db, sql) {
2010
2255
  }
2011
2256
  function refuseInFlight(path) {
2012
2257
  for (const suffix of ["-journal", "-wal"]) {
2013
- if (existsSync6(`${path}${suffix}`)) {
2258
+ if (existsSync7(`${path}${suffix}`)) {
2014
2259
  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");
2015
2260
  }
2016
2261
  }
@@ -2033,21 +2278,78 @@ function refuseCorrupt(db) {
2033
2278
  throw new RebuildRefusal(`the rebuilt store failed integrity_check: ${answer ?? "no answer"}`);
2034
2279
  }
2035
2280
  function removeParallel(parallelPath) {
2036
- rmSync4(parallelPath, { force: true });
2037
- rmSync4(`${parallelPath}-journal`, { force: true });
2281
+ rmSync6(parallelPath, { force: true });
2282
+ rmSync6(`${parallelPath}-journal`, { force: true });
2283
+ }
2284
+ var STEP_TEXT = {
2285
+ backup: ({ path, backupPath }) => ({
2286
+ doing: `writing ${path} out to the backup ${backupPath}`,
2287
+ backup: `any part of the backup written at ${backupPath}`
2288
+ }),
2289
+ carry: ({ path, parallelPath, backupPath }) => ({
2290
+ doing: `carrying the store id of ${path} onto ${parallelPath}`,
2291
+ backup: `the whole backup at ${backupPath}`
2292
+ }),
2293
+ record: ({ path, parallelPath, backupPath }) => ({
2294
+ doing: `writing the generation carried onto ${parallelPath} to the side record of ${path}`,
2295
+ backup: `the whole backup at ${backupPath}`
2296
+ }),
2297
+ rename: ({ path, parallelPath, backupPath }) => ({
2298
+ doing: `renaming ${parallelPath} over ${path}`,
2299
+ backup: `the whole backup at ${backupPath}`
2300
+ })
2301
+ };
2302
+
2303
+ class SwapFailure extends RebuildRefusal {
2304
+ step;
2305
+ parallelPath;
2306
+ backupPath;
2307
+ constructor(step, files, cause) {
2308
+ const { doing, backup } = STEP_TEXT[step](files);
2309
+ const reason = cause instanceof Error ? cause.message : String(cause);
2310
+ super(`${doing} failed (${reason}), so ${files.path} was not replaced and is unchanged.` + ` The rebuilt store is left at ${files.parallelPath}, and ${backup}.`, { cause });
2311
+ this.name = "SwapFailure";
2312
+ this.step = step;
2313
+ this.parallelPath = files.parallelPath;
2314
+ this.backupPath = files.backupPath;
2315
+ }
2316
+ }
2317
+ function writeBackup(path, backupPath) {
2318
+ vacuumInto(path, backupPath);
2319
+ const backup = openSync(backupPath, "r");
2320
+ try {
2321
+ fsyncSync(backup);
2322
+ } finally {
2323
+ closeSync(backup);
2324
+ }
2038
2325
  }
2039
- function swapIn(path, parallelPath, backupPath) {
2040
- renameSync4(path, backupPath);
2326
+ var SWAP_STEPS = {
2327
+ backup: writeBackup,
2328
+ carry: (path, parallelPath) => carryStoreIdentity(path, parallelPath),
2329
+ record: writeStoreGeneration,
2330
+ rename: renameSync5
2331
+ };
2332
+ function runStep(step, files, act) {
2041
2333
  try {
2042
- renameSync4(parallelPath, path);
2334
+ return act();
2043
2335
  } catch (error) {
2044
- renameSync4(backupPath, path);
2045
- throw error;
2336
+ throw new SwapFailure(step, files, error);
2337
+ }
2338
+ }
2339
+ function swapIn(path, parallelPath, backupPath, steps = {}) {
2340
+ const { backup, carry, record, rename } = { ...SWAP_STEPS, ...steps };
2341
+ const files = { path, parallelPath, backupPath };
2342
+ runStep("backup", files, () => backup(path, backupPath));
2343
+ const carried = runStep("carry", files, () => carry(path, parallelPath));
2344
+ if (carried.action === "carried" && carried.generation !== null) {
2345
+ const { generation } = carried;
2346
+ runStep("record", files, () => record(path, generation));
2046
2347
  }
2348
+ runStep("rename", files, () => rename(parallelPath, path));
2047
2349
  }
2048
2350
  function refuseTaken(paths) {
2049
2351
  for (const taken of paths) {
2050
- if (existsSync6(taken))
2352
+ if (existsSync7(taken))
2051
2353
  throw new RebuildRefusal(`${taken} is already there; move it aside or pass another stamp`);
2052
2354
  }
2053
2355
  }
@@ -2086,7 +2388,7 @@ function columnNames(db, schema, table) {
2086
2388
  return db.query("SELECT name FROM pragma_table_info(?, ?)").all(table, schema).map((row) => row.name);
2087
2389
  }
2088
2390
  function readOnlySchema(path) {
2089
- const db = new Database2(path, { readonly: true });
2391
+ const db = new Database4(path, { readonly: true });
2090
2392
  try {
2091
2393
  return readStoreSchema(db, path);
2092
2394
  } finally {
@@ -2134,7 +2436,7 @@ function refuseUnusable(db, parallelPath, migrations) {
2134
2436
  throw new RebuildRefusal(`the rebuilt store still needs ${needs.join(", ")}`);
2135
2437
  }
2136
2438
  function buildParallel(path, parallelPath, migrations, stamp) {
2137
- const db = new Database2(parallelPath, { create: true, readwrite: true });
2439
+ const db = new Database4(parallelPath, { create: true, readwrite: true });
2138
2440
  try {
2139
2441
  bringForward(db, parallelPath, "write", "open", { migrations, ...stamp, builtAside: true });
2140
2442
  db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
@@ -2171,7 +2473,7 @@ function fixStoreSchema(options) {
2171
2473
  const appliedBy = appliedByName(identity);
2172
2474
  const known = migrations.map(({ id }) => id);
2173
2475
  const nothing = { known, appliedBy, pending: [], kept: [], leftTables: [], leftColumns: [], backupPath: null };
2174
- if (!existsSync7(path)) {
2476
+ if (!existsSync8(path)) {
2175
2477
  return { path, status: "missing", storeVersion: null, logged: false, reason: null, refusal: null, unknown: [], ...nothing };
2176
2478
  }
2177
2479
  refuseInFlight(path);
@@ -2213,19 +2515,19 @@ function withoutWayOut(message, nextStep) {
2213
2515
  }
2214
2516
 
2215
2517
  // src/commands/plan/plan-files.ts
2216
- import { statSync as statSync2 } from "fs";
2217
- import { resolve as resolve3 } from "path";
2518
+ import { statSync as statSync3 } from "fs";
2519
+ import { resolve as resolve4 } from "path";
2218
2520
 
2219
2521
  // src/config-load.ts
2220
- import { existsSync as existsSync8, readFileSync as readFileSync7, realpathSync as realpathSync5 } from "fs";
2522
+ import { existsSync as existsSync9, readFileSync as readFileSync8, realpathSync as realpathSync5 } from "fs";
2221
2523
  import { isAbsolute as isAbsolute6 } from "path";
2222
2524
  function readConfigFile(root) {
2223
2525
  const path = configFilePath(root);
2224
- if (!existsSync8(path))
2526
+ if (!existsSync9(path))
2225
2527
  return null;
2226
2528
  let text;
2227
2529
  try {
2228
- text = readFileSync7(path, "utf8");
2530
+ text = readFileSync8(path, "utf8");
2229
2531
  } catch (error) {
2230
2532
  const problem = `${path}: cannot be read (${messageOf(error)})`;
2231
2533
  throw new ConfigError([problem], { cause: error });
@@ -2262,7 +2564,7 @@ function loadConfig(roots, cli = {}, warn = printWarning) {
2262
2564
 
2263
2565
  // src/commands/plan/plan-files.ts
2264
2566
  function plansDirAt(root, planDir) {
2265
- return { label: planDir, path: resolve3(root, planDir) };
2567
+ return { label: planDir, path: resolve4(root, planDir) };
2266
2568
  }
2267
2569
  function requireProject(context, command) {
2268
2570
  if (context.project === null)
@@ -2309,7 +2611,7 @@ function stubOfPlanFile(name2) {
2309
2611
  return stub !== undefined && isStampableStub(stub) ? stub : null;
2310
2612
  }
2311
2613
  function isFile(path) {
2312
- return statSync2(path, { throwIfNoEntry: false })?.isFile() ?? false;
2614
+ return statSync3(path, { throwIfNoEntry: false })?.isFile() ?? false;
2313
2615
  }
2314
2616
  function issueLine(file, issue) {
2315
2617
  return `${file}:${issue.line}: ${issue.reason}: ${issue.text}`;
@@ -2355,6 +2657,9 @@ var DRY_RUN_FLAG = "dry-run";
2355
2657
  function fileStamp(date) {
2356
2658
  return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
2357
2659
  }
2660
+ function keepsIdLine(swapped) {
2661
+ return `The ${swapped} keeps the store's id; the backup is a copy, so renamed back it takes a new id on its` + " next write.";
2662
+ }
2358
2663
  function refuseLiveLoop(root, isAlive) {
2359
2664
  let records;
2360
2665
  try {
@@ -2412,7 +2717,7 @@ function renderFixSchema(result) {
2412
2717
  case "rebuilt":
2413
2718
  return [
2414
2719
  ...rebuildLines(result),
2415
- `\u2705 Rebuilt at this rafa's migrations. The original is kept whole at ${String(result.backupPath)};` + " rename it back to undo."
2720
+ `\u2705 Rebuilt at this rafa's migrations. The original is kept whole at ${String(result.backupPath)};` + ` rename it back to undo. ${keepsIdLine("rebuild")}`
2416
2721
  ];
2417
2722
  }
2418
2723
  }
@@ -2452,7 +2757,7 @@ function createFixSchemaCommand(seams = {}) {
2452
2757
  subject: "effort",
2453
2758
  action: "fix-schema",
2454
2759
  summary: "rebuild an effort store this rafa refuses at the migrations it knows, keeping the original",
2455
- 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.",
2760
+ 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 writes the original to `effort.sqlite.v<user_version>-<stamp>.bak`," + " whole, and moves the rebuild into its place. The rebuild keeps the store's id; the backup is a copy," + " so renamed back it takes a new id on its next write. 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.",
2456
2761
  args: [],
2457
2762
  flags: [
2458
2763
  {
@@ -2481,101 +2786,12 @@ function createFixSchemaCommand(seams = {}) {
2481
2786
  }
2482
2787
  var fix_schema_default = createFixSchemaCommand();
2483
2788
 
2484
- // src/effort/store/copy.ts
2485
- import { copyFileSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync2, rmSync as rmSync5, statSync as statSync3 } from "fs";
2486
- import { join as join10, resolve as resolve4 } from "path";
2487
- import { Database as Database3 } from "bun:sqlite";
2488
- var NDJSON_KINDS = { sessions: true, commits: true };
2489
- var EFFORT_COPY_FILE_NAMES = [
2490
- SQLITE_STORE_FILE_NAME,
2491
- ...Object.keys(NDJSON_KINDS).map((kind) => effortStoreFileName(kind))
2492
- ];
2493
-
2494
- class EffortCopyRefusal extends Error {
2495
- name = "EffortCopyRefusal";
2496
- }
2497
-
2498
- class EffortCopyFailure extends Error {
2499
- name = "EffortCopyFailure";
2500
- }
2501
- function refuseUsedTarget(target) {
2502
- if (!existsSync9(target))
2503
- return;
2504
- let entries;
2505
- try {
2506
- if (!statSync3(target).isDirectory()) {
2507
- throw new EffortCopyRefusal(`${target} is a file, not a directory; name an empty or absent directory with --to=<dir>.`);
2508
- }
2509
- entries = readdirSync2(target);
2510
- } catch (error) {
2511
- if (error instanceof EffortCopyRefusal)
2512
- throw error;
2513
- throw new EffortCopyFailure(`${target} cannot be read: ${String(error)}`);
2514
- }
2515
- if (entries.length === 0)
2516
- return;
2517
- throw new EffortCopyRefusal(`${target} is not empty (${String(entries.length)} entries); nothing was copied.` + " Name an empty or absent directory with --to=<dir>.");
2518
- }
2519
- function vacuumInto(source, target) {
2520
- const db = new Database3(source, { readonly: true });
2521
- try {
2522
- db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
2523
- db.run("VACUUM INTO ?", [target]);
2524
- } finally {
2525
- db.close();
2526
- }
2527
- }
2528
- function copyOne(name2, source, target) {
2529
- if (name2 === SQLITE_STORE_FILE_NAME) {
2530
- vacuumInto(join10(source, name2), join10(target, name2));
2531
- return;
2532
- }
2533
- copyFileSync(join10(source, name2), join10(target, name2), constants.COPYFILE_EXCL);
2534
- }
2535
- function undo(target, written, madeDir) {
2536
- try {
2537
- if (madeDir === undefined) {
2538
- for (const name2 of written)
2539
- rmSync5(join10(target, name2), { force: true });
2540
- } else {
2541
- rmSync5(madeDir, { recursive: true, force: true });
2542
- }
2543
- return "";
2544
- } catch (error) {
2545
- return ` Removing what it made failed too: ${String(error)}`;
2546
- }
2547
- }
2548
- function copyEffortStore(request) {
2549
- const source = resolve4(request.source);
2550
- const target = resolve4(request.target);
2551
- guardTestProcess(source);
2552
- guardTestProcess(target);
2553
- const files = EFFORT_COPY_FILE_NAMES.filter((name2) => existsSync9(join10(source, name2)));
2554
- if (files.length === 0) {
2555
- throw new EffortCopyRefusal(`no effort store at ${source}: it holds none of ${EFFORT_COPY_FILE_NAMES.join(", ")}. Nothing to copy.`);
2556
- }
2557
- refuseUsedTarget(target);
2558
- const written = [];
2559
- let madeDir;
2560
- try {
2561
- madeDir = mkdirSync6(target, { recursive: true });
2562
- for (const name2 of files) {
2563
- written.push(name2);
2564
- copyOne(name2, source, target);
2565
- }
2566
- } catch (error) {
2567
- const left = undo(target, written, madeDir);
2568
- throw new EffortCopyFailure(`copying ${source} to ${target} failed: ${String(error)}.${left}`);
2569
- }
2570
- return Object.freeze({ source, directory: target, files: Object.freeze(files) });
2571
- }
2572
-
2573
2789
  // src/effort/store/merge-store.ts
2574
- import { randomUUID as randomUUID4 } from "crypto";
2575
- import { existsSync as existsSync11, mkdtempSync, rmSync as rmSync6 } from "fs";
2790
+ import { randomUUID as randomUUID5 } from "crypto";
2791
+ import { existsSync as existsSync11, mkdtempSync, rmSync as rmSync7 } from "fs";
2576
2792
  import { tmpdir as tmpdir3 } from "os";
2577
2793
  import { dirname as dirname11, join as join11 } from "path";
2578
- import { Database as Database5, SQLiteError as SQLiteError2 } from "bun:sqlite";
2794
+ import { Database as Database6, SQLiteError as SQLiteError2 } from "bun:sqlite";
2579
2795
 
2580
2796
  // src/effort/commits.ts
2581
2797
  import { execFileSync } from "child_process";
@@ -2986,7 +3202,7 @@ function settleMatches(local, other, unions, trail) {
2986
3202
 
2987
3203
  // src/effort/store/migrate.ts
2988
3204
  import { existsSync as existsSync10 } from "fs";
2989
- import { Database as Database4 } from "bun:sqlite";
3205
+ import { Database as Database5 } from "bun:sqlite";
2990
3206
  var LIVE2 = "live";
2991
3207
  function asidePaths2(path, first, stamp) {
2992
3208
  return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
@@ -3014,7 +3230,7 @@ function refuseLiveLoop2(path, breaking, options) {
3014
3230
  }
3015
3231
  function buildMigrated(path, parallelPath, migrations, stamp) {
3016
3232
  vacuumInto(path, parallelPath);
3017
- const db = new Database4(parallelPath, { readwrite: true });
3233
+ const db = new Database5(parallelPath, { readwrite: true });
3018
3234
  try {
3019
3235
  const brought = bringForward(db, parallelPath, "write", "migrate", { migrations, ...stamp, builtAside: true });
3020
3236
  db.run(`ATTACH DATABASE ? AS ${LIVE2}`, [path]);
@@ -3098,7 +3314,7 @@ function refuseMissing(path, which) {
3098
3314
  function refuseDamaged(path) {
3099
3315
  let answers;
3100
3316
  try {
3101
- const db = new Database5(path, { readonly: true });
3317
+ const db = new Database6(path, { readonly: true });
3102
3318
  try {
3103
3319
  answers = db.query("PRAGMA integrity_check").all().map((row) => row.integrity_check);
3104
3320
  } finally {
@@ -3120,7 +3336,7 @@ function writablePlan(path, migrations) {
3120
3336
  return plan;
3121
3337
  }
3122
3338
  function recordedMeta(path) {
3123
- const db = new Database5(path, { readonly: true });
3339
+ const db = new Database6(path, { readonly: true });
3124
3340
  try {
3125
3341
  const hasTable = db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
3126
3342
  return hasTable ? readStoreMeta(db) : null;
@@ -3146,18 +3362,18 @@ function refuseLiveLoop3(path, options, sessionId) {
3146
3362
  function openOther(otherPath, plan, stamp) {
3147
3363
  const needs = writeNames(plan);
3148
3364
  if (needs.length === 0) {
3149
- const db = new Database5(otherPath, { readonly: true });
3365
+ const db = new Database6(otherPath, { readonly: true });
3150
3366
  return { db, broughtForward: [], release: () => db.close() };
3151
3367
  }
3152
3368
  const dir = mkdtempSync(join11(tmpdir3(), "rafa-merge-other-"));
3153
3369
  const release = (db) => {
3154
3370
  db?.close();
3155
- rmSync6(dir, { recursive: true, force: true });
3371
+ rmSync7(dir, { recursive: true, force: true });
3156
3372
  };
3157
3373
  try {
3158
3374
  const copyPath = join11(dir, "other.sqlite");
3159
3375
  vacuumInto(otherPath, copyPath);
3160
- const db = new Database5(copyPath, { readwrite: true });
3376
+ const db = new Database6(copyPath, { readwrite: true });
3161
3377
  try {
3162
3378
  bringForward(db, copyPath, "write", "open", { ...stamp, builtAside: true });
3163
3379
  } catch (error) {
@@ -3201,7 +3417,7 @@ function addedRows(merged) {
3201
3417
  }
3202
3418
  function buildMerged(path, parallelPath, other, merge2) {
3203
3419
  vacuumInto(path, parallelPath);
3204
- const db = new Database5(parallelPath, { readwrite: true });
3420
+ const db = new Database6(parallelPath, { readwrite: true });
3205
3421
  try {
3206
3422
  bringForward(db, parallelPath, "write", "open", { ...merge2.stamp, builtAside: true });
3207
3423
  const merged = mergeInto(db, other, merge2.id, merge2.otherStore, merge2.stamp.now().toISOString());
@@ -3244,7 +3460,7 @@ function mergeStore(options) {
3244
3460
  appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
3245
3461
  now: options.now ?? (() => new Date)
3246
3462
  };
3247
- const mergeId = (options.newMergeId ?? randomUUID4)();
3463
+ const mergeId = (options.newMergeId ?? randomUUID5)();
3248
3464
  const other = openOther(otherPath, otherPlan, mergeStamp);
3249
3465
  try {
3250
3466
  const { built, backupPath } = rebuildAside({
@@ -3371,7 +3587,7 @@ import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
3371
3587
  import { join as join15 } from "path";
3372
3588
 
3373
3589
  // src/commands/instinct/instinct-records.ts
3374
- import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
3590
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
3375
3591
  import { join as join14 } from "path";
3376
3592
 
3377
3593
  // src/check/layout.ts
@@ -4024,7 +4240,7 @@ function unreadableIssue(message) {
4024
4240
  function readRecord(path, scope, id) {
4025
4241
  let text;
4026
4242
  try {
4027
- text = readFileSync8(path, "utf8");
4243
+ text = readFileSync9(path, "utf8");
4028
4244
  } catch (error) {
4029
4245
  const message = error instanceof Error ? error.message : String(error);
4030
4246
  return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
@@ -4725,35 +4941,6 @@ function sessionSpawnEnv(base, binDir = bundledBinDirectory()) {
4725
4941
  }
4726
4942
 
4727
4943
  // src/utils/claude.ts
4728
- async function getClaudeUsagePercent() {
4729
- const envPct = process.env["CLAUDE_USAGE_PERCENT"];
4730
- if (envPct !== undefined) {
4731
- const pct = Number(envPct);
4732
- return Number.isFinite(pct) ? Math.max(0, Math.min(100, pct)) : null;
4733
- }
4734
- return null;
4735
- }
4736
- async function checkUsage(context) {
4737
- const pct = await getClaudeUsagePercent();
4738
- if (pct === null)
4739
- return false;
4740
- if (context === "task" && pct >= 90) {
4741
- activeOutput().warn(`
4742
- Claude usage at ${pct.toFixed(0)}% (>=90%). Pausing after current task to avoid hitting the limit.`);
4743
- return true;
4744
- }
4745
- if (pct >= 80) {
4746
- activeOutput().warn(`
4747
- Claude usage at ${pct.toFixed(0)}% (>=80%). Monitor closely \u2014 tasks may be interrupted.`);
4748
- } else if (context === "issue" && pct >= 70) {
4749
- activeOutput().warn(`
4750
- Claude usage at ${pct.toFixed(0)}% (>=70%). Consider whether to start the next issue.`);
4751
- } else {
4752
- activeOutput().info(`
4753
- Claude usage: ${pct.toFixed(0)}%`);
4754
- }
4755
- return false;
4756
- }
4757
4944
  var CLAUDE_BIN = "claude";
4758
4945
  var CLAUDE_BASE_ARGS = [
4759
4946
  "-p",
@@ -4941,7 +5128,8 @@ var TYPE_MEMBERS = {
4941
5128
  adr: true,
4942
5129
  chore: true,
4943
5130
  "package-api": true,
4944
- epic: true
5131
+ epic: true,
5132
+ spec: true
4945
5133
  };
4946
5134
  var PRIORITY_MEMBERS = {
4947
5135
  urgent: true,
@@ -4969,7 +5157,8 @@ var GITHUB_LABELS = Object.freeze({
4969
5157
  typePrefix: "type:",
4970
5158
  priorityPrefix: "priority:",
4971
5159
  needsTriage: "needs-triage",
4972
- blockedByPrefix: "blocked-by:OPT-"
5160
+ blockedByPrefix: "blocked-by:OPT-",
5161
+ specBlocked: "spec:blocked"
4973
5162
  });
4974
5163
  var VIEW_FIELDS = "number,title,body,state,stateReason,labels,url";
4975
5164
  var LIST_COMMAND = "gh issue list";
@@ -5078,7 +5267,8 @@ var DRAFT_CHECKS = [
5078
5267
  "blockedBy",
5079
5268
  (value) => Array.isArray(value) && value.every((item) => Number.isSafeInteger(item) && item > 0),
5080
5269
  "a list of positive whole numbers"
5081
- ]
5270
+ ],
5271
+ ["specBlocked", (value) => value === undefined || typeof value === "boolean", "absent or a boolean"]
5082
5272
  ];
5083
5273
  function draftProblem(draft) {
5084
5274
  if (!isMapping(draft))
@@ -5092,6 +5282,7 @@ function labelsFor(draft) {
5092
5282
  `${GITHUB_LABELS.modulePrefix}${draft.module}`,
5093
5283
  `${GITHUB_LABELS.typePrefix}${draft.type}`,
5094
5284
  ...draft.blockedBy.map((opt) => `${GITHUB_LABELS.blockedByPrefix}${opt}`),
5285
+ ...draft.specBlocked === true ? [GITHUB_LABELS.specBlocked] : [],
5095
5286
  priority
5096
5287
  ];
5097
5288
  }
@@ -5387,6 +5578,12 @@ function oneOf2(members) {
5387
5578
  function orNull(accepts) {
5388
5579
  return (value) => value === null || accepts(value);
5389
5580
  }
5581
+ function orAbsent(accepts) {
5582
+ return (value) => value === undefined || accepts(value);
5583
+ }
5584
+ function isBoolean(value) {
5585
+ return typeof value === "boolean";
5586
+ }
5390
5587
  function listOf(accepts) {
5391
5588
  return (value) => Array.isArray(value) && value.every(accepts);
5392
5589
  }
@@ -5398,6 +5595,7 @@ var FIELD_CHECKS = {
5398
5595
  project: [orNull(isString2), "a string or null"],
5399
5596
  title: [isString2, "a string"],
5400
5597
  blockedBy: [listOf(isFiniteNumber), "a list of numbers"],
5598
+ specBlocked: [orAbsent(isBoolean), "absent or a boolean"],
5401
5599
  state: [oneOf2(ISSUE_STATES), `one of: ${ISSUE_STATES.join(", ")}`],
5402
5600
  fallbackReason: [orNull(isString2), "a string or null"],
5403
5601
  capturedAt: [isString2, "a string"],
@@ -5453,6 +5651,7 @@ function renderLocalIssue(record) {
5453
5651
  project: draft.project,
5454
5652
  title: draft.title,
5455
5653
  blockedBy: draft.blockedBy,
5654
+ ...draft.specBlocked === undefined ? {} : { specBlocked: draft.specBlocked },
5456
5655
  state: record.state,
5457
5656
  fallbackReason: record.fallbackReason,
5458
5657
  capturedAt: record.capturedAt,
@@ -5494,7 +5693,8 @@ function parseLocalIssue(contents, sourcePath) {
5494
5693
  module: fields.module,
5495
5694
  priority: fields.priority,
5496
5695
  project: fields.project,
5497
- blockedBy: fields.blockedBy
5696
+ blockedBy: fields.blockedBy,
5697
+ ...fields.specBlocked === undefined ? {} : { specBlocked: fields.specBlocked }
5498
5698
  },
5499
5699
  fallbackReason: fields.fallbackReason,
5500
5700
  capturedAt: fields.capturedAt,
@@ -5911,10 +6111,10 @@ function requireTrustedAuthor(item, reading) {
5911
6111
  throw new CommandExit(TRUST_REFUSAL_EXIT, trustRefusalMessage(item, reading));
5912
6112
  }
5913
6113
  // src/effort/sync/wire.ts
5914
- import { existsSync as existsSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync7 } from "fs";
6114
+ import { existsSync as existsSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
5915
6115
  import { tmpdir as tmpdir4 } from "os";
5916
6116
  import { join as join17 } from "path";
5917
- import { Database as Database6, SQLiteError as SQLiteError3 } from "bun:sqlite";
6117
+ import { Database as Database7, SQLiteError as SQLiteError3 } from "bun:sqlite";
5918
6118
  var WIRE_FORMAT = "rafa-effort-wire";
5919
6119
  var WIRE_VERSION = 1;
5920
6120
 
@@ -6076,7 +6276,7 @@ function exportWirePayload(options) {
6076
6276
  guardTestProcess(path);
6077
6277
  if (!existsSync14(path))
6078
6278
  throw new WireExportRefusal(`wire: ${path} is not there`);
6079
- const db = new Database6(path, { readonly: true, safeIntegers: true });
6279
+ const db = new Database7(path, { readonly: true, safeIntegers: true });
6080
6280
  try {
6081
6281
  db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
6082
6282
  return db.transaction(() => readPayload(db, path, cursor))();
@@ -6115,7 +6315,7 @@ function insertRows(db, table, rows) {
6115
6315
  }
6116
6316
  function buildStore(path, payload, options) {
6117
6317
  const migrations = namedMigrations(payload.migrations, options.migrations ?? SQLITE_MIGRATIONS);
6118
- const db = new Database6(path, { create: true, readwrite: true });
6318
+ const db = new Database7(path, { create: true, readwrite: true });
6119
6319
  try {
6120
6320
  bringForward(db, path, "write", "open", {
6121
6321
  migrations,
@@ -6134,7 +6334,7 @@ function buildStore(path, payload, options) {
6134
6334
  function materialiseWirePayload(payload, options = {}) {
6135
6335
  const checked = checkPayload(payload);
6136
6336
  const directory = mkdtempSync2(join17(options.tempDir ?? tmpdir4(), "rafa-wire-"));
6137
- const release = () => rmSync7(directory, { recursive: true, force: true });
6337
+ const release = () => rmSync8(directory, { recursive: true, force: true });
6138
6338
  const path = join17(directory, SQLITE_STORE_FILE_NAME);
6139
6339
  try {
6140
6340
  guardTestProcess(path);
@@ -6163,4 +6363,4 @@ function selectEffortStore(repoRoot, config) {
6163
6363
  return adapter.create({ repoRoot });
6164
6364
  }
6165
6365
 
6166
- 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, isSessionId, sessionPhase, SessionConflictError, SessionStateError, 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, readStoreMeta, 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, padKind, oneLine, 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, TRUSTED_PERMISSIONS, createGhPermissions, readAuthorTrust, ghBoardTrust, readBoardTrust, requireTrustedBoardAuthor, trustRefusalClause, requireTrustedAuthor, WIRE_FORMAT, WIRE_VERSION, WireFormatError, WireExportRefusal, encodeWirePayload, decodeWirePayload, exportWirePayload, materialiseWirePayload, selectEffortStore };
6366
+ 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, isSessionId, sessionPhase, SessionConflictError, SessionStateError, 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, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, EffortCopyRefusal, EffortCopyFailure, copyEffortStore, RebuildRefusal, readConfigFile, loadConfig, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, stubOfPlanFile, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, fileStamp, keepsIdLine, fix_schema_default, 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, EVENT_PREFIX, padKind, oneLine, sessionSpawnEnv, 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, TRUSTED_PERMISSIONS, createGhPermissions, readAuthorTrust, ghBoardTrust, readBoardTrust, requireTrustedBoardAuthor, trustRefusalClause, requireTrustedAuthor, WIRE_FORMAT, WIRE_VERSION, WireFormatError, WireExportRefusal, encodeWirePayload, decodeWirePayload, exportWirePayload, materialiseWirePayload, selectEffortStore };