@open-tomato/rafa 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -238,13 +238,13 @@ function openNdjsonStore(repoRoot) {
238
238
  }
239
239
 
240
240
  // 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";
241
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
242
+ import { dirname as dirname9, join as join9 } from "path";
243
243
  import { Database } from "bun:sqlite";
244
244
 
245
245
  // package.json
246
246
  var name = "@open-tomato/rafa";
247
- var version = "0.26.0";
247
+ var version = "0.28.0";
248
248
 
249
249
  // src/cli/version.ts
250
250
  var RAFA_VERSION = version;
@@ -254,19 +254,29 @@ function versionLine() {
254
254
 
255
255
  // src/effort/store/development-build.ts
256
256
  import { tmpdir as tmpdir2 } from "os";
257
- import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
257
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute3 } from "path";
258
258
 
259
259
  // src/loop/sessions.ts
260
260
  import {
261
261
  linkSync,
262
- mkdirSync as mkdirSync2,
262
+ mkdirSync as mkdirSync4,
263
263
  readdirSync,
264
- readFileSync as readFileSync2,
265
- renameSync,
266
- rmSync,
267
- writeFileSync
264
+ readFileSync as readFileSync4,
265
+ renameSync as renameSync3,
266
+ rmSync as rmSync3,
267
+ writeFileSync as writeFileSync3
268
268
  } from "fs";
269
- import { basename, dirname as dirname3, join as join5 } from "path";
269
+ import { basename, dirname as dirname5, join as join7 } from "path";
270
+
271
+ // src/next/hop-record.ts
272
+ import { randomUUID as randomUUID2 } from "crypto";
273
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
274
+ import { dirname as dirname4, join as join6 } from "path";
275
+
276
+ // src/project/position.ts
277
+ import { randomUUID } from "crypto";
278
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
279
+ import { dirname as dirname3, join as join5 } from "path";
270
280
 
271
281
  // src/project/scope.ts
272
282
  import { existsSync as existsSync3, realpathSync as realpathSync2 } from "fs";
@@ -344,6 +354,185 @@ function resolveScope(start, seams) {
344
354
  return { found: true, root: walk.root, home, project: scopeAt(walk.root), user };
345
355
  }
346
356
 
357
+ // src/project/position.ts
358
+ var POSITION_FILE = "position.json";
359
+ function positionFilePath(root) {
360
+ return join5(scopeAt(root).dir, POSITION_FILE);
361
+ }
362
+ function isRecord(value) {
363
+ return typeof value === "object" && value !== null && !Array.isArray(value);
364
+ }
365
+ function isIssueNumber(value) {
366
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
367
+ }
368
+ function asPlace(value) {
369
+ if (!isRecord(value) || !isIssueNumber(value.board))
370
+ return null;
371
+ if (value.epic !== null && !isIssueNumber(value.epic))
372
+ return null;
373
+ return { board: value.board, epic: value.epic };
374
+ }
375
+ function asPosition(value) {
376
+ if (!isRecord(value))
377
+ return null;
378
+ const current = asPlace(value.current);
379
+ const home = asPlace(value.home);
380
+ const previous = value.previous === null ? null : asPlace(value.previous);
381
+ if (current === null || home === null)
382
+ return null;
383
+ if (previous === null && value.previous !== null)
384
+ return null;
385
+ return { current, previous, home };
386
+ }
387
+ function readPositionFile(root) {
388
+ const file = positionFilePath(root);
389
+ let text;
390
+ try {
391
+ text = readFileSync2(file, "utf8");
392
+ } catch (error) {
393
+ const code = error.code;
394
+ return code === "ENOENT" ? { set: false, reason: "absent", detail: `No position file at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
395
+ }
396
+ let parsed;
397
+ try {
398
+ parsed = JSON.parse(text);
399
+ } catch (error) {
400
+ return { set: false, reason: "invalid-json", detail: `${file} holds no JSON: ${messageOf(error)}` };
401
+ }
402
+ const position = asPosition(parsed);
403
+ return position === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a current, previous and home place` } : { set: true, position };
404
+ }
405
+ function writePositionFile(root, position) {
406
+ const file = positionFilePath(root);
407
+ mkdirSync2(dirname3(file), { recursive: true });
408
+ const temporary = `${file}.${String(process.pid)}.${randomUUID()}.tmp`;
409
+ const { current, previous, home } = position;
410
+ try {
411
+ writeFileSync(temporary, `${JSON.stringify({ current, previous, home }, null, 2)}
412
+ `);
413
+ renameSync(temporary, file);
414
+ } catch (error) {
415
+ rmSync(temporary, { force: true });
416
+ throw error;
417
+ }
418
+ }
419
+ function positionAt(place) {
420
+ return { current: place, previous: null, home: place };
421
+ }
422
+ function rehome(position, place) {
423
+ return { current: place, previous: position.current, home: place };
424
+ }
425
+ function hop(position, place) {
426
+ return { current: place, previous: position.current, home: position.home };
427
+ }
428
+ function goHome(position) {
429
+ return { current: position.home, previous: position.current, home: position.home };
430
+ }
431
+
432
+ // src/next/hop-record.ts
433
+ var HOP_FILE = "hop.json";
434
+ var HOP_KINDS = ["blocker", "dry"];
435
+ var HOP_STATES = ["away", "waiting", "merged", "halted"];
436
+ function hopFilePath(root) {
437
+ return join6(scopeAt(root).dir, HOP_FILE);
438
+ }
439
+ function isRecord2(value) {
440
+ return typeof value === "object" && value !== null && !Array.isArray(value);
441
+ }
442
+ function isIssueNumber2(value) {
443
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
444
+ }
445
+ function isIssueOrNull(value) {
446
+ return value === null || isIssueNumber2(value);
447
+ }
448
+ function isTimestamp(value) {
449
+ return typeof value === "string" && !Number.isNaN(Date.parse(value));
450
+ }
451
+ function issuesMatchKind(kind, blocked, target) {
452
+ return kind === "blocker" ? blocked !== null && target !== null : blocked === null && target === null;
453
+ }
454
+ function asHopRecord(value) {
455
+ if (!isRecord2(value))
456
+ return null;
457
+ const { kind, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = value;
458
+ const home = asPlace(value.home);
459
+ const from = asPlace(value.from);
460
+ if (typeof kind !== "string" || !HOP_KINDS.includes(kind))
461
+ return null;
462
+ if (typeof state !== "string" || !HOP_STATES.includes(state))
463
+ return null;
464
+ if (home === null || from === null)
465
+ return null;
466
+ if (!isIssueOrNull(blocked) || !isIssueOrNull(target) || !isIssueOrNull(pullRequest))
467
+ return null;
468
+ if (!isIssueNumber2(targetEpic) || !isIssueNumber2(targetBoard) || !isTimestamp(startedAt))
469
+ return null;
470
+ const hopKind = kind;
471
+ if (!issuesMatchKind(hopKind, blocked, target))
472
+ return null;
473
+ return {
474
+ kind: hopKind,
475
+ home,
476
+ from,
477
+ blocked,
478
+ target,
479
+ targetEpic,
480
+ targetBoard,
481
+ state,
482
+ pullRequest,
483
+ startedAt
484
+ };
485
+ }
486
+ function readHopRecord(root) {
487
+ const file = hopFilePath(root);
488
+ let text;
489
+ try {
490
+ text = readFileSync3(file, "utf8");
491
+ } catch (error) {
492
+ const code = error.code;
493
+ return code === "ENOENT" ? { set: false, reason: "absent", detail: `No hop record at ${file}` } : { set: false, reason: "unreadable", detail: `Cannot read ${file}: ${messageOf(error)}` };
494
+ }
495
+ let parsed;
496
+ try {
497
+ parsed = JSON.parse(text);
498
+ } catch (error) {
499
+ return { set: false, reason: "wrong-shape", detail: `${file} holds no JSON: ${messageOf(error)}` };
500
+ }
501
+ const record = asHopRecord(parsed);
502
+ return record === null ? { set: false, reason: "wrong-shape", detail: `${file} does not hold a hop record` } : { set: true, record };
503
+ }
504
+ function ownKeys(record) {
505
+ const { kind, home, from, blocked, target, targetEpic, targetBoard, state, pullRequest, startedAt } = record;
506
+ return {
507
+ kind,
508
+ home: { board: home.board, epic: home.epic },
509
+ from: { board: from.board, epic: from.epic },
510
+ blocked,
511
+ target,
512
+ targetEpic,
513
+ targetBoard,
514
+ state,
515
+ pullRequest,
516
+ startedAt
517
+ };
518
+ }
519
+ function writeHopRecord(root, record) {
520
+ const file = hopFilePath(root);
521
+ mkdirSync3(dirname4(file), { recursive: true });
522
+ const temporary = `${file}.${String(process.pid)}.${randomUUID2()}.tmp`;
523
+ try {
524
+ writeFileSync2(temporary, `${JSON.stringify(ownKeys(record), null, 2)}
525
+ `);
526
+ renameSync2(temporary, file);
527
+ } catch (error) {
528
+ rmSync2(temporary, { force: true });
529
+ throw error;
530
+ }
531
+ }
532
+ function staleAgainst(record, position) {
533
+ return position.home.board !== record.home.board || position.home.epic !== record.home.epic;
534
+ }
535
+
347
536
  // src/loop/sessions.ts
348
537
  var SESSION_STATES = Object.freeze(["running", "paused", "stopped", "done"]);
349
538
  var RUNS_DIR = "runs";
@@ -384,13 +573,13 @@ function isSessionId(value) {
384
573
  return SESSION_ID_PATTERN.test(value);
385
574
  }
386
575
  function runsDir(root) {
387
- return join5(scopeAt(root).dir, RUNS_DIR);
576
+ return join7(scopeAt(root).dir, RUNS_DIR);
388
577
  }
389
578
  function sessionFilePath(root, sessionId) {
390
579
  if (!isSessionId(sessionId)) {
391
580
  throw new Error(`session record: unusable session id ${JSON.stringify(sessionId)}`);
392
581
  }
393
- return join5(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
582
+ return join7(runsDir(root), `${sessionId}${RECORD_EXTENSION}`);
394
583
  }
395
584
  function describeValue2(value) {
396
585
  return value === undefined ? "missing" : JSON.stringify(value) ?? String(value);
@@ -417,6 +606,12 @@ function taskProblem(task) {
417
606
  }
418
607
  return isText(field(task, "text")) ? null : `task.text is ${describeValue2(field(task, "text"))}, expected a non-empty string`;
419
608
  }
609
+ function hopProblem(fields) {
610
+ if (!Object.hasOwn(fields, "hop"))
611
+ return null;
612
+ const hop2 = field(fields, "hop");
613
+ return asHopRecord(hop2) === null ? `hop is ${describeValue2(hop2)}, expected a hop record` : null;
614
+ }
420
615
  function sessionIdProblem(sessionId, file) {
421
616
  if (typeof sessionId !== "string" || !isSessionId(sessionId)) {
422
617
  return `sessionId is ${describeValue2(sessionId)}, expected a plain file name`;
@@ -440,12 +635,18 @@ function recordProblems(fields, file) {
440
635
  isPositiveWhole(pid) ? null : `pid is ${describeValue2(pid)}, expected a whole number from 1`,
441
636
  typeof startedAt === "string" && !Number.isNaN(Date.parse(startedAt)) ? null : `startedAt is ${describeValue2(startedAt)}, expected a timestamp`,
442
637
  SESSION_STATES.includes(state) ? null : `state is ${describeValue2(state)}, expected one of ${SESSION_STATES.join(", ")}`,
443
- taskProblem(field(fields, "task"))
638
+ taskProblem(field(fields, "task")),
639
+ hopProblem(fields)
444
640
  ];
445
641
  return problems.filter((problem) => problem !== null);
446
642
  }
643
+ function freezeHop(value) {
644
+ const hop2 = asHopRecord(value);
645
+ return Object.freeze({ ...hop2, home: Object.freeze(hop2.home), from: Object.freeze(hop2.from) });
646
+ }
447
647
  function freezeRecord(fields) {
448
648
  const task = field(fields, "task");
649
+ const hop2 = field(fields, "hop");
449
650
  return Object.freeze({
450
651
  sessionId: field(fields, "sessionId"),
451
652
  planStub: field(fields, "planStub"),
@@ -454,7 +655,8 @@ function freezeRecord(fields) {
454
655
  pid: field(fields, "pid"),
455
656
  startedAt: field(fields, "startedAt"),
456
657
  state: field(fields, "state"),
457
- task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null
658
+ task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
659
+ ...hop2 === undefined ? {} : { hop: freezeHop(hop2) }
458
660
  });
459
661
  }
460
662
  function parseSessionRecord(text, file) {
@@ -492,7 +694,7 @@ function readState(record, isAlive = isPidAlive) {
492
694
  function readRecordFile(file) {
493
695
  let text;
494
696
  try {
495
- text = readFileSync2(file, "utf8");
697
+ text = readFileSync4(file, "utf8");
496
698
  } catch (error) {
497
699
  throw new SessionRecordError(file, `cannot be read: ${messageOf(error)}`);
498
700
  }
@@ -513,7 +715,7 @@ function byStart(a, b) {
513
715
  function readSessions(root, seams = {}) {
514
716
  const dir = runsDir(root);
515
717
  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) }));
718
+ 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
719
  return Object.freeze(records.sort(byStart));
518
720
  }
519
721
  function readSession(root, sessionId, seams = {}) {
@@ -539,17 +741,17 @@ function checkRecord(record, file) {
539
741
  }
540
742
  function writeRecordFile(file, record, isNew) {
541
743
  checkRecord(record, file);
542
- mkdirSync2(dirname3(file), { recursive: true });
744
+ mkdirSync4(dirname5(file), { recursive: true });
543
745
  const temporary = `${file}.${process.pid}.tmp`;
544
- writeFileSync(temporary, `${JSON.stringify(record, null, 2)}
746
+ writeFileSync3(temporary, `${JSON.stringify(record, null, 2)}
545
747
  `);
546
748
  try {
547
749
  if (isNew)
548
750
  linkSync(temporary, file);
549
751
  else
550
- renameSync(temporary, file);
752
+ renameSync3(temporary, file);
551
753
  } finally {
552
- rmSync(temporary, { force: true });
754
+ rmSync3(temporary, { force: true });
553
755
  }
554
756
  }
555
757
  function beginSession(root, draft, seams = {}) {
@@ -577,10 +779,10 @@ function updateSession(root, sessionId, change) {
577
779
  }
578
780
 
579
781
  // 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";
782
+ import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3 } from "fs";
783
+ import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
582
784
  var MANIFEST = "package.json";
583
- var CHECKOUT_ENTRY = join6("src", "rafa.ts");
785
+ var CHECKOUT_ENTRY = join8("src", "rafa.ts");
584
786
  function realOrResolved(path) {
585
787
  const resolved = resolve2(path);
586
788
  return existsSync4(resolved) ? realpathSync3(resolved) : resolved;
@@ -588,7 +790,7 @@ function realOrResolved(path) {
588
790
  function manifestName(file) {
589
791
  let parsed;
590
792
  try {
591
- parsed = JSON.parse(readFileSync3(file, "utf8"));
793
+ parsed = JSON.parse(readFileSync5(file, "utf8"));
592
794
  } catch {
593
795
  return null;
594
796
  }
@@ -600,10 +802,10 @@ function manifestName(file) {
600
802
  function nearestRafaPackage(start) {
601
803
  let dir = start;
602
804
  for (;; ) {
603
- const manifest = join6(dir, MANIFEST);
805
+ const manifest = join8(dir, MANIFEST);
604
806
  if (existsSync4(manifest) && manifestName(manifest) === name)
605
807
  return dir;
606
- const parent = dirname4(dir);
808
+ const parent = dirname6(dir);
607
809
  if (parent === dir)
608
810
  return null;
609
811
  dir = parent;
@@ -611,8 +813,8 @@ function nearestRafaPackage(start) {
611
813
  }
612
814
  function readRuntimeIdentity(entry = Bun.main) {
613
815
  const real = realOrResolved(entry);
614
- const packageDir = nearestRafaPackage(dirname4(real));
615
- if (packageDir !== null && existsSync4(join6(packageDir, CHECKOUT_ENTRY))) {
816
+ const packageDir = nearestRafaPackage(dirname6(real));
817
+ if (packageDir !== null && existsSync4(join8(packageDir, CHECKOUT_ENTRY))) {
616
818
  return { kind: "development", entry: real, checkout: packageDir };
617
819
  }
618
820
  return { kind: "installed", entry: real };
@@ -638,11 +840,11 @@ class DevelopmentBuildRefusedError extends Error {
638
840
  }
639
841
  }
640
842
  function storeProjectRoot(path) {
641
- const storeDir = dirname5(path);
642
- const scopeDir = dirname5(storeDir);
843
+ const storeDir = dirname7(path);
844
+ const scopeDir = dirname7(storeDir);
643
845
  if (basename2(storeDir) !== EFFORT_DIR_NAME || basename2(scopeDir) !== SCOPE_DIR)
644
846
  return null;
645
- return dirname5(scopeDir);
847
+ return dirname7(scopeDir);
646
848
  }
647
849
  function isOwnedStore(path, env, tempDir) {
648
850
  if (isUnderTempDir(path, tempDir))
@@ -926,6 +1128,120 @@ var SQLITE_MIGRATIONS = [
926
1128
  CHECK ((verdict = 'red') = (json_array_length(failing) > 0))
927
1129
  );
928
1130
  `
1131
+ },
1132
+ {
1133
+ id: "row-origins",
1134
+ breaks: [],
1135
+ sql: `
1136
+ ALTER TABLE sessions ADD COLUMN origin_store TEXT;
1137
+ ALTER TABLE sessions ADD COLUMN origin_seq INTEGER;
1138
+ CREATE UNIQUE INDEX sessions_by_origin
1139
+ ON sessions (origin_store, origin_seq)
1140
+ WHERE origin_store IS NOT NULL;
1141
+
1142
+ ALTER TABLE commits ADD COLUMN origin_store TEXT;
1143
+ ALTER TABLE commits ADD COLUMN origin_seq INTEGER;
1144
+ CREATE UNIQUE INDEX commits_by_origin
1145
+ ON commits (origin_store, origin_seq)
1146
+ WHERE origin_store IS NOT NULL;
1147
+
1148
+ ALTER TABLE findings ADD COLUMN origin_store TEXT;
1149
+ ALTER TABLE findings ADD COLUMN origin_seq INTEGER;
1150
+ CREATE UNIQUE INDEX findings_by_origin
1151
+ ON findings (origin_store, origin_seq)
1152
+ WHERE origin_store IS NOT NULL;
1153
+
1154
+ ALTER TABLE blockers ADD COLUMN origin_store TEXT;
1155
+ ALTER TABLE blockers ADD COLUMN origin_seq INTEGER;
1156
+ CREATE UNIQUE INDEX blockers_by_origin
1157
+ ON blockers (origin_store, origin_seq)
1158
+ WHERE origin_store IS NOT NULL;
1159
+
1160
+ ALTER TABLE out_of_scope_bugs ADD COLUMN origin_store TEXT;
1161
+ ALTER TABLE out_of_scope_bugs ADD COLUMN origin_seq INTEGER;
1162
+ CREATE UNIQUE INDEX out_of_scope_bugs_by_origin
1163
+ ON out_of_scope_bugs (origin_store, origin_seq)
1164
+ WHERE origin_store IS NOT NULL;
1165
+
1166
+ ALTER TABLE report_absences ADD COLUMN origin_store TEXT;
1167
+ ALTER TABLE report_absences ADD COLUMN origin_seq INTEGER;
1168
+ CREATE UNIQUE INDEX report_absences_by_origin
1169
+ ON report_absences (origin_store, origin_seq)
1170
+ WHERE origin_store IS NOT NULL;
1171
+
1172
+ ALTER TABLE task_reports ADD COLUMN origin_store TEXT;
1173
+ ALTER TABLE task_reports ADD COLUMN origin_seq INTEGER;
1174
+ CREATE UNIQUE INDEX task_reports_by_origin
1175
+ ON task_reports (origin_store, origin_seq)
1176
+ WHERE origin_store IS NOT NULL;
1177
+
1178
+ ALTER TABLE preflight ADD COLUMN origin_store TEXT;
1179
+ ALTER TABLE preflight ADD COLUMN origin_seq INTEGER;
1180
+ CREATE UNIQUE INDEX preflight_by_origin
1181
+ ON preflight (origin_store, origin_seq)
1182
+ WHERE origin_store IS NOT NULL;
1183
+
1184
+ ALTER TABLE dispatches ADD COLUMN origin_store TEXT;
1185
+ ALTER TABLE dispatches ADD COLUMN origin_seq INTEGER;
1186
+ CREATE UNIQUE INDEX dispatches_by_origin
1187
+ ON dispatches (origin_store, origin_seq)
1188
+ WHERE origin_store IS NOT NULL;
1189
+
1190
+ ALTER TABLE changes ADD COLUMN origin_store TEXT;
1191
+ ALTER TABLE changes ADD COLUMN origin_seq INTEGER;
1192
+ CREATE UNIQUE INDEX changes_by_origin
1193
+ ON changes (origin_store, origin_seq)
1194
+ WHERE origin_store IS NOT NULL;
1195
+
1196
+ ALTER TABLE skill_invocations ADD COLUMN origin_store TEXT;
1197
+ ALTER TABLE skill_invocations ADD COLUMN origin_seq INTEGER;
1198
+ CREATE UNIQUE INDEX skill_invocations_by_origin
1199
+ ON skill_invocations (origin_store, origin_seq)
1200
+ WHERE origin_store IS NOT NULL;
1201
+
1202
+ ALTER TABLE plan_ci ADD COLUMN origin_store TEXT;
1203
+ ALTER TABLE plan_ci ADD COLUMN origin_seq INTEGER;
1204
+ CREATE UNIQUE INDEX plan_ci_by_origin
1205
+ ON plan_ci (origin_store, origin_seq)
1206
+ WHERE origin_store IS NOT NULL;
1207
+ `
1208
+ },
1209
+ {
1210
+ id: "store-meta",
1211
+ breaks: [],
1212
+ sql: `
1213
+ CREATE TABLE store_meta (
1214
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1215
+ store_id TEXT NOT NULL CHECK (store_id <> ''),
1216
+ project_root_commit TEXT NOT NULL CHECK (project_root_commit <> ''),
1217
+ project_remote TEXT CHECK (project_remote <> ''),
1218
+ host_id TEXT NOT NULL CHECK (host_id <> ''),
1219
+ store_path TEXT NOT NULL CHECK (store_path <> ''),
1220
+ file_dev INTEGER NOT NULL CHECK (typeof(file_dev) = 'integer'),
1221
+ file_ino INTEGER NOT NULL CHECK (typeof(file_ino) = 'integer'),
1222
+ minted_at TEXT NOT NULL CHECK (minted_at <> '')
1223
+ );
1224
+
1225
+ CREATE TABLE merges (
1226
+ seq INTEGER PRIMARY KEY,
1227
+ id TEXT NOT NULL UNIQUE CHECK (id <> ''),
1228
+ other_store TEXT CHECK (other_store <> ''),
1229
+ merged_at TEXT NOT NULL CHECK (merged_at <> ''),
1230
+ rows_added INTEGER NOT NULL CHECK (typeof(rows_added) = 'integer' AND rows_added >= 0),
1231
+ rows_skipped INTEGER NOT NULL CHECK (typeof(rows_skipped) = 'integer' AND rows_skipped >= 0),
1232
+ rows_in_conflict INTEGER NOT NULL CHECK (typeof(rows_in_conflict) = 'integer' AND rows_in_conflict >= 0)
1233
+ );
1234
+
1235
+ CREATE TABLE merge_conflicts (
1236
+ seq INTEGER PRIMARY KEY,
1237
+ merge_id TEXT NOT NULL CHECK (merge_id <> ''),
1238
+ table_name TEXT NOT NULL CHECK (table_name <> ''),
1239
+ local_seq INTEGER NOT NULL CHECK (typeof(local_seq) = 'integer'),
1240
+ field TEXT CHECK (field <> ''),
1241
+ incoming TEXT NOT NULL CHECK (json_valid(incoming) AND json_type(incoming) = 'object'),
1242
+ recorded_at TEXT NOT NULL CHECK (recorded_at <> '')
1243
+ );
1244
+ `
929
1245
  }
930
1246
  ];
931
1247
  var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
@@ -1128,6 +1444,15 @@ function logMigration(db, entry, stamp) {
1128
1444
  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]);
1129
1445
  }
1130
1446
 
1447
+ // src/effort/store/origins.ts
1448
+ var ACROSS_STORES_ORDER = "collected_at, origin_store, origin_seq, seq";
1449
+ var STAMPED_COLUMNS = "seq, origin_store, origin_seq";
1450
+ var STORE_ID = "(SELECT store_id FROM store_meta WHERE id = 1)";
1451
+ function stampedValues(table) {
1452
+ const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${table})`;
1453
+ return `${nextSeq}, ${STORE_ID}, CASE WHEN ${STORE_ID} IS NULL THEN NULL ELSE ${nextSeq} END`;
1454
+ }
1455
+
1131
1456
  // src/effort/store/settings.ts
1132
1457
  var DEFAULT_STORE_SETTINGS = Object.freeze({
1133
1458
  busyTimeoutMs: CONFIG_DEFAULTS.effortBusyTimeoutMs
@@ -1147,10 +1472,211 @@ function activeStoreSettings() {
1147
1472
  return active;
1148
1473
  }
1149
1474
 
1475
+ // src/effort/store/store-meta.ts
1476
+ import { randomUUID as randomUUID3 } from "crypto";
1477
+ import { dirname as dirname8 } from "path";
1478
+
1479
+ // src/effort/store/store-identity.ts
1480
+ import { spawnSync as spawnSync3 } from "child_process";
1481
+ import { createHmac } from "crypto";
1482
+ import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync } from "fs";
1483
+ import { hostname, platform as currentPlatform } from "os";
1484
+
1485
+ // src/pr/git.ts
1486
+ import { spawnSync } from "child_process";
1487
+ function createGitRunner(cwd) {
1488
+ return (args) => {
1489
+ const result = spawnSync("git", [...args], {
1490
+ cwd,
1491
+ encoding: "utf8",
1492
+ env: { ...process.env, LC_ALL: "C" }
1493
+ });
1494
+ if (result.error !== undefined) {
1495
+ return { ok: false, stdout: "", stderr: `could not run git in ${cwd}: ${messageOf(result.error)}` };
1496
+ }
1497
+ return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1498
+ };
1499
+ }
1500
+ function gitSaid(result) {
1501
+ return [result.stderr, result.stdout].map((stream) => stream.trim()).filter((stream) => stream !== "").join(`
1502
+ `);
1503
+ }
1504
+
1505
+ // src/schema/project-id.ts
1506
+ import { spawnSync as spawnSync2 } from "child_process";
1507
+ var PROJECT_ID_LENGTH = 12;
1508
+ var PROJECT_ID_PATTERN = new RegExp(`^[0-9a-f]{${PROJECT_ID_LENGTH}}$`);
1509
+ function hasColonAfterAt(url) {
1510
+ const at = url.indexOf("@");
1511
+ return at !== -1 && url.lastIndexOf(":") > at;
1512
+ }
1513
+ function normalizeRemote(url) {
1514
+ const stripped = url.replace(/:\/\/[^@]+@/, "://");
1515
+ const network = !stripped.startsWith("file://") && (stripped.includes("://") || hasColonAfterAt(stripped));
1516
+ const trimmed = stripped.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "").replace(/^[^@/:]+@([^:/]+):/, "$1/").replace(/\.git\/?$/, "").replace(/\/+$/, "");
1517
+ return network ? trimmed.toLowerCase() : trimmed;
1518
+ }
1519
+ function gitRemoteUrl(dir) {
1520
+ const result = spawnSync2("git", ["remote", "get-url", "origin"], {
1521
+ cwd: dir,
1522
+ encoding: "utf8",
1523
+ env: { ...process.env, LC_ALL: "C" }
1524
+ });
1525
+ if (result.error !== undefined || result.status !== 0)
1526
+ return null;
1527
+ const url = result.stdout.trim();
1528
+ return url === "" ? null : url;
1529
+ }
1530
+
1531
+ // src/effort/store/store-identity.ts
1532
+ var HOST_ID_KEY = "rafa effort store host id";
1533
+ var MACHINE_ID_PATH = "/etc/machine-id";
1534
+ var IOREG_ARGS = ["-rd1", "-c", "IOPlatformExpertDevice"];
1535
+ function hostIdFrom(source, value) {
1536
+ return createHmac("sha256", HOST_ID_KEY).update(`${source}:${value}`).digest("hex");
1537
+ }
1538
+ function readMachineIdFile() {
1539
+ try {
1540
+ const value = readFileSync6(MACHINE_ID_PATH, "utf8").trim();
1541
+ return value === "" ? null : value;
1542
+ } catch {
1543
+ return null;
1544
+ }
1545
+ }
1546
+ function parseIoregPlatformUuid(output) {
1547
+ const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(output);
1548
+ return match?.[1] ?? null;
1549
+ }
1550
+ function readIoregPlatformUuid() {
1551
+ const result = spawnSync3("ioreg", [...IOREG_ARGS], { encoding: "utf8" });
1552
+ if (result.error !== undefined || result.status !== 0)
1553
+ return null;
1554
+ return parseIoregPlatformUuid(result.stdout);
1555
+ }
1556
+ function readHostId(seams = {}) {
1557
+ const platform = seams.platform ?? currentPlatform();
1558
+ if (platform === "linux") {
1559
+ const machineId = (seams.readMachineId ?? readMachineIdFile)();
1560
+ if (machineId !== null)
1561
+ return hostIdFrom("machine-id", machineId);
1562
+ }
1563
+ if (platform === "darwin") {
1564
+ const platformUuid = (seams.readPlatformUuid ?? readIoregPlatformUuid)();
1565
+ if (platformUuid !== null)
1566
+ return hostIdFrom("platform-uuid", platformUuid);
1567
+ }
1568
+ return hostIdFrom("hostname", (seams.readHostname ?? hostname)());
1569
+ }
1570
+ function readStoreFileFacts(path) {
1571
+ const storePath = realpathSync4(path);
1572
+ const stats = statSync(storePath, { bigint: true });
1573
+ return { storePath, fileDev: stats.dev, fileIno: stats.ino };
1574
+ }
1575
+ function observeStore(path, readHost = readHostId) {
1576
+ return { hostId: readHost(), ...readStoreFileFacts(path) };
1577
+ }
1578
+ function movedFacts(recorded, facts) {
1579
+ const sameFile = BigInt(recorded.fileDev) === facts.fileDev && BigInt(recorded.fileIno) === facts.fileIno;
1580
+ const moved = [
1581
+ ["host", recorded.hostId !== facts.hostId],
1582
+ ["path", recorded.storePath !== facts.storePath],
1583
+ ["file", !sameFile]
1584
+ ];
1585
+ return moved.filter(([, differs]) => differs).map(([reason]) => reason);
1586
+ }
1587
+ function decideStoreIdentity(access, recorded, observe) {
1588
+ if (access === "read")
1589
+ return { action: "none" };
1590
+ const facts = observe();
1591
+ if (recorded === null)
1592
+ return { action: "mint", reasons: ["unminted"], facts };
1593
+ const reasons = movedFacts(recorded, facts);
1594
+ return reasons.length === 0 ? { action: "keep", storeId: recorded.storeId } : { action: "mint", reasons, facts };
1595
+ }
1596
+ function smallestRootCommit(git) {
1597
+ const result = git(["rev-list", "--max-parents=0", "HEAD"]);
1598
+ if (!result.ok)
1599
+ return null;
1600
+ const roots = result.stdout.split(`
1601
+ `).map((line) => line.trim()).filter((line) => line !== "");
1602
+ return [...roots].sort()[0] ?? null;
1603
+ }
1604
+ function normalisedRemote(git) {
1605
+ const result = git(["remote", "get-url", "origin"]);
1606
+ if (!result.ok)
1607
+ return null;
1608
+ const remote = normalizeRemote(result.stdout.trim());
1609
+ return remote === "" ? null : remote;
1610
+ }
1611
+ function readProjectIdentity(dir, git = createGitRunner(dir)) {
1612
+ return { rootCommit: smallestRootCommit(git), remote: normalisedRemote(git) };
1613
+ }
1614
+
1615
+ // src/effort/store/store-meta.ts
1616
+ var STORED_INTEGER_BITS = 64;
1617
+ function toStoredInteger(value) {
1618
+ return BigInt.asIntN(STORED_INTEGER_BITS, value);
1619
+ }
1620
+ function fromStoredInteger(text) {
1621
+ return BigInt.asUintN(STORED_INTEGER_BITS, BigInt(text));
1622
+ }
1623
+ function readStoreMeta(db) {
1624
+ 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();
1625
+ if (row === null)
1626
+ return null;
1627
+ return {
1628
+ storeId: row.store_id,
1629
+ projectRootCommit: row.project_root_commit,
1630
+ projectRemote: row.project_remote,
1631
+ hostId: row.host_id,
1632
+ storePath: row.store_path,
1633
+ fileDev: fromStoredInteger(row.file_dev),
1634
+ fileIno: fromStoredInteger(row.file_ino),
1635
+ mintedAt: row.minted_at
1636
+ };
1637
+ }
1638
+ function projectFor(storePath, recorded, seams) {
1639
+ const read = (seams.readProject ?? readProjectIdentity)(dirname8(storePath));
1640
+ if (read.rootCommit !== null)
1641
+ return { rootCommit: read.rootCommit, remote: read.remote };
1642
+ if (recorded === null)
1643
+ return null;
1644
+ return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
1645
+ }
1646
+ function writeStoreMeta(db, facts, project, seams) {
1647
+ const storeId = (seams.newStoreId ?? randomUUID3)();
1648
+ const mintedAt = (seams.now?.() ?? new Date).toISOString();
1649
+ 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);
1650
+ return storeId;
1651
+ }
1652
+ function unminted(decision) {
1653
+ return decision.action === "keep" ? { action: "keep", storeId: decision.storeId } : { action: "none" };
1654
+ }
1655
+ function settleStoreIdentity(db, path, access, seams = {}) {
1656
+ if (access === "read")
1657
+ return { action: "none" };
1658
+ const observe = () => observeStore(path, seams.readHostId ?? readHostId);
1659
+ const recorded = readStoreMeta(db);
1660
+ const first = decideStoreIdentity(access, recorded, observe);
1661
+ if (first.action !== "mint")
1662
+ return unminted(first);
1663
+ const project = projectFor(first.facts.storePath, recorded, seams);
1664
+ if (project === null)
1665
+ return { action: "no-project", reasons: first.reasons };
1666
+ const mintUnderLock = db.transaction(() => {
1667
+ const second = decideStoreIdentity(access, readStoreMeta(db), observe);
1668
+ if (second.action !== "mint")
1669
+ return unminted(second);
1670
+ const storeId = writeStoreMeta(db, second.facts, project, seams);
1671
+ return { action: "mint", storeId, reasons: second.reasons };
1672
+ });
1673
+ return mintUnderLock.immediate();
1674
+ }
1675
+
1150
1676
  // src/effort/store/sqlite.ts
1151
1677
  var SQLITE_STORE_FILE_NAME = "effort.sqlite";
1152
1678
  function sqliteStorePath(repoRoot) {
1153
- return join7(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
1679
+ return join9(effortStoreDir(repoRoot), SQLITE_STORE_FILE_NAME);
1154
1680
  }
1155
1681
  var KIND_TABLES = {
1156
1682
  sessions: { name: "sessions", keyColumn: "session_id" },
@@ -1178,21 +1704,22 @@ function batchEntries(kind, rows) {
1178
1704
  }
1179
1705
  return keyed.map(({ key, row }) => ({ key, body: JSON.stringify(row) }));
1180
1706
  }
1181
- function withSqliteStore(path, access, create, use) {
1707
+ function withSqliteStore(path, access, create, use, identity = {}) {
1182
1708
  guardTestProcess(path);
1183
1709
  if (create)
1184
- mkdirSync3(dirname6(path), { recursive: true });
1710
+ mkdirSync5(dirname9(path), { recursive: true });
1185
1711
  const db = new Database(path, { readwrite: true, create });
1186
1712
  try {
1187
1713
  db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
1188
1714
  bringForward(db, path, access, "open");
1715
+ settleStoreIdentity(db, path, access, identity);
1189
1716
  return use(db);
1190
1717
  } finally {
1191
1718
  db.close();
1192
1719
  }
1193
1720
  }
1194
1721
  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`);
1722
+ const insert = db.query(`INSERT INTO ${table.name} (${table.keyColumn}, row_json, ${STAMPED_COLUMNS})` + ` VALUES (?, ?, ${stampedValues(table.name)})` + ` ON CONFLICT (${table.keyColumn}) DO NOTHING`);
1196
1723
  const insertAll = db.transaction(() => entries.reduce((appended, { key, body }) => appended + insert.run(key, body).changes, 0));
1197
1724
  return insertAll.immediate();
1198
1725
  }
@@ -1253,15 +1780,15 @@ function openSqliteStore(repoRoot) {
1253
1780
 
1254
1781
  // src/adapters/learning/local.ts
1255
1782
  import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
1256
- import { join as join10 } from "path";
1783
+ import { join as join12 } from "path";
1257
1784
 
1258
1785
  // 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";
1786
+ import { readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
1787
+ import { join as join11 } from "path";
1261
1788
 
1262
1789
  // src/check/layout.ts
1263
- import { readdirSync as readdirSync2, statSync } from "fs";
1264
- import { join as join8, relative as relative2 } from "path";
1790
+ import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1791
+ import { join as join10, relative as relative2 } from "path";
1265
1792
  var SKILL_FILE = "SKILL.md";
1266
1793
  var MARKDOWN_EXTENSION = ".md";
1267
1794
  var LAYOUT_SEVERITY = Object.freeze({
@@ -1279,7 +1806,7 @@ function layoutIssue(code, message) {
1279
1806
  }
1280
1807
  function entryKind(path) {
1281
1808
  try {
1282
- const stats = statSync(path);
1809
+ const stats = statSync2(path);
1283
1810
  if (stats.isFile())
1284
1811
  return "file";
1285
1812
  if (stats.isDirectory())
@@ -1378,7 +1905,7 @@ function scanGroupMember(root, dir, name2) {
1378
1905
  const children = visibleNames(dir);
1379
1906
  if (children.length === 0)
1380
1907
  return [dotfileOnlyEntry("skill", root, dir)];
1381
- const skillPath = join8(dir, SKILL_FILE);
1908
+ const skillPath = join10(dir, SKILL_FILE);
1382
1909
  if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
1383
1910
  return [nestedGroupEntry(root, skillPath, name2)];
1384
1911
  }
@@ -1387,7 +1914,7 @@ function scanGroupMember(root, dir, name2) {
1387
1914
  function scanSkillGroup(root, dir, children) {
1388
1915
  const entries = [];
1389
1916
  for (const child of children) {
1390
- const path = join8(dir, child);
1917
+ const path = join10(dir, child);
1391
1918
  const found = entryKind(path);
1392
1919
  if (found === null) {
1393
1920
  entries.push(unreadableEntry("skill", root, path));
@@ -1408,7 +1935,7 @@ function scanSkillDirectory(root, dir, name2) {
1408
1935
  const children = visibleNames(dir);
1409
1936
  if (children.length === 0)
1410
1937
  return [dotfileOnlyEntry("skill", root, dir)];
1411
- const skillPath = join8(dir, SKILL_FILE);
1938
+ const skillPath = join10(dir, SKILL_FILE);
1412
1939
  if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
1413
1940
  return [skillFileEntry(skillPath, name2)];
1414
1941
  }
@@ -1417,7 +1944,7 @@ function scanSkillDirectory(root, dir, name2) {
1417
1944
  function scanSkillRoot(root) {
1418
1945
  const entries = [];
1419
1946
  for (const name2 of visibleNames(root)) {
1420
- const path = join8(root, name2);
1947
+ const path = join10(root, name2);
1421
1948
  const found = entryKind(path);
1422
1949
  if (found === null) {
1423
1950
  entries.push(unreadableEntry("skill", root, path));
@@ -1438,7 +1965,7 @@ function scanInstinctSubdirectory(root, dir) {
1438
1965
  return [dotfileOnlyEntry("instinct", root, dir)];
1439
1966
  const entries = [];
1440
1967
  for (const child of children) {
1441
- const path = join8(dir, child);
1968
+ const path = join10(dir, child);
1442
1969
  const found = entryKind(path);
1443
1970
  if (found === null) {
1444
1971
  entries.push(unreadableEntry("instinct", root, path));
@@ -1456,7 +1983,7 @@ function scanInstinctSubdirectory(root, dir) {
1456
1983
  function scanInstinctRoot(root) {
1457
1984
  const entries = [];
1458
1985
  for (const name2 of visibleNames(root)) {
1459
- const path = join8(root, name2);
1986
+ const path = join10(root, name2);
1460
1987
  const found = entryKind(path);
1461
1988
  if (found === null) {
1462
1989
  entries.push(unreadableEntry("instinct", root, path));
@@ -1515,7 +2042,7 @@ function scanLine(text, offset) {
1515
2042
  next: index + 1
1516
2043
  };
1517
2044
  }
1518
- function isRecord(value) {
2045
+ function isRecord3(value) {
1519
2046
  return typeof value === "object" && value !== null && !Array.isArray(value);
1520
2047
  }
1521
2048
  function parseBlock(yaml) {
@@ -1526,7 +2053,7 @@ function parseBlock(yaml) {
1526
2053
  } catch {
1527
2054
  return null;
1528
2055
  }
1529
- return isRecord(parsed) ? parsed : null;
2056
+ return isRecord3(parsed) ? parsed : null;
1530
2057
  }
1531
2058
  function readFrontmatterDocument(text) {
1532
2059
  const opening = scanLine(text, 0);
@@ -1583,32 +2110,6 @@ function writeFrontmatter(document, data) {
1583
2110
  ].join("");
1584
2111
  }
1585
2112
 
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
2113
  // src/schema/instinct.ts
1613
2114
  var INSTINCT_KINDS = FINDING_KINDS;
1614
2115
  var INSTINCT_SIGNALS = FINDING_SIGNALS;
@@ -1935,7 +2436,7 @@ function unreadableIssue(message) {
1935
2436
  function readRecord(path, scope, id) {
1936
2437
  let text;
1937
2438
  try {
1938
- text = readFileSync4(path, "utf8");
2439
+ text = readFileSync7(path, "utf8");
1939
2440
  } catch (error) {
1940
2441
  const message = error instanceof Error ? error.message : String(error);
1941
2442
  return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
@@ -1945,13 +2446,13 @@ function readRecord(path, scope, id) {
1945
2446
  }
1946
2447
  function isFile(path) {
1947
2448
  try {
1948
- return statSync2(path).isFile();
2449
+ return statSync3(path).isFile();
1949
2450
  } catch {
1950
2451
  return false;
1951
2452
  }
1952
2453
  }
1953
2454
  function recordFiles(dir) {
1954
- return readdirSync3(dir).filter((name2) => isRecordFile(name2) && isFile(join9(dir, name2))).sort((left, right) => left.localeCompare(right));
2455
+ return readdirSync3(dir).filter((name2) => isRecordFile(name2) && isFile(join11(dir, name2))).sort((left, right) => left.localeCompare(right));
1955
2456
  }
1956
2457
  function readScope(scope, dir) {
1957
2458
  if (!tierExists(dir))
@@ -1960,7 +2461,7 @@ function readScope(scope, dir) {
1960
2461
  scope,
1961
2462
  dir,
1962
2463
  exists: true,
1963
- records: recordFiles(dir).map((name2) => readRecord(join9(dir, name2), scope, recordId(name2)))
2464
+ records: recordFiles(dir).map((name2) => readRecord(join11(dir, name2), scope, recordId(name2)))
1964
2465
  };
1965
2466
  }
1966
2467
  function readScopes(seams) {
@@ -2263,7 +2764,7 @@ function heldChanges(dir, held, result, described) {
2263
2764
  if (issue2 !== undefined) {
2264
2765
  throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue2.field}: ${issue2.message}`);
2265
2766
  }
2266
- const path = join10(dir, file);
2767
+ const path = join12(dir, file);
2267
2768
  if (held.unreadable.has(path)) {
2268
2769
  throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
2269
2770
  }
@@ -2293,18 +2794,18 @@ function pushLogLines(sourceId, described, result) {
2293
2794
  });
2294
2795
  }
2295
2796
  function localInstinctsDir(repoRoot) {
2296
- return join10(repoRoot, RAFA_INSTINCTS_PATH);
2797
+ return join12(repoRoot, RAFA_INSTINCTS_PATH);
2297
2798
  }
2298
2799
  function userInstinctsDir(home) {
2299
- return join10(home, RAFA_INSTINCTS_PATH);
2800
+ return join12(home, RAFA_INSTINCTS_PATH);
2300
2801
  }
2301
2802
  function createLocalLearning(options) {
2302
2803
  const { instinctsDir, minConfidence } = options;
2303
2804
  const userDir = userInstinctsDir(options.home);
2304
2805
  const now = options.now ?? (() => new Date().toISOString());
2305
2806
  const warn = options.warn ?? warnThroughActiveOutput;
2306
- const instinctsPath = join10(instinctsDir, INSTINCTS_FILE);
2307
- const flagsPath = join10(instinctsDir, FLAGS_FILE);
2807
+ const instinctsPath = join12(instinctsDir, INSTINCTS_FILE);
2808
+ const flagsPath = join12(instinctsDir, FLAGS_FILE);
2308
2809
  function readHeld(scope, dir) {
2309
2810
  const { records } = readScope(scope, dir);
2310
2811
  const unreadable = new Set;
@@ -2919,7 +3420,7 @@ function notOnPath(command, cwd, path) {
2919
3420
  function isOneOf(members, value) {
2920
3421
  return typeof value === "string" && members.includes(value);
2921
3422
  }
2922
- function isIssueNumber(value) {
3423
+ function isIssueNumber3(value) {
2923
3424
  return typeof value === "string" && ISSUE_NUMBER.test(value) && Number.isSafeInteger(Number(value));
2924
3425
  }
2925
3426
  function isRepoName(value) {
@@ -2974,7 +3475,7 @@ function issueNumberOfRef(ref) {
2974
3475
  if (ref.kind !== "github") {
2975
3476
  throw new Error(`${PREFIX2}: refused a ref of kind ${describeValue(ref.kind)}; this tracker reads github refs only`);
2976
3477
  }
2977
- if (!isIssueNumber(ref.externalId)) {
3478
+ if (!isIssueNumber3(ref.externalId)) {
2978
3479
  throw new Error(`${PREFIX2}: externalId ${describeValue(ref.externalId)} is not a GitHub issue number,` + " expected a positive whole number with no leading zero");
2979
3480
  }
2980
3481
  if (ref.repo !== undefined && !isRepoName(ref.repo)) {
@@ -3036,7 +3537,7 @@ function listedIssues(stdout) {
3036
3537
  throw refuse2(`${describeValue(row)}, expected a mapping`);
3037
3538
  const { number, url } = row;
3038
3539
  const labels = labelNames(row["labels"]);
3039
- if (typeof number !== "number" || !isIssueNumber(String(number))) {
3540
+ if (typeof number !== "number" || !isIssueNumber3(String(number))) {
3040
3541
  throw refuse2(`with number ${describeValue(number)}, expected an issue number`);
3041
3542
  }
3042
3543
  if (typeof url !== "string")
@@ -3071,7 +3572,7 @@ function issueOf(ref, viewed) {
3071
3572
  module: moduleOfLabels(viewed.labels),
3072
3573
  priority: isOneOf(ISSUE_PRIORITIES, priority) ? priority : null,
3073
3574
  project: null,
3074
- blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(isIssueNumber).map(Number),
3575
+ blockedBy: viewed.labels.map((label) => label.startsWith(GITHUB_LABELS.blockedByPrefix) ? label.slice(GITHUB_LABELS.blockedByPrefix.length) : "").filter(isIssueNumber3).map(Number),
3075
3576
  ref: { ...ref, url: viewed.url },
3076
3577
  state: stateOf(viewed)
3077
3578
  };
@@ -3209,7 +3710,7 @@ function createGithubTracker(options) {
3209
3710
 
3210
3711
  // src/adapters/tracker/local.ts
3211
3712
  import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3212
- import { join as join11 } from "path";
3713
+ import { join as join13 } from "path";
3213
3714
  var PREFIX3 = "local tracker";
3214
3715
  var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
3215
3716
  var ISSUE_NUMBER2 = /^[1-9]\d*$/;
@@ -3266,7 +3767,7 @@ function issueNumberOfFile(name2) {
3266
3767
  return name2.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name2.slice(0, -ISSUE_EXTENSION.length)) : null;
3267
3768
  }
3268
3769
  function issuePath(issuesDir, number) {
3269
- return join11(issuesDir, `${number}${ISSUE_EXTENSION}`);
3770
+ return join13(issuesDir, `${number}${ISSUE_EXTENSION}`);
3270
3771
  }
3271
3772
  async function issueNumbers(issuesDir) {
3272
3773
  let names;
@@ -3280,7 +3781,7 @@ async function issueNumbers(issuesDir) {
3280
3781
  return names.map(issueNumberOfFile).filter((number) => number !== null).sort((a, b) => a - b);
3281
3782
  }
3282
3783
  function localIssuesDir(repoRoot) {
3283
- return join11(repoRoot, ".rafa", "issues");
3784
+ return join13(repoRoot, ".rafa", "issues");
3284
3785
  }
3285
3786
  function renderLocalIssue(record) {
3286
3787
  const { draft } = record;
@@ -3617,4 +4118,4 @@ function selectEffortStore(repoRoot, config) {
3617
4118
  return adapter.create({ repoRoot });
3618
4119
  }
3619
4120
 
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 };
4121
+ export { EFFORT_STORE_DIR, effortStoreFileName, EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, version, RAFA_VERSION, versionLine, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, POSITION_FILE, positionFilePath, readPositionFile, writePositionFile, positionAt, rehome, hop, goHome, HOP_FILE, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, SessionConflictError, SessionStateError, isSessionId, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, beginSession, updateSession, readRuntimeIdentity, DEVELOPMENT_NEXT_STEP, ADOPTION_NAME, DEVELOPMENT_MARK, appliedByName, DevelopmentBuildRefusedError, storeProjectRoot, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, MIGRATION_LOG_TABLE, SchemaRefusedError, bringForward, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, setActiveStoreSettings, activeStoreSettings, createGitRunner, gitSaid, normalizeRemote, gitRemoteUrl, readProjectIdentity, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, sessionSpawnEnv, getClaudeUsagePercent, checkUsage, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, selectEffortStore };