@open-tomato/rafa 0.26.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
@@ -238,13 +243,13 @@ function openNdjsonStore(repoRoot) {
238
243
  }
239
244
 
240
245
  // src/effort/store/sqlite.ts
241
- import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
242
- import { dirname as dirname6, join as join7 } from "path";
246
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
247
+ import { dirname as dirname9, join as join9 } from "path";
243
248
  import { Database } from "bun:sqlite";
244
249
 
245
250
  // package.json
246
251
  var name = "@open-tomato/rafa";
247
- var version = "0.26.0";
252
+ var version = "0.31.0";
248
253
 
249
254
  // src/cli/version.ts
250
255
  var RAFA_VERSION = version;
@@ -254,23 +259,99 @@ 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 dirname5, 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 {
261
266
  linkSync,
262
- mkdirSync as mkdirSync2,
267
+ mkdirSync as mkdirSync4,
263
268
  readdirSync,
264
- readFileSync as readFileSync2,
265
- renameSync,
266
- rmSync,
267
- writeFileSync
269
+ readFileSync as readFileSync4,
270
+ renameSync as renameSync3,
271
+ rmSync as rmSync3,
272
+ writeFileSync as writeFileSync3
268
273
  } from "fs";
269
- import { basename, dirname as dirname3, join as join5 } from "path";
274
+ import { basename, dirname as dirname5, isAbsolute as isAbsolute4, join as join7 } from "path";
275
+
276
+ // src/next/hop-record.ts
277
+ import { randomUUID as randomUUID2 } from "crypto";
278
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
279
+ import { dirname as dirname4, join as join6 } from "path";
280
+
281
+ // src/project/position.ts
282
+ import { randomUUID } from "crypto";
283
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
284
+ import { dirname as dirname3, join as join5 } from "path";
270
285
 
271
286
  // src/project/scope.ts
272
287
  import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
273
- 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
274
355
  var SCOPE_DIR = dirname2(CONFIG_FILE);
275
356
  var INIT_COMMAND = "rafa init";
276
357
  var DISK_FILE_SYSTEM = Object.freeze({
@@ -287,17 +368,19 @@ class ScopeError extends Error {
287
368
  function scopeAt(base) {
288
369
  return { dir: join4(base, SCOPE_DIR), configFile: configFilePath(base) };
289
370
  }
290
- function initHint(start, passedUserConfig = null) {
371
+ function initHint(start, passedUserConfig = null, probedMainCheckout = null) {
291
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`];
292
374
  return [
293
375
  `not inside a rafa project: no ${CONFIG_FILE} in ${start} or any directory above it`,
376
+ ...main,
294
377
  ...passed,
295
378
  `run \`${INIT_COMMAND}\` to set one up`
296
379
  ].join(`
297
380
  `);
298
381
  }
299
382
  function requireAbsolute(label, path) {
300
- if (!isAbsolute2(path)) {
383
+ if (!isAbsolute3(path)) {
301
384
  throw new ScopeError(`${label} is ${describeValue(path)}, expected an absolute path`);
302
385
  }
303
386
  }
@@ -331,17 +414,215 @@ function walkUp(realStart, realHome, fs) {
331
414
  }
332
415
  return { root: null, passedUserConfig };
333
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
+ }
334
433
  function resolveScope(start, seams) {
335
- const { home, fs = DISK_FILE_SYSTEM } = seams;
434
+ const { home, fs = DISK_FILE_SYSTEM, mainCheckout = mainCheckoutOf } = seams;
336
435
  requireAbsolute("start directory", start);
337
436
  requireAbsolute("home directory", home);
338
437
  const user = scopeAt(home);
339
- const walk = walkUp(realStartOf(start, fs), realHomeOf(home, fs), fs);
340
- 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) {
341
443
  const passed = walk.passedUserConfig ? user.configFile : null;
342
- return { found: false, start, home, user, hint: initHint(start, passed) };
444
+ return { found: false, start, home, user, hint: initHint(start, passed, fallback.probed) };
445
+ }
446
+ return { found: true, root: fallback.root, home, project: scopeAt(fallback.root), user };
447
+ }
448
+
449
+ // src/project/position.ts
450
+ var POSITION_FILE = "position.json";
451
+ function positionFilePath(root) {
452
+ return join5(scopeAt(root).dir, POSITION_FILE);
453
+ }
454
+ function isRecord(value) {
455
+ return typeof value === "object" && value !== null && !Array.isArray(value);
456
+ }
457
+ function isIssueNumber(value) {
458
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
459
+ }
460
+ function asPlace(value) {
461
+ if (!isRecord(value) || !isIssueNumber(value.board))
462
+ return null;
463
+ if (value.epic !== null && !isIssueNumber(value.epic))
464
+ return null;
465
+ return { board: value.board, epic: value.epic };
466
+ }
467
+ function asPosition(value) {
468
+ if (!isRecord(value))
469
+ return null;
470
+ const current = asPlace(value.current);
471
+ const home = asPlace(value.home);
472
+ const previous = value.previous === null ? null : asPlace(value.previous);
473
+ if (current === null || home === null)
474
+ return null;
475
+ if (previous === null && value.previous !== null)
476
+ return null;
477
+ return { current, previous, home };
478
+ }
479
+ function readPositionFile(root) {
480
+ const file = positionFilePath(root);
481
+ let text;
482
+ try {
483
+ text = readFileSync2(file, "utf8");
484
+ } catch (error) {
485
+ const code = error.code;
486
+ return code === "ENOENT" ? { set: false, reason: "absent", detail: `No position file at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
487
+ }
488
+ let parsed;
489
+ try {
490
+ parsed = JSON.parse(text);
491
+ } catch (error) {
492
+ return { set: false, reason: "invalid-json", detail: `${file} holds no JSON: ${messageOf(error)}` };
493
+ }
494
+ const position = asPosition(parsed);
495
+ return position === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a current, previous and home place` } : { set: true, position };
496
+ }
497
+ function writePositionFile(root, position) {
498
+ const file = positionFilePath(root);
499
+ mkdirSync2(dirname3(file), { recursive: true });
500
+ const temporary = `${file}.${String(process.pid)}.${randomUUID()}.tmp`;
501
+ const { current, previous, home } = position;
502
+ try {
503
+ writeFileSync(temporary, `${JSON.stringify({ current, previous, home }, null, 2)}
504
+ `);
505
+ renameSync(temporary, file);
506
+ } catch (error) {
507
+ rmSync(temporary, { force: true });
508
+ throw error;
509
+ }
510
+ }
511
+ function positionAt(place) {
512
+ return { current: place, previous: null, home: place };
513
+ }
514
+ function rehome(position, place) {
515
+ return { current: place, previous: position.current, home: place };
516
+ }
517
+ function hop(position, place) {
518
+ return { current: place, previous: position.current, home: position.home };
519
+ }
520
+ function goHome(position) {
521
+ return { current: position.home, previous: position.current, home: position.home };
522
+ }
523
+
524
+ // src/next/hop-record.ts
525
+ var HOP_FILE = "hop.json";
526
+ var HOP_KINDS = ["blocker", "dry"];
527
+ var HOP_STATES = ["away", "waiting", "merged", "halted"];
528
+ function hopFilePath(root) {
529
+ return join6(scopeAt(root).dir, HOP_FILE);
530
+ }
531
+ function isRecord2(value) {
532
+ return typeof value === "object" && value !== null && !Array.isArray(value);
533
+ }
534
+ function isIssueNumber2(value) {
535
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
536
+ }
537
+ function isIssueOrNull(value) {
538
+ return value === null || isIssueNumber2(value);
539
+ }
540
+ function isTimestamp(value) {
541
+ return typeof value === "string" && !Number.isNaN(Date.parse(value));
542
+ }
543
+ function issuesMatchKind(kind, blocked, target) {
544
+ return kind === "blocker" ? blocked !== null && target !== null : blocked === null && target === null;
545
+ }
546
+ function asHopRecord(value) {
547
+ if (!isRecord2(value))
548
+ return null;
549
+ const { kind, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = value;
550
+ const home = asPlace(value.home);
551
+ const from = asPlace(value.from);
552
+ if (typeof kind !== "string" || !HOP_KINDS.includes(kind))
553
+ return null;
554
+ if (typeof state !== "string" || !HOP_STATES.includes(state))
555
+ return null;
556
+ if (home === null || from === null)
557
+ return null;
558
+ if (!isIssueOrNull(blocked) || !isIssueOrNull(target) || !isIssueOrNull(pullRequest))
559
+ return null;
560
+ if (!isIssueNumber2(targetEpic) || !isIssueNumber2(targetBoard) || !isTimestamp(startedAt))
561
+ return null;
562
+ const hopKind = kind;
563
+ if (!issuesMatchKind(hopKind, blocked, target))
564
+ return null;
565
+ return {
566
+ kind: hopKind,
567
+ home,
568
+ from,
569
+ blocked,
570
+ target,
571
+ targetEpic,
572
+ targetBoard,
573
+ state,
574
+ pullRequest,
575
+ startedAt
576
+ };
577
+ }
578
+ function readHopRecord(root) {
579
+ const file = hopFilePath(root);
580
+ let text;
581
+ try {
582
+ text = readFileSync3(file, "utf8");
583
+ } catch (error) {
584
+ const code = error.code;
585
+ return code === "ENOENT" ? { set: false, reason: "absent", detail: `No hop record at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
586
+ }
587
+ let parsed;
588
+ try {
589
+ parsed = JSON.parse(text);
590
+ } catch (error) {
591
+ return { set: false, reason: "wrong-shape", detail: `${file} holds no JSON: ${messageOf(error)}` };
343
592
  }
344
- return { found: true, root: walk.root, home, project: scopeAt(walk.root), user };
593
+ const record = asHopRecord(parsed);
594
+ return record === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a hop record` } : { set: true, record };
595
+ }
596
+ function ownKeys(record) {
597
+ const { kind, home, from, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = record;
598
+ return {
599
+ kind,
600
+ home: { board: home.board, epic: home.epic },
601
+ from: { board: from.board, epic: from.epic },
602
+ blocked,
603
+ target,
604
+ targetEpic,
605
+ targetBoard,
606
+ state,
607
+ pullRequest,
608
+ startedAt
609
+ };
610
+ }
611
+ function writeHopRecord(root, record) {
612
+ const file = hopFilePath(root);
613
+ mkdirSync3(dirname4(file), { recursive: true });
614
+ const temporary = `${file}.${String(process.pid)}.${randomUUID2()}.tmp`;
615
+ try {
616
+ writeFileSync2(temporary, `${JSON.stringify(ownKeys(record), null, 2)}
617
+ `);
618
+ renameSync2(temporary, file);
619
+ } catch (error) {
620
+ rmSync2(temporary, { force: true });
621
+ throw error;
622
+ }
623
+ }
624
+ function staleAgainst(record, position) {
625
+ return position.home.board !== record.home.board || position.home.epic !== record.home.epic;
345
626
  }
346
627
 
347
628
  // src/loop/sessions.ts
@@ -384,13 +665,13 @@ function isSessionId(value) {
384
665
  return SESSION_ID_PATTERN.test(value);
385
666
  }
386
667
  function runsDir(root) {
387
- return join5(scopeAt(root).dir, RUNS_DIR);
668
+ return join7(scopeAt(root).dir, RUNS_DIR);
388
669
  }
389
670
  function sessionFilePath(root, sessionId) {
390
671
  if (!isSessionId(sessionId)) {
391
672
  throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
392
673
  }
393
- return join5(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
674
+ return join7(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
394
675
  }
395
676
  function describeValue2(value) {
396
677
  return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
@@ -417,6 +698,18 @@ function taskProblem(task) {
417
698
  }
418
699
  return isText(field(task, "text")) ? null : `task.text is ${describeValue2(field(task, "text"))}, expected a non-empty string`;
419
700
  }
701
+ function hopProblem(fields) {
702
+ if (!Object.hasOwn(fields, "hop"))
703
+ return null;
704
+ const hop2 = field(fields, "hop");
705
+ return asHopRecord(hop2) === null ? `hop is ${describeValue2(hop2)}, expected a hop record` : null;
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
+ }
420
713
  function sessionIdProblem(sessionId, file) {
421
714
  if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
422
715
  return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
@@ -440,12 +733,20 @@ function recordProblems(fields, file) {
440
733
  isPositiveWhole(pid) ? null : `pid is ${describeValue2(pid)}, expected a whole number from 1`,
441
734
  typeof startedAt === "string" && !Number.isNaN(Date.parse(startedAt)) ? null : `startedAt is ${describeValue2(startedAt)}, expected a timestamp`,
442
735
  SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
443
- taskProblem(field(fields, "task"))
736
+ taskProblem(field(fields, "task")),
737
+ hopProblem(fields),
738
+ worktreeProblem(fields)
444
739
  ];
445
740
  return problems.filter((problem) => problem !== null);
446
741
  }
742
+ function freezeHop(value) {
743
+ const hop2 = asHopRecord(value);
744
+ return Object.freeze({ ...hop2, home: Object.freeze(hop2.home), from: Object.freeze(hop2.from) });
745
+ }
447
746
  function freezeRecord(fields) {
448
747
  const task = field(fields, "task");
748
+ const hop2 = field(fields, "hop");
749
+ const worktree = field(fields, "worktree");
449
750
  return Object.freeze({
450
751
  sessionId: field(fields, "sessionId"),
451
752
  planStub: field(fields, "planStub"),
@@ -454,7 +755,9 @@ function freezeRecord(fields) {
454
755
  pid: field(fields, "pid"),
455
756
  startedAt: field(fields, "startedAt"),
456
757
  state: field(fields, "state"),
457
- task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null
758
+ task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
759
+ ...hop2 === undefined ? {} : { hop: freezeHop(hop2) },
760
+ ...worktree === undefined ? {} : { worktree }
458
761
  });
459
762
  }
460
763
  function parseSessionRecord(text, file) {
@@ -492,7 +795,7 @@ function readState(record, isAlive = isPidAlive) {
492
795
  function readRecordFile(file) {
493
796
  let text;
494
797
  try {
495
- text = readFileSync2(file, "utf8");
798
+ text = readFileSync4(file, "utf8");
496
799
  } catch (error) {
497
800
  throw new SessionRecordError(file, `cannot be read: ${messageOf(error)}`);
498
801
  }
@@ -513,7 +816,7 @@ function byStart(a, b) {
513
816
  function readSessions(root, seams = {}) {
514
817
  const dir = runsDir(root);
515
818
  const isAlive = seams.isAlive ?? isPidAlive;
516
- const records = recordNames(dir).filter((name2) => name2.endsWith(RECORD_EXTENSION)).map((name2) => readRecordFile(join5(dir, name2))).map((record) => Object.freeze({ ...record, state: readState(record, isAlive) }));
819
+ const records = recordNames(dir).filter((name2) => name2.endsWith(RECORD_EXTENSION)).map((name2) => readRecordFile(join7(dir, name2))).map((record) => Object.freeze({ ...record, state: readState(record, isAlive) }));
517
820
  return Object.freeze(records.sort(byStart));
518
821
  }
519
822
  function readSession(root, sessionId, seams = {}) {
@@ -539,17 +842,17 @@ function checkRecord(record, file) {
539
842
  }
540
843
  function writeRecordFile(file, record, isNew) {
541
844
  checkRecord(record, file);
542
- mkdirSync2(dirname3(file), { recursive: true });
845
+ mkdirSync4(dirname5(file), { recursive: true });
543
846
  const temporary = `${file}.${process.pid}.tmp`;
544
- writeFileSync(temporary, `${JSON.stringify(record, null, 2)}
847
+ writeFileSync3(temporary, `${JSON.stringify(record, null, 2)}
545
848
  `);
546
849
  try {
547
850
  if (isNew)
548
851
  linkSync(temporary, file);
549
852
  else
550
- renameSync(temporary, file);
853
+ renameSync3(temporary, file);
551
854
  } finally {
552
- rmSync(temporary, { force: true });
855
+ rmSync3(temporary, { force: true });
553
856
  }
554
857
  }
555
858
  function beginSession(root, draft, seams = {}) {
@@ -577,10 +880,10 @@ function updateSession(root, sessionId, change) {
577
880
  }
578
881
 
579
882
  // src/runtime/identity.ts
580
- import { existsSync as existsSync4, readFileSync as readFileSync3, realpathSync as realpathSync3 } from "fs";
581
- import { dirname as dirname4, join as join6, resolve as resolve2 } from "path";
883
+ import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3 } from "fs";
884
+ import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
582
885
  var MANIFEST = "package.json";
583
- var CHECKOUT_ENTRY = join6("src", "rafa.ts");
886
+ var CHECKOUT_ENTRY = join8("src", "rafa.ts");
584
887
  function realOrResolved(path) {
585
888
  const resolved = resolve2(path);
586
889
  return existsSync4(resolved) ? realpathSync3(resolved) : resolved;
@@ -588,7 +891,7 @@ function realOrResolved(path) {
588
891
  function manifestName(file) {
589
892
  let parsed;
590
893
  try {
591
- parsed = JSON.parse(readFileSync3(file, "utf8"));
894
+ parsed = JSON.parse(readFileSync5(file, "utf8"));
592
895
  } catch {
593
896
  return null;
594
897
  }
@@ -600,10 +903,10 @@ function manifestName(file) {
600
903
  function nearestRafaPackage(start) {
601
904
  let dir = start;
602
905
  for (;; ) {
603
- const manifest = join6(dir, MANIFEST);
906
+ const manifest = join8(dir, MANIFEST);
604
907
  if (existsSync4(manifest) && manifestName(manifest) === name)
605
908
  return dir;
606
- const parent = dirname4(dir);
909
+ const parent = dirname6(dir);
607
910
  if (parent === dir)
608
911
  return null;
609
912
  dir = parent;
@@ -611,8 +914,8 @@ function nearestRafaPackage(start) {
611
914
  }
612
915
  function readRuntimeIdentity(entry = Bun.main) {
613
916
  const real = realOrResolved(entry);
614
- const packageDir = nearestRafaPackage(dirname4(real));
615
- if (packageDir !== null && existsSync4(join6(packageDir, CHECKOUT_ENTRY))) {
917
+ const packageDir = nearestRafaPackage(dirname6(real));
918
+ if (packageDir !== null && existsSync4(join8(packageDir, CHECKOUT_ENTRY))) {
616
919
  return { kind: "development", entry: real, checkout: packageDir };
617
920
  }
618
921
  return { kind: "installed", entry: real };
@@ -638,17 +941,17 @@ class DevelopmentBuildRefusedError extends Error {
638
941
  }
639
942
  }
640
943
  function storeProjectRoot(path) {
641
- const storeDir = dirname5(path);
642
- const scopeDir = dirname5(storeDir);
944
+ const storeDir = dirname7(path);
945
+ const scopeDir = dirname7(storeDir);
643
946
  if (basename2(storeDir) !== EFFORT_DIR_NAME || basename2(scopeDir) !== SCOPE_DIR)
644
947
  return null;
645
- return dirname5(scopeDir);
948
+ return dirname7(scopeDir);
646
949
  }
647
950
  function isOwnedStore(path, env, tempDir) {
648
951
  if (isUnderTempDir(path, tempDir))
649
952
  return true;
650
953
  const override = env[EFFORT_DIR_VARIABLE];
651
- if (override === undefined || override === "" || !isAbsolute3(override))
954
+ if (override === undefined || override === "" || !isAbsolute5(override))
652
955
  return false;
653
956
  return storeProjectRoot(path) === null && isUnderTempDir(path, override);
654
957
  }
@@ -926,6 +1229,120 @@ var SQLITE_MIGRATIONS = [
926
1229
  CHECK ((verdict = 'red') = (json_array_length(failing) > 0))
927
1230
  );
928
1231
  `
1232
+ },
1233
+ {
1234
+ id: "row-origins",
1235
+ breaks: [],
1236
+ sql: `
1237
+ ALTER TABLE sessions ADD COLUMN origin_store TEXT;
1238
+ ALTER TABLE sessions ADD COLUMN origin_seq INTEGER;
1239
+ CREATE UNIQUE INDEX sessions_by_origin
1240
+ ON sessions (origin_store, origin_seq)
1241
+ WHERE origin_store IS NOT NULL;
1242
+
1243
+ ALTER TABLE commits ADD COLUMN origin_store TEXT;
1244
+ ALTER TABLE commits ADD COLUMN origin_seq INTEGER;
1245
+ CREATE UNIQUE INDEX commits_by_origin
1246
+ ON commits (origin_store, origin_seq)
1247
+ WHERE origin_store IS NOT NULL;
1248
+
1249
+ ALTER TABLE findings ADD COLUMN origin_store TEXT;
1250
+ ALTER TABLE findings ADD COLUMN origin_seq INTEGER;
1251
+ CREATE UNIQUE INDEX findings_by_origin
1252
+ ON findings (origin_store, origin_seq)
1253
+ WHERE origin_store IS NOT NULL;
1254
+
1255
+ ALTER TABLE blockers ADD COLUMN origin_store TEXT;
1256
+ ALTER TABLE blockers ADD COLUMN origin_seq INTEGER;
1257
+ CREATE UNIQUE INDEX blockers_by_origin
1258
+ ON blockers (origin_store, origin_seq)
1259
+ WHERE origin_store IS NOT NULL;
1260
+
1261
+ ALTER TABLE out_of_scope_bugs ADD COLUMN origin_store TEXT;
1262
+ ALTER TABLE out_of_scope_bugs ADD COLUMN origin_seq INTEGER;
1263
+ CREATE UNIQUE INDEX out_of_scope_bugs_by_origin
1264
+ ON out_of_scope_bugs (origin_store, origin_seq)
1265
+ WHERE origin_store IS NOT NULL;
1266
+
1267
+ ALTER TABLE report_absences ADD COLUMN origin_store TEXT;
1268
+ ALTER TABLE report_absences ADD COLUMN origin_seq INTEGER;
1269
+ CREATE UNIQUE INDEX report_absences_by_origin
1270
+ ON report_absences (origin_store, origin_seq)
1271
+ WHERE origin_store IS NOT NULL;
1272
+
1273
+ ALTER TABLE task_reports ADD COLUMN origin_store TEXT;
1274
+ ALTER TABLE task_reports ADD COLUMN origin_seq INTEGER;
1275
+ CREATE UNIQUE INDEX task_reports_by_origin
1276
+ ON task_reports (origin_store, origin_seq)
1277
+ WHERE origin_store IS NOT NULL;
1278
+
1279
+ ALTER TABLE preflight ADD COLUMN origin_store TEXT;
1280
+ ALTER TABLE preflight ADD COLUMN origin_seq INTEGER;
1281
+ CREATE UNIQUE INDEX preflight_by_origin
1282
+ ON preflight (origin_store, origin_seq)
1283
+ WHERE origin_store IS NOT NULL;
1284
+
1285
+ ALTER TABLE dispatches ADD COLUMN origin_store TEXT;
1286
+ ALTER TABLE dispatches ADD COLUMN origin_seq INTEGER;
1287
+ CREATE UNIQUE INDEX dispatches_by_origin
1288
+ ON dispatches (origin_store, origin_seq)
1289
+ WHERE origin_store IS NOT NULL;
1290
+
1291
+ ALTER TABLE changes ADD COLUMN origin_store TEXT;
1292
+ ALTER TABLE changes ADD COLUMN origin_seq INTEGER;
1293
+ CREATE UNIQUE INDEX changes_by_origin
1294
+ ON changes (origin_store, origin_seq)
1295
+ WHERE origin_store IS NOT NULL;
1296
+
1297
+ ALTER TABLE skill_invocations ADD COLUMN origin_store TEXT;
1298
+ ALTER TABLE skill_invocations ADD COLUMN origin_seq INTEGER;
1299
+ CREATE UNIQUE INDEX skill_invocations_by_origin
1300
+ ON skill_invocations (origin_store, origin_seq)
1301
+ WHERE origin_store IS NOT NULL;
1302
+
1303
+ ALTER TABLE plan_ci ADD COLUMN origin_store TEXT;
1304
+ ALTER TABLE plan_ci ADD COLUMN origin_seq INTEGER;
1305
+ CREATE UNIQUE INDEX plan_ci_by_origin
1306
+ ON plan_ci (origin_store, origin_seq)
1307
+ WHERE origin_store IS NOT NULL;
1308
+ `
1309
+ },
1310
+ {
1311
+ id: "store-meta",
1312
+ breaks: [],
1313
+ sql: `
1314
+ CREATE TABLE store_meta (
1315
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1316
+ store_id TEXT NOT NULL CHECK (store_id <> ''),
1317
+ project_root_commit TEXT NOT NULL CHECK (project_root_commit <> ''),
1318
+ project_remote TEXT CHECK (project_remote <> ''),
1319
+ host_id TEXT NOT NULL CHECK (host_id <> ''),
1320
+ store_path TEXT NOT NULL CHECK (store_path <> ''),
1321
+ file_dev INTEGER NOT NULL CHECK (typeof(file_dev) = 'integer'),
1322
+ file_ino INTEGER NOT NULL CHECK (typeof(file_ino) = 'integer'),
1323
+ minted_at TEXT NOT NULL CHECK (minted_at <> '')
1324
+ );
1325
+
1326
+ CREATE TABLE merges (
1327
+ seq INTEGER PRIMARY KEY,
1328
+ id TEXT NOT NULL UNIQUE CHECK (id <> ''),
1329
+ other_store TEXT CHECK (other_store <> ''),
1330
+ merged_at TEXT NOT NULL CHECK (merged_at <> ''),
1331
+ rows_added INTEGER NOT NULL CHECK (typeof(rows_added) = 'integer' AND rows_added >= 0),
1332
+ rows_skipped INTEGER NOT NULL CHECK (typeof(rows_skipped) = 'integer' AND rows_skipped >= 0),
1333
+ rows_in_conflict INTEGER NOT NULL CHECK (typeof(rows_in_conflict) = 'integer' AND rows_in_conflict >= 0)
1334
+ );
1335
+
1336
+ CREATE TABLE merge_conflicts (
1337
+ seq INTEGER PRIMARY KEY,
1338
+ merge_id TEXT NOT NULL CHECK (merge_id <> ''),
1339
+ table_name TEXT NOT NULL CHECK (table_name <> ''),
1340
+ local_seq INTEGER NOT NULL CHECK (typeof(local_seq) = 'integer'),
1341
+ field TEXT CHECK (field <> ''),
1342
+ incoming TEXT NOT NULL CHECK (json_valid(incoming) AND json_type(incoming) = 'object'),
1343
+ recorded_at TEXT NOT NULL CHECK (recorded_at <> '')
1344
+ );
1345
+ `
929
1346
  }
930
1347
  ];
931
1348
  var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
@@ -1080,188 +1497,1752 @@ function usablePlan(store, catalogue, access, caller) {
1080
1497
  throw new SchemaRefusedError(plan);
1081
1498
  return plan;
1082
1499
  }
1083
- function hasWork(plan) {
1084
- return plan.adopted.length > 0 || plan.pending.length > 0;
1500
+ function hasWork(plan) {
1501
+ return plan.adopted.length > 0 || plan.pending.length > 0;
1502
+ }
1503
+ function writeNames(plan) {
1504
+ const adoption = plan.adopted.length > 0 ? [ADOPTION_NAME] : [];
1505
+ return [...adoption, ...plan.pending.map(({ id }) => id)];
1506
+ }
1507
+ function nothingWritten(db) {
1508
+ return { adopted: [], applied: [], userVersion: readUserVersion(db) };
1509
+ }
1510
+ function readUserVersion(db) {
1511
+ const row = db.query("PRAGMA user_version").get();
1512
+ return row?.user_version ?? 0;
1513
+ }
1514
+ function readMigrationLog(db, path) {
1515
+ const table = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE);
1516
+ if (table === null)
1517
+ return null;
1518
+ return db.query(`SELECT id, sha256, breaks, applied_at, applied_by FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map((row) => ({
1519
+ id: row.id,
1520
+ sha256: row.sha256,
1521
+ breaks: parseBreaks(row.breaks, row.id, path),
1522
+ appliedAt: row.applied_at,
1523
+ appliedBy: row.applied_by
1524
+ }));
1525
+ }
1526
+ function parseBreaks(text, id, path) {
1527
+ const refused = new Error(`effort store: ${path} logs migration ${id} with breaks ${text},` + " which is not a JSON array of words; refusing to read or write it");
1528
+ let parsed;
1529
+ try {
1530
+ parsed = JSON.parse(text);
1531
+ } catch (cause) {
1532
+ throw new Error(refused.message, { cause });
1533
+ }
1534
+ if (!Array.isArray(parsed) || !parsed.every((word) => typeof word === "string"))
1535
+ throw refused;
1536
+ return parsed;
1537
+ }
1538
+ function bodyOf(migrations, id) {
1539
+ const migration = migrations.find((candidate) => candidate.id === id);
1540
+ if (migration === undefined)
1541
+ throw new Error(`bring forward: no migration ${id} in the catalogue it planned from`);
1542
+ return migration.sql;
1543
+ }
1544
+ function logMigration(db, entry, stamp) {
1545
+ db.run(`INSERT INTO ${MIGRATION_LOG_TABLE} (id, sha256, breaks, applied_at, applied_by) VALUES (?, ?, ?, ?, ?)`, [entry.id, entry.sha256, JSON.stringify(entry.breaks), stamp.appliedAt, stamp.appliedBy]);
1546
+ }
1547
+
1548
+ // src/effort/store/origins.ts
1549
+ var ACROSS_STORES_ORDER = "collected_at, origin_store, origin_seq, seq";
1550
+ var STAMPED_COLUMNS = "seq, origin_store, origin_seq";
1551
+ var STORE_ID = "(SELECT store_id FROM store_meta WHERE id = 1)";
1552
+ function stampedValues(table) {
1553
+ const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${table})`;
1554
+ return `${nextSeq}, ${STORE_ID}, CASE WHEN ${STORE_ID} IS NULL THEN NULL ELSE ${nextSeq} END`;
1555
+ }
1556
+
1557
+ // src/effort/store/settings.ts
1558
+ var DEFAULT_STORE_SETTINGS = Object.freeze({
1559
+ busyTimeoutMs: CONFIG_DEFAULTS.effortBusyTimeoutMs
1560
+ });
1561
+ var active = DEFAULT_STORE_SETTINGS;
1562
+ function setActiveStoreSettings(settings) {
1563
+ if (settings === null) {
1564
+ active = DEFAULT_STORE_SETTINGS;
1565
+ return;
1566
+ }
1567
+ if (!isBusyTimeoutMs(settings.busyTimeoutMs)) {
1568
+ throw new RangeError(`effort store: busy timeout ${String(settings.busyTimeoutMs)} is not a whole number ` + `of milliseconds from ${String(BUSY_TIMEOUT_MIN_MS)} to ${String(BUSY_TIMEOUT_MAX_MS)}`);
1569
+ }
1570
+ active = Object.freeze({ busyTimeoutMs: settings.busyTimeoutMs });
1571
+ }
1572
+ function activeStoreSettings() {
1573
+ return active;
1574
+ }
1575
+
1576
+ // src/effort/store/store-meta.ts
1577
+ import { randomUUID as randomUUID3 } from "crypto";
1578
+ import { dirname as dirname8 } from "path";
1579
+
1580
+ // src/effort/store/store-identity.ts
1581
+ import { spawnSync as spawnSync3 } from "child_process";
1582
+ import { createHmac } from "crypto";
1583
+ import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync } from "fs";
1584
+ import { hostname, platform as currentPlatform } from "os";
1585
+
1586
+ // src/schema/project-id.ts
1587
+ import { spawnSync as spawnSync2 } from "child_process";
1588
+ var PROJECT_ID_LENGTH = 12;
1589
+ var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
1590
+ function hasColonAfterAt(url) {
1591
+ const at = url.indexOf("@");
1592
+ return at !== -1 && url.lastIndexOf(":") > at;
1593
+ }
1594
+ function normalizeRemote(url) {
1595
+ const stripped = url.replace(/:\/\/[^@]+@/, "://");
1596
+ const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
1597
+ const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
1598
+ return network ? trimmed.toLowerCase() : trimmed;
1599
+ }
1600
+ function gitRemoteUrl(dir) {
1601
+ const result = spawnSync2("git", ["remote", "get-url", "origin"], {
1602
+ cwd: dir,
1603
+ encoding: "utf8",
1604
+ env: { ...process.env, LC_ALL: "C" }
1605
+ });
1606
+ if (result.error !== undefined || result.status !== 0)
1607
+ return null;
1608
+ const url = result.stdout.trim();
1609
+ return url === "" ? null : url;
1610
+ }
1611
+
1612
+ // src/effort/store/store-identity.ts
1613
+ var HOST_ID_KEY = "rafa effort store host id";
1614
+ var MACHINE_ID_PATH = "/etc/machine-id";
1615
+ var IOREG_ARGS = ["-rd1", "-c", "IOPlatformExpertDevice"];
1616
+ function hostIdFrom(source, value) {
1617
+ return createHmac("sha256", HOST_ID_KEY).update(`${source}:${value}`).digest("hex");
1618
+ }
1619
+ function readMachineIdFile() {
1620
+ try {
1621
+ const value = readFileSync6(MACHINE_ID_PATH, "utf8").trim();
1622
+ return value === "" ? null : value;
1623
+ } catch {
1624
+ return null;
1625
+ }
1626
+ }
1627
+ function parseIoregPlatformUuid(output) {
1628
+ const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(output);
1629
+ return match?.[1] ?? null;
1630
+ }
1631
+ function readIoregPlatformUuid() {
1632
+ const result = spawnSync3("ioreg", [...IOREG_ARGS], { encoding: "utf8" });
1633
+ if (result.error !== undefined || result.status !== 0)
1634
+ return null;
1635
+ return parseIoregPlatformUuid(result.stdout);
1636
+ }
1637
+ function readHostId(seams = {}) {
1638
+ const platform = seams.platform ?? currentPlatform();
1639
+ if (platform === "linux") {
1640
+ const machineId = (seams.readMachineId ?? readMachineIdFile)();
1641
+ if (machineId !== null)
1642
+ return hostIdFrom("machine-id", machineId);
1643
+ }
1644
+ if (platform === "darwin") {
1645
+ const platformUuid = (seams.readPlatformUuid ?? readIoregPlatformUuid)();
1646
+ if (platformUuid !== null)
1647
+ return hostIdFrom("platform-uuid", platformUuid);
1648
+ }
1649
+ return hostIdFrom("hostname", (seams.readHostname ?? hostname)());
1650
+ }
1651
+ function readStoreFileFacts(path) {
1652
+ const storePath = realpathSync4(path);
1653
+ const stats = statSync(storePath, { bigint: true });
1654
+ return { storePath, fileDev: stats.dev, fileIno: stats.ino };
1655
+ }
1656
+ function observeStore(path, readHost = readHostId) {
1657
+ return { hostId: readHost(), ...readStoreFileFacts(path) };
1658
+ }
1659
+ function movedFacts(recorded, facts) {
1660
+ const sameFile = BigInt(recorded.fileDev) === facts.fileDev && BigInt(recorded.fileIno) === facts.fileIno;
1661
+ const moved = [
1662
+ ["host", recorded.hostId !== facts.hostId],
1663
+ ["path", recorded.storePath !== facts.storePath],
1664
+ ["file", !sameFile]
1665
+ ];
1666
+ return moved.filter(([, differs]) => differs).map(([reason]) => reason);
1667
+ }
1668
+ function decideStoreIdentity(access, recorded, observe) {
1669
+ if (access === "read")
1670
+ return { action: "none" };
1671
+ const facts = observe();
1672
+ if (recorded === null)
1673
+ return { action: "mint", reasons: ["unminted"], facts };
1674
+ const reasons = movedFacts(recorded, facts);
1675
+ return reasons.length === 0 ? { action: "keep", storeId: recorded.storeId } : { action: "mint", reasons, facts };
1676
+ }
1677
+ function smallestRootCommit(git) {
1678
+ const result = git(["rev-list", "--max-parents=0", "HEAD"]);
1679
+ if (!result.ok)
1680
+ return null;
1681
+ const roots = result.stdout.split(`
1682
+ `).map((line) => line.trim()).filter((line) => line !== "");
1683
+ return [...roots].sort()[0] ?? null;
1684
+ }
1685
+ function normalisedRemote(git) {
1686
+ const result = git(["remote", "get-url", "origin"]);
1687
+ if (!result.ok)
1688
+ return null;
1689
+ const remote = normalizeRemote(result.stdout.trim());
1690
+ return remote === "" ? null : remote;
1691
+ }
1692
+ function readProjectIdentity(dir, git = createGitRunner(dir)) {
1693
+ return { rootCommit: smallestRootCommit(git), remote: normalisedRemote(git) };
1694
+ }
1695
+
1696
+ // src/effort/store/store-meta.ts
1697
+ var STORED_INTEGER_BITS = 64;
1698
+ function toStoredInteger(value) {
1699
+ return BigInt.asIntN(STORED_INTEGER_BITS, value);
1700
+ }
1701
+ function fromStoredInteger(text) {
1702
+ return BigInt.asUintN(STORED_INTEGER_BITS, BigInt(text));
1703
+ }
1704
+ function readStoreMeta(db) {
1705
+ 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();
1706
+ if (row === null)
1707
+ return null;
1708
+ return {
1709
+ storeId: row.store_id,
1710
+ projectRootCommit: row.project_root_commit,
1711
+ projectRemote: row.project_remote,
1712
+ hostId: row.host_id,
1713
+ storePath: row.store_path,
1714
+ fileDev: fromStoredInteger(row.file_dev),
1715
+ fileIno: fromStoredInteger(row.file_ino),
1716
+ mintedAt: row.minted_at
1717
+ };
1718
+ }
1719
+ function projectFor(storePath, recorded, seams) {
1720
+ const read = (seams.readProject ?? readProjectIdentity)(dirname8(storePath));
1721
+ if (read.rootCommit !== null)
1722
+ return { rootCommit: read.rootCommit, remote: read.remote };
1723
+ if (recorded === null)
1724
+ return null;
1725
+ return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
1726
+ }
1727
+ function writeStoreMeta(db, facts, project, seams) {
1728
+ const storeId = (seams.newStoreId ?? randomUUID3)();
1729
+ const mintedAt = (seams.now?.() ?? new Date).toISOString();
1730
+ 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);
1731
+ return storeId;
1732
+ }
1733
+ function unminted(decision) {
1734
+ return decision.action === "keep" ? { action: "keep", storeId: decision.storeId } : { action: "none" };
1735
+ }
1736
+ function settleStoreIdentity(db, path, access, seams = {}) {
1737
+ if (access === "read")
1738
+ return { action: "none" };
1739
+ const observe = () => observeStore(path, seams.readHostId ?? readHostId);
1740
+ const recorded = readStoreMeta(db);
1741
+ const first = decideStoreIdentity(access, recorded, observe);
1742
+ if (first.action !== "mint")
1743
+ return unminted(first);
1744
+ const project = projectFor(first.facts.storePath, recorded, seams);
1745
+ if (project === null)
1746
+ return { action: "no-project", reasons: first.reasons };
1747
+ const mintUnderLock = db.transaction(() => {
1748
+ const second = decideStoreIdentity(access, readStoreMeta(db), observe);
1749
+ if (second.action !== "mint")
1750
+ return unminted(second);
1751
+ const storeId = writeStoreMeta(db, second.facts, project, seams);
1752
+ return { action: "mint", storeId, reasons: second.reasons };
1753
+ });
1754
+ return mintUnderLock.immediate();
1755
+ }
1756
+
1757
+ // src/effort/store/sqlite.ts
1758
+ var SQLITE_STORE_FILE_NAME = "effort.sqlite";
1759
+ function sqliteStorePath(repoRoot) {
1760
+ return join9(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
1761
+ }
1762
+ var KIND_TABLES = {
1763
+ sessions: { name: "sessions", keyColumn: "session_id" },
1764
+ commits: { name: "commits", keyColumn: "sha" }
1765
+ };
1766
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
1767
+ function hasKey(entry) {
1768
+ return entry.key !== null;
1769
+ }
1770
+ function refused(index, length, reason) {
1771
+ return new Error(`effort store: row ${index} of ${length} ${reason}; batch refused`);
1772
+ }
1773
+ function batchEntries(kind, rows) {
1774
+ const keyOf = EFFORT_KEY_PROJECTIONS[kind];
1775
+ const projected = rows.map((row) => ({ key: keyOf(row), row }));
1776
+ const keyed = projected.filter(hasKey);
1777
+ if (keyed.length < projected.length) {
1778
+ const keylessAt = projected.findIndex((entry) => !hasKey(entry));
1779
+ throw refused(keylessAt, rows.length, "carries no key");
1780
+ }
1781
+ const unholdableAt = keyed.findIndex(({ key }) => LONE_SURROGATE.test(key));
1782
+ if (unholdableAt !== -1) {
1783
+ const reason = "carries a key with a lone UTF-16 surrogate," + " which SQLite text cannot hold";
1784
+ throw refused(unholdableAt, rows.length, reason);
1785
+ }
1786
+ return keyed.map(({ key, row }) => ({ key, body: JSON.stringify(row) }));
1787
+ }
1788
+ function withSqliteStore(path, access, create, use, identity = {}) {
1789
+ guardTestProcess(path);
1790
+ if (create)
1791
+ mkdirSync5(dirname9(path), { recursive: true });
1792
+ const db = new Database(path, { readwrite: true, create });
1793
+ try {
1794
+ db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
1795
+ bringForward(db, path, access, "open");
1796
+ settleStoreIdentity(db, path, access, identity);
1797
+ return use(db);
1798
+ } finally {
1799
+ db.close();
1800
+ }
1801
+ }
1802
+ function insertBatch(db, table, entries) {
1803
+ const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json, ${STAMPED_COLUMNS})` + ` VALUES (?, ?, ${stampedValues(table.name)})` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
1804
+ const insertAll = db.transaction(() => entries.reduce((appended, { key, body }) => appended + insert.run(key, body).changes, 0));
1805
+ return insertAll.immediate();
1806
+ }
1807
+ function writeSqliteStore(path, rowCount, empty, insert) {
1808
+ const exists = existsSync5(path);
1809
+ if (rowCount > 0)
1810
+ return withSqliteStore(path, "write", !exists, insert);
1811
+ if (exists)
1812
+ withSqliteStore(path, "write", false, () => {
1813
+ return;
1814
+ });
1815
+ return empty;
1816
+ }
1817
+ function appendKind2(path, kind, rows) {
1818
+ const entries = batchEntries(kind, rows);
1819
+ const table = KIND_TABLES[kind];
1820
+ const appended = writeSqliteStore(path, entries.length, 0, (db) => insertBatch(db, table, entries));
1821
+ return { path, appended, skipped: entries.length - appended };
1822
+ }
1823
+ function keysOfKind2(path, kind) {
1824
+ if (!existsSync5(path))
1825
+ return new Set;
1826
+ const { name: name2, keyColumn } = KIND_TABLES[kind];
1827
+ const keys = withSqliteStore(path, "read", false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${name2} ORDER BY seq`).all());
1828
+ return new Set(keys.map(({ key }) => key));
1829
+ }
1830
+ function parsedObject(body) {
1831
+ try {
1832
+ const value = JSON.parse(body);
1833
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1834
+ } catch {
1835
+ return null;
1836
+ }
1837
+ }
1838
+ function rowOf(body, where) {
1839
+ const value = parsedObject(body);
1840
+ if (value === null) {
1841
+ throw new Error(`effort store: ${where} does not hold a JSON object;` + " no append writes one");
1842
+ }
1843
+ return value;
1844
+ }
1845
+ function readKind2(path, kind) {
1846
+ if (!existsSync5(path))
1847
+ return [];
1848
+ const { name: name2 } = KIND_TABLES[kind];
1849
+ const stored = withSqliteStore(path, "read", false, (db) => db.query(`SELECT seq, row_json AS body FROM ${name2} ORDER BY seq`).all());
1850
+ return stored.map(({ seq, body }) => rowOf(body, `${path}: ${name2} row ${seq}`));
1851
+ }
1852
+ function openSqliteStore(repoRoot) {
1853
+ const path = sqliteStorePath(repoRoot);
1854
+ return {
1855
+ append: (kind, rows) => appendKind2(path, kind, rows),
1856
+ keys: (kind) => keysOfKind2(path, kind),
1857
+ read: (kind) => readKind2(path, kind),
1858
+ path: () => path
1859
+ };
1860
+ }
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 }] : []);
1085
2697
  }
1086
- function writeNames(plan) {
1087
- const adoption = plan.adopted.length > 0 ? [ADOPTION_NAME] : [];
1088
- return [...adoption, ...plan.pending.map(({ id }) => id)];
2698
+ function carriedColumns(db, table) {
2699
+ return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name: name2 }) => name2).filter((name2) => name2 !== "seq");
1089
2700
  }
1090
- function nothingWritten(db) {
1091
- return { adopted: [], applied: [], userVersion: readUserVersion(db) };
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
+ }
1092
2712
  }
1093
- function readUserVersion(db) {
1094
- const row = db.query("PRAGMA user_version").get();
1095
- return row?.user_version ?? 0;
2713
+ function valueOf(row, column) {
2714
+ return row[column] ?? null;
1096
2715
  }
1097
- function readMigrationLog(db, path) {
1098
- const table = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE);
1099
- if (table === null)
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)
1100
2720
  return null;
1101
- return db.query(`SELECT id, sha256, breaks, applied_at, applied_by FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map((row) => ({
1102
- id: row.id,
1103
- sha256: row.sha256,
1104
- breaks: parseBreaks(row.breaks, row.id, path),
1105
- appliedAt: row.applied_at,
1106
- appliedBy: row.applied_by
1107
- }));
2721
+ return local.query(`SELECT seq FROM ${quoted(table)} WHERE origin_store = ? AND origin_seq = ?`).get(store, seq)?.seq ?? null;
1108
2722
  }
1109
- function parseBreaks(text, id, path) {
1110
- const refused = new Error(`effort store: ${path} logs migration ${id} with breaks ${text},` + " which is not a JSON array of words; refusing to read or write it");
1111
- let parsed;
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)})`;
1112
2731
  try {
1113
- parsed = JSON.parse(text);
1114
- } catch (cause) {
1115
- throw new Error(refused.message, { cause });
1116
- }
1117
- if (!Array.isArray(parsed) || !parsed.every((word) => typeof word === "string"))
1118
- throw refused;
1119
- return parsed;
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
+ };
1120
2762
  }
1121
- function bodyOf(migrations, id) {
1122
- const migration = migrations.find((candidate) => candidate.id === id);
1123
- if (migration === undefined)
1124
- throw new Error(`bring forward: no migration ${id} in the catalogue it planned from`);
1125
- return migration.sql;
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)))();
1126
2767
  }
1127
- function logMigration(db, entry, stamp) {
1128
- db.run(`INSERT INTO ${MIGRATION_LOG_TABLE} (id, sha256, breaks, applied_at, applied_by) VALUES (?, ?, ?, ?, ?)`, [entry.id, entry.sha256, JSON.stringify(entry.breaks), stamp.appliedAt, stamp.appliedBy]);
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)))();
1129
2859
  }
1130
2860
 
1131
- // src/effort/store/settings.ts
1132
- var DEFAULT_STORE_SETTINGS = Object.freeze({
1133
- busyTimeoutMs: CONFIG_DEFAULTS.effortBusyTimeoutMs
1134
- });
1135
- var active = DEFAULT_STORE_SETTINGS;
1136
- function setActiveStoreSettings(settings) {
1137
- if (settings === null) {
1138
- active = DEFAULT_STORE_SETTINGS;
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)
1139
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}`);
1140
2877
  }
1141
- if (!isBusyTimeoutMs(settings.busyTimeoutMs)) {
1142
- throw new RangeError(`effort store: busy timeout ${String(settings.busyTimeoutMs)} is not a whole number ` + `of milliseconds from ${String(BUSY_TIMEOUT_MIN_MS)} to ${String(BUSY_TIMEOUT_MAX_MS)}`);
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();
1143
2903
  }
1144
- active = Object.freeze({ busyTimeoutMs: settings.busyTimeoutMs });
1145
2904
  }
1146
- function activeStoreSettings() {
1147
- return active;
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 };
1148
2944
  }
1149
2945
 
1150
- // src/effort/store/sqlite.ts
1151
- var SQLITE_STORE_FILE_NAME = "effort.sqlite";
1152
- function sqliteStorePath(repoRoot) {
1153
- return join7(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
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
+ }
1154
2956
  }
1155
- var KIND_TABLES = {
1156
- sessions: { name: "sessions", keyColumn: "session_id" },
1157
- commits: { name: "commits", keyColumn: "sha" }
1158
- };
1159
- var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
1160
- function hasKey(entry) {
1161
- return entry.key !== null;
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}`);
1162
2966
  }
1163
- function refused(index, length, reason) {
1164
- return new Error(`effort store: row ${index} of ${length} ${reason}; batch refused`);
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}`);
1165
2971
  }
1166
- function batchEntries(kind, rows) {
1167
- const keyOf = EFFORT_KEY_PROJECTIONS[kind];
1168
- const projected = rows.map((row) => ({ key: keyOf(row), row }));
1169
- const keyed = projected.filter(hasKey);
1170
- if (keyed.length < projected.length) {
1171
- const keylessAt = projected.findIndex((entry) => !hasKey(entry));
1172
- throw refused(keylessAt, rows.length, "carries no key");
1173
- }
1174
- const unholdableAt = keyed.findIndex(({ key }) => LONE_SURROGATE.test(key));
1175
- if (unholdableAt !== -1) {
1176
- const reason = "carries a key with a lone UTF-16 surrogate," + " which SQLite text cannot hold";
1177
- throw refused(unholdableAt, rows.length, reason);
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.");
1178
2985
  }
1179
- return keyed.map(({ key, row }) => ({ key, body: JSON.stringify(row) }));
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.");
1180
2989
  }
1181
- function withSqliteStore(path, access, create, use) {
1182
- guardTestProcess(path);
1183
- if (create)
1184
- mkdirSync3(dirname6(path), { recursive: true });
1185
- const db = new Database(path, { readwrite: true, create });
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 });
1186
2998
  try {
1187
- db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
1188
- bringForward(db, path, access, "open");
1189
- return use(db);
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;
1190
3001
  } finally {
1191
3002
  db.close();
1192
3003
  }
1193
3004
  }
1194
- function insertBatch(db, table, entries) {
1195
- const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json) VALUES (?, ?)` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
1196
- const insertAll = db.transaction(() => entries.reduce((appended, { key, body }) => appended + insert.run(key, body).changes, 0));
1197
- return insertAll.immediate();
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;
1198
3013
  }
1199
- function writeSqliteStore(path, rowCount, empty, insert) {
1200
- const exists = existsSync5(path);
1201
- if (rowCount > 0)
1202
- return withSqliteStore(path, "write", !exists, insert);
1203
- if (exists)
1204
- withSqliteStore(path, "write", false, () => {
1205
- return;
1206
- });
1207
- return empty;
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
+ }
1208
3046
  }
1209
- function appendKind2(path, kind, rows) {
1210
- const entries = batchEntries(kind, rows);
1211
- const table = KIND_TABLES[kind];
1212
- const appended = writeSqliteStore(path, entries.length, 0, (db) => insertBatch(db, table, entries));
1213
- return { path, appended, skipped: entries.length - appended };
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
+ })();
1214
3068
  }
1215
- function keysOfKind2(path, kind) {
1216
- if (!existsSync5(path))
1217
- return new Set;
1218
- const { name: name2, keyColumn } = KIND_TABLES[kind];
1219
- const keys = withSqliteStore(path, "read", false, (db) => db.query(`SELECT ${keyColumn} AS key FROM ${name2} ORDER BY seq`).all());
1220
- return new Set(keys.map(({ key }) => key));
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
+ };
1221
3075
  }
1222
- function parsedObject(body) {
3076
+ function buildMerged(path, parallelPath, other, merge2) {
3077
+ vacuumInto(path, parallelPath);
3078
+ const db = new Database5(parallelPath, { readwrite: true });
1223
3079
  try {
1224
- const value = JSON.parse(body);
1225
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1226
- } catch {
1227
- return null;
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();
1228
3091
  }
1229
3092
  }
1230
- function rowOf(body, where) {
1231
- const value = parsedObject(body);
1232
- if (value === null) {
1233
- throw new Error(`effort store: ${where} does not hold a JSON object;` + " no append writes one");
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();
1234
3135
  }
1235
- return value;
1236
3136
  }
1237
- function readKind2(path, kind) {
1238
- if (!existsSync5(path))
1239
- return [];
1240
- const { name: name2 } = KIND_TABLES[kind];
1241
- const stored = withSqliteStore(path, "read", false, (db) => db.query(`SELECT seq, row_json AS body FROM ${name2} ORDER BY seq`).all());
1242
- return stored.map(({ seq, body }) => rowOf(body, `${path}: ${name2} row ${seq}`));
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";
1243
3144
  }
1244
- function openSqliteStore(repoRoot) {
1245
- const path = sqliteStorePath(repoRoot);
1246
- return {
1247
- append: (kind, rows) => appendKind2(path, kind, rows),
1248
- keys: (kind) => keysOfKind2(path, kind),
1249
- read: (kind) => readKind2(path, kind),
1250
- path: () => path
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 });
1251
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}`);
1252
3233
  }
1253
3234
 
1254
3235
  // src/adapters/learning/local.ts
1255
3236
  import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
1256
- import { join as join10 } from "path";
3237
+ import { join as join15 } from "path";
1257
3238
 
1258
3239
  // src/commands/instinct/instinct-records.ts
1259
- import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
1260
- import { join as join9 } from "path";
3240
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
3241
+ import { join as join14 } from "path";
1261
3242
 
1262
3243
  // src/check/layout.ts
1263
- import { readdirSync as readdirSync2, statSync } from "fs";
1264
- import { join as join8, 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";
1265
3246
  var SKILL_FILE = "SKILL.md";
1266
3247
  var MARKDOWN_EXTENSION = ".md";
1267
3248
  var LAYOUT_SEVERITY = Object.freeze({
@@ -1279,7 +3260,7 @@ function layoutIssue(code, message) {
1279
3260
  }
1280
3261
  function entryKind(path) {
1281
3262
  try {
1282
- const stats = statSync(path);
3263
+ const stats = statSync4(path);
1283
3264
  if (stats.isFile())
1284
3265
  return "file";
1285
3266
  if (stats.isDirectory())
@@ -1290,7 +3271,7 @@ function entryKind(path) {
1290
3271
  }
1291
3272
  }
1292
3273
  function visibleNames(dir) {
1293
- 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));
1294
3275
  }
1295
3276
  function isMarkdown(name2) {
1296
3277
  return name2.endsWith(MARKDOWN_EXTENSION);
@@ -1378,7 +3359,7 @@ function scanGroupMember(root, dir, name2) {
1378
3359
  const children = visibleNames(dir);
1379
3360
  if (children.length === 0)
1380
3361
  return [dotfileOnlyEntry("skill", root, dir)];
1381
- const skillPath = join8(dir, SKILL_FILE);
3362
+ const skillPath = join13(dir, SKILL_FILE);
1382
3363
  if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
1383
3364
  return [nestedGroupEntry(root, skillPath, name2)];
1384
3365
  }
@@ -1387,7 +3368,7 @@ function scanGroupMember(root, dir, name2) {
1387
3368
  function scanSkillGroup(root, dir, children) {
1388
3369
  const entries = [];
1389
3370
  for (const child of children) {
1390
- const path = join8(dir, child);
3371
+ const path = join13(dir, child);
1391
3372
  const found = entryKind(path);
1392
3373
  if (found === null) {
1393
3374
  entries.push(unreadableEntry("skill", root, path));
@@ -1408,7 +3389,7 @@ function scanSkillDirectory(root, dir, name2) {
1408
3389
  const children = visibleNames(dir);
1409
3390
  if (children.length === 0)
1410
3391
  return [dotfileOnlyEntry("skill", root, dir)];
1411
- const skillPath = join8(dir, SKILL_FILE);
3392
+ const skillPath = join13(dir, SKILL_FILE);
1412
3393
  if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
1413
3394
  return [skillFileEntry(skillPath, name2)];
1414
3395
  }
@@ -1417,7 +3398,7 @@ function scanSkillDirectory(root, dir, name2) {
1417
3398
  function scanSkillRoot(root) {
1418
3399
  const entries = [];
1419
3400
  for (const name2 of visibleNames(root)) {
1420
- const path = join8(root, name2);
3401
+ const path = join13(root, name2);
1421
3402
  const found = entryKind(path);
1422
3403
  if (found === null) {
1423
3404
  entries.push(unreadableEntry("skill", root, path));
@@ -1438,7 +3419,7 @@ function scanInstinctSubdirectory(root, dir) {
1438
3419
  return [dotfileOnlyEntry("instinct", root, dir)];
1439
3420
  const entries = [];
1440
3421
  for (const child of children) {
1441
- const path = join8(dir, child);
3422
+ const path = join13(dir, child);
1442
3423
  const found = entryKind(path);
1443
3424
  if (found === null) {
1444
3425
  entries.push(unreadableEntry("instinct", root, path));
@@ -1456,7 +3437,7 @@ function scanInstinctSubdirectory(root, dir) {
1456
3437
  function scanInstinctRoot(root) {
1457
3438
  const entries = [];
1458
3439
  for (const name2 of visibleNames(root)) {
1459
- const path = join8(root, name2);
3440
+ const path = join13(root, name2);
1460
3441
  const found = entryKind(path);
1461
3442
  if (found === null) {
1462
3443
  entries.push(unreadableEntry("instinct", root, path));
@@ -1515,7 +3496,7 @@ function scanLine(text, offset) {
1515
3496
  next: index + 1
1516
3497
  };
1517
3498
  }
1518
- function isRecord(value) {
3499
+ function isRecord3(value) {
1519
3500
  return typeof value === "object" && value !== null && !Array.isArray(value);
1520
3501
  }
1521
3502
  function parseBlock(yaml) {
@@ -1526,7 +3507,7 @@ function parseBlock(yaml) {
1526
3507
  } catch {
1527
3508
  return null;
1528
3509
  }
1529
- return isRecord(parsed) ? parsed : null;
3510
+ return isRecord3(parsed) ? parsed : null;
1530
3511
  }
1531
3512
  function readFrontmatterDocument(text) {
1532
3513
  const opening = scanLine(text, 0);
@@ -1583,32 +3564,6 @@ function writeFrontmatter(document, data) {
1583
3564
  ].join("");
1584
3565
  }
1585
3566
 
1586
- // src/schema/project-id.ts
1587
- import { spawnSync } from "child_process";
1588
- var PROJECT_ID_LENGTH = 12;
1589
- var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
1590
- function hasColonAfterAt(url) {
1591
- const at = url.indexOf("@");
1592
- return at !== -1 && url.lastIndexOf(":") > at;
1593
- }
1594
- function normalizeRemote(url) {
1595
- const stripped = url.replace(/:\/\/[^@]+@/, "://");
1596
- const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
1597
- const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
1598
- return network ? trimmed.toLowerCase() : trimmed;
1599
- }
1600
- function gitRemoteUrl(dir) {
1601
- const result = spawnSync("git", ["remote", "get-url", "origin"], {
1602
- cwd: dir,
1603
- encoding: "utf8",
1604
- env: { ...process.env, LC_ALL: "C" }
1605
- });
1606
- if (result.error !== undefined || result.status !== 0)
1607
- return null;
1608
- const url = result.stdout.trim();
1609
- return url === "" ? null : url;
1610
- }
1611
-
1612
3567
  // src/schema/instinct.ts
1613
3568
  var INSTINCT_KINDS = FINDING_KINDS;
1614
3569
  var INSTINCT_SIGNALS = FINDING_SIGNALS;
@@ -1935,7 +3890,7 @@ function unreadableIssue(message) {
1935
3890
  function readRecord(path, scope, id) {
1936
3891
  let text;
1937
3892
  try {
1938
- text = readFileSync4(path, "utf8");
3893
+ text = readFileSync8(path, "utf8");
1939
3894
  } catch (error) {
1940
3895
  const message = error instanceof Error ? error.message : String(error);
1941
3896
  return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
@@ -1943,15 +3898,15 @@ function readRecord(path, scope, id) {
1943
3898
  const parsed = parseInstinct(text);
1944
3899
  return { scope, path, id, instinct: parsed.instinct, issues: parsed.issues };
1945
3900
  }
1946
- function isFile(path) {
3901
+ function isFile2(path) {
1947
3902
  try {
1948
- return statSync2(path).isFile();
3903
+ return statSync5(path).isFile();
1949
3904
  } catch {
1950
3905
  return false;
1951
3906
  }
1952
3907
  }
1953
3908
  function recordFiles(dir) {
1954
- return readdirSync3(dir).filter((name2) => isRecordFile(name2) && isFile(join9(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));
1955
3910
  }
1956
3911
  function readScope(scope, dir) {
1957
3912
  if (!tierExists(dir))
@@ -1960,7 +3915,7 @@ function readScope(scope, dir) {
1960
3915
  scope,
1961
3916
  dir,
1962
3917
  exists: true,
1963
- records: recordFiles(dir).map((name2) => readRecord(join9(dir, name2), scope, recordId(name2)))
3918
+ records: recordFiles(dir).map((name2) => readRecord(join14(dir, name2), scope, recordId(name2)))
1964
3919
  };
1965
3920
  }
1966
3921
  function readScopes(seams) {
@@ -1979,7 +3934,7 @@ function instinctProject(context, name2) {
1979
3934
  }
1980
3935
 
1981
3936
  // src/adapters/learning/held.ts
1982
- function compareText(left, right) {
3937
+ function compareText2(left, right) {
1983
3938
  if (left < right) {
1984
3939
  return -1;
1985
3940
  }
@@ -1989,7 +3944,7 @@ function compareText(left, right) {
1989
3944
  return 0;
1990
3945
  }
1991
3946
  function byAge(left, right) {
1992
- return compareText(left.createdAt, right.createdAt) || compareText(left.id, right.id);
3947
+ return compareText2(left.createdAt, right.createdAt) || compareText2(left.id, right.id);
1993
3948
  }
1994
3949
  function toHeldRecord(instinct) {
1995
3950
  return {
@@ -2263,7 +4218,7 @@ function heldChanges(dir, held, result, described) {
2263
4218
  if (issue2 !== undefined) {
2264
4219
  throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue2.field}: ${issue2.message}`);
2265
4220
  }
2266
- const path = join10(dir, file);
4221
+ const path = join15(dir, file);
2267
4222
  if (held.unreadable.has(path)) {
2268
4223
  throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
2269
4224
  }
@@ -2293,18 +4248,18 @@ function pushLogLines(sourceId, described, result) {
2293
4248
  });
2294
4249
  }
2295
4250
  function localInstinctsDir(repoRoot) {
2296
- return join10(repoRoot, RAFA_INSTINCTS_PATH);
4251
+ return join15(repoRoot, RAFA_INSTINCTS_PATH);
2297
4252
  }
2298
4253
  function userInstinctsDir(home) {
2299
- return join10(home, RAFA_INSTINCTS_PATH);
4254
+ return join15(home, RAFA_INSTINCTS_PATH);
2300
4255
  }
2301
4256
  function createLocalLearning(options) {
2302
4257
  const { instinctsDir, minConfidence } = options;
2303
4258
  const userDir = userInstinctsDir(options.home);
2304
4259
  const now = options.now ?? (() => new Date().toISOString());
2305
4260
  const warn = options.warn ?? warnThroughActiveOutput;
2306
- const instinctsPath = join10(instinctsDir, INSTINCTS_FILE);
2307
- const flagsPath = join10(instinctsDir, FLAGS_FILE);
4261
+ const instinctsPath = join15(instinctsDir, INSTINCTS_FILE);
4262
+ const flagsPath = join15(instinctsDir, FLAGS_FILE);
2308
4263
  function readHeld(scope, dir) {
2309
4264
  const { records } = readScope(scope, dir);
2310
4265
  const unreadable = new Set;
@@ -2421,9 +4376,9 @@ function createJsonOutput({
2421
4376
  }
2422
4377
 
2423
4378
  // src/adapters/planner/claude.ts
2424
- import { existsSync as existsSync6 } from "fs";
4379
+ import { existsSync as existsSync13 } from "fs";
2425
4380
  import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2426
- import { posix, resolve as resolve3 } from "path";
4381
+ import { posix, resolve as resolve6 } from "path";
2427
4382
 
2428
4383
  // src/board/spec-review.ts
2429
4384
  var SPEC_REVIEW_KIND = "spec-review";
@@ -2459,8 +4414,8 @@ function describeValue3(value) {
2459
4414
  function messageOf2(error) {
2460
4415
  return error instanceof Error ? error.message : String(error);
2461
4416
  }
2462
- function counted(count, noun) {
2463
- return count === 1 ? `1 ${noun}` : `${String(count)} ${noun}s`;
4417
+ function counted(count2, noun) {
4418
+ return count2 === 1 ? `1 ${noun}` : `${String(count2)} ${noun}s`;
2464
4419
  }
2465
4420
  function valueAt(mapping, key) {
2466
4421
  return Object.hasOwn(mapping, key) ? mapping[key] ?? null : null;
@@ -2686,17 +4641,18 @@ function guardSpend() {
2686
4641
  if (refusal !== null)
2687
4642
  throw new UndeclaredSpendError(refusal);
2688
4643
  }
2689
- async function spawnClaude(args, prompt) {
4644
+ async function spawnClaude(args, prompt, options = {}) {
2690
4645
  guardSpend();
2691
4646
  if (activeOutputMode() === "json") {
2692
- const { exitCode } = await spawnCaptured(args, prompt);
4647
+ const { exitCode } = await spawnCaptured(args, prompt, options);
2693
4648
  return exitCode;
2694
4649
  }
2695
4650
  const proc = Bun.spawn([CLAUDE_BIN, ...args], {
2696
4651
  stdin: new TextEncoder().encode(prompt),
2697
4652
  stdout: "inherit",
2698
4653
  stderr: "inherit",
2699
- env: sessionSpawnEnv(process.env)
4654
+ env: sessionSpawnEnv(process.env),
4655
+ ...cwdOption(options)
2700
4656
  });
2701
4657
  liveSessions.add(proc);
2702
4658
  try {
@@ -2705,8 +4661,11 @@ async function spawnClaude(args, prompt) {
2705
4661
  liveSessions.delete(proc);
2706
4662
  }
2707
4663
  }
2708
- function runClaude(prompt, settingSources, flags = [], spawn = spawnClaude, served = []) {
2709
- 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 };
2710
4669
  }
2711
4670
  function writeWholeLines(text, write) {
2712
4671
  const lines = text.split(`
@@ -2750,7 +4709,7 @@ async function spawnCaptured(args, prompt, options = {}) {
2750
4709
  stdout: "pipe",
2751
4710
  stderr: "inherit",
2752
4711
  env: sessionSpawnEnv(process.env),
2753
- ...options.cwd === undefined ? {} : { cwd: options.cwd }
4712
+ ...cwdOption(options)
2754
4713
  });
2755
4714
  liveSessions.add(proc);
2756
4715
  let stdout;
@@ -2762,8 +4721,8 @@ async function spawnCaptured(args, prompt, options = {}) {
2762
4721
  }
2763
4722
  return { exitCode: await proc.exited, stdout };
2764
4723
  }
2765
- function runClaudeCaptured(prompt, settingSources, flags = [], spawn = spawnClaudeCaptured, served = []) {
2766
- 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);
2767
4726
  }
2768
4727
 
2769
4728
  // src/adapters/planner/claude.ts
@@ -2784,23 +4743,24 @@ class ClaudePlannerError extends Error {
2784
4743
  }
2785
4744
  function createClaudePlanner(options) {
2786
4745
  const { repoRoot, planDir, settingSources, buildPrompt, spawn = spawnClaudeCaptured } = options;
4746
+ const root = resolve6(repoRoot);
2787
4747
  const create = async ({ specPath, stub }) => {
2788
4748
  const planPath = planFilePath(planDir, `PLAN-${stub}.md`);
2789
4749
  const prerequisitesPath = planFilePath(planDir, `PREREQUISITES-${stub}.md`);
2790
- const isWritten = (path) => existsSync6(resolve3(repoRoot, path));
4750
+ const isWritten = (path) => existsSync13(resolve6(root, path));
2791
4751
  if (isWritten(planPath)) {
2792
4752
  throw new ClaudePlannerError(`${planPath} already exists`, FAILURE_EXIT_CODE);
2793
4753
  }
2794
- const specContent = await readFile2(resolve3(repoRoot, specPath), "utf8");
2795
- await mkdir2(resolve3(repoRoot, planDir), { recursive: true });
2796
- 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 });
2797
4757
  const review = parseSpecReview(session.stdout);
2798
4758
  const { exitCode } = session;
2799
4759
  if (exitCode !== 0) {
2800
4760
  throw new ClaudePlannerError(`Plan generation failed (exit ${exitCode}).`, exitCode, review);
2801
4761
  }
2802
4762
  if (!isWritten(planPath)) {
2803
- 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);
2804
4764
  }
2805
4765
  return {
2806
4766
  planPath,
@@ -2903,8 +4863,8 @@ async function withDeadline(work, timeoutMs) {
2903
4863
  if (timeoutMs === undefined)
2904
4864
  return work;
2905
4865
  let timer;
2906
- const deadline = new Promise((resolve4) => {
2907
- timer = setTimeout(() => resolve4(TIMED_OUT), timeoutMs);
4866
+ const deadline = new Promise((resolve7) => {
4867
+ timer = setTimeout(() => resolve7(TIMED_OUT), timeoutMs);
2908
4868
  });
2909
4869
  try {
2910
4870
  return await Promise.race([work, deadline]);
@@ -2919,7 +4879,7 @@ function notOnPath(command, cwd, path) {
2919
4879
  function isOneOf(members, value) {
2920
4880
  return typeof value === "string" && members.includes(value);
2921
4881
  }
2922
- function isIssueNumber(value) {
4882
+ function isIssueNumber3(value) {
2923
4883
  return typeof value === "string" && ISSUE_NUMBER.test(value) && Number.isSafeInteger(Number(value));
2924
4884
  }
2925
4885
  function isRepoName(value) {
@@ -2974,7 +4934,7 @@ function issueNumberOfRef(ref) {
2974
4934
  if (ref.kind !== "github") {
2975
4935
  throw new Error(`${PREFIX2}: refused a ref of kind ${describeValue(ref.kind)}; this tracker reads github refs only`);
2976
4936
  }
2977
- if (!isIssueNumber(ref.externalId)) {
4937
+ if (!isIssueNumber3(ref.externalId)) {
2978
4938
  throw new Error(`${PREFIX2}: externalId ${describeValue(ref.externalId)} is not a GitHub issue number,` + " expected a positive whole number with no leading zero");
2979
4939
  }
2980
4940
  if (ref.repo !== undefined && !isRepoName(ref.repo)) {
@@ -3036,7 +4996,7 @@ function listedIssues(stdout) {
3036
4996
  throw refuse2(`${describeValue(row)}, expected a mapping`);
3037
4997
  const { number, url } = row;
3038
4998
  const labels = labelNames(row["labels"]);
3039
- if (typeof number !== "number" || !isIssueNumber(String(number))) {
4999
+ if (typeof number !== "number" || !isIssueNumber3(String(number))) {
3040
5000
  throw refuse2(`with number ${describeValue(number)}, expected an issue number`);
3041
5001
  }
3042
5002
  if (typeof url !== "string")
@@ -3071,7 +5031,7 @@ function issueOf(ref, viewed) {
3071
5031
  module: moduleOfLabels(viewed.labels),
3072
5032
  priority: isOneOf(ISSUE_PRIORITIES, priority) ? priority : null,
3073
5033
  project: null,
3074
- blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(isIssueNumber).map(Number),
5034
+ blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(isIssueNumber3).map(Number),
3075
5035
  ref: { ...ref, url: viewed.url },
3076
5036
  state: stateOf(viewed)
3077
5037
  };
@@ -3209,7 +5169,7 @@ function createGithubTracker(options) {
3209
5169
 
3210
5170
  // src/adapters/tracker/local.ts
3211
5171
  import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3212
- import { join as join11 } from "path";
5172
+ import { join as join16 } from "path";
3213
5173
  var PREFIX3 = "local tracker";
3214
5174
  var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
3215
5175
  var ISSUE_NUMBER2 = /^[1-9]\d*$/;
@@ -3266,7 +5226,7 @@ function issueNumberOfFile(name2) {
3266
5226
  return name2.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name2.slice(0, -ISSUE_EXTENSION.length)) : null;
3267
5227
  }
3268
5228
  function issuePath(issuesDir, number) {
3269
- return join11(issuesDir, `${number}${ISSUE_EXTENSION}`);
5229
+ return join16(issuesDir, `${number}${ISSUE_EXTENSION}`);
3270
5230
  }
3271
5231
  async function issueNumbers(issuesDir) {
3272
5232
  let names;
@@ -3280,7 +5240,7 @@ async function issueNumbers(issuesDir) {
3280
5240
  return names.map(issueNumberOfFile).filter((number) => number !== null).sort((a, b) => a - b);
3281
5241
  }
3282
5242
  function localIssuesDir(repoRoot) {
3283
- return join11(repoRoot, ".rafa", "issues");
5243
+ return join16(repoRoot, ".rafa", "issues");
3284
5244
  }
3285
5245
  function renderLocalIssue(record) {
3286
5246
  const { draft } = record;
@@ -3459,40 +5419,41 @@ function createLocalTracker(options) {
3459
5419
  }
3460
5420
 
3461
5421
  // src/adapters/registry.ts
3462
- var REFUSAL = "adapter registry";
5422
+ var REFUSAL3 = "adapter registry";
3463
5423
  var PORT_VERSIONS = Object.freeze({
3464
5424
  tracker: 1,
3465
5425
  store: 1,
3466
5426
  learning: 1,
3467
5427
  output: 1,
3468
- planner: 1
5428
+ planner: 1,
5429
+ sync: 1
3469
5430
  });
3470
5431
  var SERVED_VERSIONS = new Map(Object.entries(PORT_VERSIONS));
3471
5432
  var PORT_TYPE_LIST = [...SERVED_VERSIONS.keys()].join(", ");
3472
5433
  function checkPortType(port) {
3473
5434
  if (!SERVED_VERSIONS.has(port)) {
3474
- 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}`);
3475
5436
  }
3476
5437
  }
3477
5438
  function accept(adapter, held) {
3478
5439
  if (typeof adapter !== "object" || adapter === null) {
3479
- 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`);
3480
5441
  }
3481
5442
  const { port, kind, portVersion, create } = adapter;
3482
5443
  checkPortType(port);
3483
5444
  if (typeof kind !== "string" || kind === "") {
3484
- 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`);
3485
5446
  }
3486
5447
  const key = `${port}/${kind}`;
3487
5448
  if (typeof create !== "function") {
3488
- 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`);
3489
5450
  }
3490
5451
  const served = SERVED_VERSIONS.get(port);
3491
5452
  if (portVersion !== served) {
3492
- 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}`);
3493
5454
  }
3494
5455
  if (held.some((other) => other.port === port && other.kind === kind)) {
3495
- throw new Error(`${REFUSAL}: ${key} is already registered`);
5456
+ throw new Error(`${REFUSAL3}: ${key} is already registered`);
3496
5457
  }
3497
5458
  return Object.freeze({ port, kind, portVersion, create });
3498
5459
  }
@@ -3503,7 +5464,8 @@ function indexByPort(adapters) {
3503
5464
  store: only("store"),
3504
5465
  learning: only("learning"),
3505
5466
  output: only("output"),
3506
- planner: only("planner")
5467
+ planner: only("planner"),
5468
+ sync: only("sync")
3507
5469
  };
3508
5470
  }
3509
5471
  function createAdapterRegistry(adapters = []) {
@@ -3518,8 +5480,8 @@ function createAdapterRegistry(adapters = []) {
3518
5480
  if (adapter !== undefined)
3519
5481
  return adapter;
3520
5482
  const registered = [...byPort[port].keys()];
3521
- const listed = registered.length === 0 ? "none" : registered.join(", ");
3522
- 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}`);
3523
5485
  },
3524
5486
  kinds: (port) => {
3525
5487
  checkPortType(port);
@@ -3586,16 +5548,33 @@ var CORE_ADAPTERS = [
3586
5548
  portVersion: PORT_VERSIONS.planner,
3587
5549
  create: ({ repoRoot, settingSources, planPrompt, planDir, claude }) => {
3588
5550
  if (!Array.isArray(settingSources)) {
3589
- 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");
3590
5552
  }
3591
5553
  if (typeof planPrompt !== "function") {
3592
- 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");
3593
5555
  }
3594
5556
  if (typeof planDir !== "string") {
3595
- 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");
3596
5558
  }
3597
5559
  return createClaudePlanner({ repoRoot, planDir, settingSources, buildPrompt: planPrompt, spawn: claude });
3598
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
+ }
3599
5578
  }
3600
5579
  ];
3601
5580
  var CORE_ADAPTER_REGISTRY = createAdapterRegistry(CORE_ADAPTERS);
@@ -3617,4 +5596,4 @@ function selectEffortStore(repoRoot, config) {
3617
5596
  return adapter.create({ repoRoot });
3618
5597
  }
3619
5598
 
3620
- 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, 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, bringForward, readStoreSchema, writeNames, setActiveStoreSettings, activeStoreSettings, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, normalizeRemote, gitRemoteUrl, 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 };