@open-tomato/rafa 0.41.0 → 0.43.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.
@@ -1,49 +1,42 @@
1
1
  // @bun
2
2
  import {
3
+ BUSY_TIMEOUT_MAX_MS,
4
+ BUSY_TIMEOUT_MIN_MS,
3
5
  CONFIG_DEFAULTS,
4
6
  CONFIG_FILE,
5
- ConfigError,
6
7
  FINDING_DOMAINS,
7
8
  FINDING_KINDS,
8
9
  FINDING_SIGNALS,
9
- activeOutput,
10
- activeOutputMode,
11
- configFilePath,
12
- createTextOutput,
13
- hubContextOf,
14
- isStampableStub,
15
- parseConfigText,
16
- readRafaBlocks,
17
- resolveConfig
18
- } from "./index-3jpz9nr3.js";
19
- import {
20
- bless,
21
- merge
22
- } from "./index-d6d0942q.js";
23
- import {
24
- CommandExit,
25
- commandSpelling
26
- } from "./index-sr3kc4mf.js";
27
- import {
28
- BUSY_TIMEOUT_MAX_MS,
29
- BUSY_TIMEOUT_MIN_MS,
30
10
  RAFA_INSTINCTS_PATH,
31
11
  STORE_BACKENDS,
32
12
  SYNC_STRATEGIES,
33
13
  TEST_SCOPES,
14
+ activeLoggerSettings,
15
+ activeOutput,
16
+ activeOutputMode,
34
17
  bundledBinDirectory,
18
+ configFilePath,
19
+ createTextOutput,
35
20
  describeValue,
21
+ hubContextOf,
36
22
  isBusyTimeoutMs,
37
23
  isGitHubLogin,
38
24
  isMapping,
25
+ isStampableStub,
26
+ levelEnabled,
39
27
  messageOf,
28
+ readRafaBlocks,
40
29
  resolveInstinctScopes,
41
30
  tierExists
42
- } from "./index-m2ahxjw8.js";
31
+ } from "./index-cknayrpc.js";
32
+ import {
33
+ bless,
34
+ merge
35
+ } from "./index-nv34ptht.js";
43
36
  import {
44
37
  actionHash,
45
38
  triggerKey
46
- } from "./index-wp8c28fd.js";
39
+ } from "./index-hf2k497d.js";
47
40
 
48
41
  // src/adapters/registry.ts
49
42
  import { homedir } from "os";
@@ -259,13 +252,10 @@ import { Database as Database2 } from "bun:sqlite";
259
252
 
260
253
  // package.json
261
254
  var name = "@open-tomato/rafa";
262
- var version = "0.41.0";
255
+ var version = "0.43.0";
263
256
 
264
257
  // src/cli/version.ts
265
258
  var RAFA_VERSION = version;
266
- function versionLine() {
267
- return `rafa ${RAFA_VERSION}`;
268
- }
269
259
 
270
260
  // src/effort/store/development-build.ts
271
261
  import { tmpdir as tmpdir2 } from "os";
@@ -535,12 +525,6 @@ function writePositionFile(root, position) {
535
525
  function positionAt(place) {
536
526
  return { current: place, previous: null, home: place };
537
527
  }
538
- function rehome(position, place) {
539
- return { current: place, previous: position.current, home: place };
540
- }
541
- function hop(position, place) {
542
- return { current: place, previous: position.current, home: position.home };
543
- }
544
528
  function goHome(position) {
545
529
  return { current: position.home, previous: position.current, home: position.home };
546
530
  }
@@ -652,7 +636,6 @@ function staleAgainst(record, position) {
652
636
  // src/loop/session-record-parse.ts
653
637
  var SESSION_STATES = Object.freeze(["running", "paused", "stopped", "done"]);
654
638
  var SESSION_PHASES = Object.freeze(["task", "wrap-up", "pull-request", "ci", "repair"]);
655
- var DEFAULT_PHASE = "task";
656
639
  var RECORD_EXTENSION = ".json";
657
640
  var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
658
641
  var SESSION_STEP_KINDS = Object.freeze(["baseline", "task", "stage", "pre-wrap-up"]);
@@ -698,8 +681,8 @@ function taskProblem(task) {
698
681
  function hopProblem(fields) {
699
682
  if (!Object.hasOwn(fields, "hop"))
700
683
  return null;
701
- const hop2 = field(fields, "hop");
702
- return asHopRecord(hop2) === null ? `hop is ${describeValue2(hop2)}, expected a hop record` : null;
684
+ const hop = field(fields, "hop");
685
+ return asHopRecord(hop) === null ? `hop is ${describeValue2(hop)}, expected a hop record` : null;
703
686
  }
704
687
  function worktreeProblem(fields) {
705
688
  if (!Object.hasOwn(fields, "worktree"))
@@ -845,11 +828,40 @@ function phaseEntry(phase) {
845
828
  return isSessionPhase(phase) ? { phase } : {};
846
829
  }
847
830
  function freezeHop(value) {
848
- const hop2 = asHopRecord(value);
849
- return Object.freeze({ ...hop2, home: Object.freeze(hop2.home), from: Object.freeze(hop2.from) });
831
+ const hop = asHopRecord(value);
832
+ return Object.freeze({ ...hop, home: Object.freeze(hop.home), from: Object.freeze(hop.from) });
833
+ }
834
+ function errorLinesEntry(errorLines) {
835
+ const kept = Array.isArray(errorLines) && errorLines.length > 0 && errorLines.every((line) => typeof line === "string");
836
+ return kept ? { errorLines: Object.freeze([...errorLines]) } : {};
850
837
  }
851
838
  function freezeFailures(failures) {
852
- return Object.freeze(failures.map((failure) => Object.freeze({ file: failure.file, name: failure.name })));
839
+ return Object.freeze(failures.map((failure) => Object.freeze({
840
+ file: failure.file,
841
+ name: failure.name,
842
+ ...errorLinesEntry(field(failure, "errorLines"))
843
+ })));
844
+ }
845
+ function isStringList(value) {
846
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
847
+ }
848
+ function isStepOnly(value) {
849
+ if (!isObject(value))
850
+ return false;
851
+ const position = field(value, "position");
852
+ return isText(field(value, "file")) && isTextList(field(value, "tests")) && isStringList(field(value, "errorLines")) && (position === null || isPositiveWhole(position)) && isTextList(field(value, "before"));
853
+ }
854
+ function stepOnlyEntry(stepOnly) {
855
+ const whole = Array.isArray(stepOnly) ? stepOnly.filter(isStepOnly) : [];
856
+ if (whole.length === 0)
857
+ return {};
858
+ return { stepOnly: Object.freeze(whole.map((entry) => Object.freeze({
859
+ file: entry.file,
860
+ tests: Object.freeze([...entry.tests]),
861
+ errorLines: Object.freeze([...entry.errorLines]),
862
+ position: entry.position,
863
+ before: Object.freeze([...entry.before])
864
+ }))) };
853
865
  }
854
866
  function isSessionStepReason(value) {
855
867
  return SESSION_STEP_REASONS.includes(value);
@@ -867,6 +879,7 @@ function freezeStep(step) {
867
879
  summary: step.summary,
868
880
  failures: freezeFailures(step.failures),
869
881
  newFailures: freezeFailures(step.newFailures),
882
+ ...stepOnlyEntry(field(step, "stepOnly")),
870
883
  ...step.interrupted === true ? { interrupted: true } : {}
871
884
  });
872
885
  }
@@ -897,7 +910,7 @@ function decisionsEntry(decisions) {
897
910
  }
898
911
  function freezeRecord(fields) {
899
912
  const task = field(fields, "task");
900
- const hop2 = field(fields, "hop");
913
+ const hop = field(fields, "hop");
901
914
  const worktree = field(fields, "worktree");
902
915
  return Object.freeze({
903
916
  sessionId: field(fields, "sessionId"),
@@ -909,7 +922,7 @@ function freezeRecord(fields) {
909
922
  state: field(fields, "state"),
910
923
  task: isObject(task) ? Object.freeze({ line: field(task, "line"), text: field(task, "text") }) : null,
911
924
  ...phaseEntry(field(fields, "phase")),
912
- ...hop2 === undefined ? {} : { hop: freezeHop(hop2) },
925
+ ...hop === undefined ? {} : { hop: freezeHop(hop) },
913
926
  ...worktree === undefined ? {} : { worktree },
914
927
  ...stepsEntry(field(fields, "steps")),
915
928
  ...decisionsEntry(field(fields, "decisions"))
@@ -921,9 +934,6 @@ function sessionSteps(record) {
921
934
  function sessionDecisions(record) {
922
935
  return record.decisions ?? [];
923
936
  }
924
- function sessionPhase(record) {
925
- return record.phase ?? DEFAULT_PHASE;
926
- }
927
937
  function parseSessionRecord(text, file) {
928
938
  let parsed;
929
939
  try {
@@ -1561,6 +1571,16 @@ var SQLITE_MIGRATIONS = [
1561
1571
  sql: `
1562
1572
  ALTER TABLE sessions ADD COLUMN worktree TEXT;
1563
1573
  `
1574
+ },
1575
+ {
1576
+ id: "store-replaced",
1577
+ breaks: [],
1578
+ sql: `
1579
+ CREATE TABLE store_replaced (
1580
+ store_id TEXT NOT NULL PRIMARY KEY CHECK (store_id <> ''),
1581
+ replaced_at TEXT NOT NULL CHECK (replaced_at <> '')
1582
+ );
1583
+ `
1564
1584
  }
1565
1585
  ];
1566
1586
  var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
@@ -1959,8 +1979,33 @@ function readProjectIdentity(dir, git = createGitRunner(dir)) {
1959
1979
  return { rootCommit: smallestRootCommit(git), remote: normalisedRemote(git) };
1960
1980
  }
1961
1981
 
1982
+ // src/effort/store/store-replaced.ts
1983
+ var STORE_REPLACED_TABLE = "store_replaced";
1984
+ function hasStoreReplacedTable(db) {
1985
+ return db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = ?").get(STORE_REPLACED_TABLE)?.n === 1;
1986
+ }
1987
+ function readReplacedStoreIds(db) {
1988
+ if (!hasStoreReplacedTable(db))
1989
+ return [];
1990
+ return db.query("SELECT store_id, replaced_at FROM store_replaced ORDER BY replaced_at, store_id").all().map((row) => ({ storeId: row.store_id, replacedAt: row.replaced_at }));
1991
+ }
1992
+ function recordReplacedStoreId(db, storeId, replacedAt) {
1993
+ if (storeId === "")
1994
+ throw new Error("replaced store id must be non-empty");
1995
+ if (Number.isNaN(replacedAt.getTime())) {
1996
+ throw new Error(`replaced time of store ${storeId} is not a valid date`);
1997
+ }
1998
+ db.query("INSERT INTO store_replaced (store_id, replaced_at) VALUES (?, ?) ON CONFLICT (store_id) DO NOTHING").run(storeId, replacedAt.toISOString());
1999
+ }
2000
+ function clearReplacedStoreIds(db) {
2001
+ if (!hasStoreReplacedTable(db))
2002
+ return;
2003
+ db.run("DELETE FROM store_replaced");
2004
+ }
2005
+
1962
2006
  // src/effort/store/store-meta.ts
1963
2007
  var STORED_INTEGER_BITS = 64;
2008
+ var COPY_REASONS = ["host", "path"];
1964
2009
  function toStoredInteger(value) {
1965
2010
  return BigInt.asIntN(STORED_INTEGER_BITS, value);
1966
2011
  }
@@ -1995,12 +2040,20 @@ function projectFor(storePath, recorded, seams) {
1995
2040
  return null;
1996
2041
  return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
1997
2042
  }
1998
- function writeStoreMeta(db, facts, project, generation, seams) {
2043
+ function writeStoreMeta(db, facts, project, generation, mintedAt, seams) {
1999
2044
  const storeId = (seams.newStoreId ?? randomUUID4)();
2000
- const mintedAt = (seams.now?.() ?? new Date).toISOString();
2001
- db.query("INSERT OR REPLACE INTO store_meta (id, store_id, project_root_commit, project_remote," + " host_id, store_path, file_dev, file_ino, minted_at, generation) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(storeId, project.rootCommit, project.remote, facts.hostId, facts.storePath, toStoredInteger(facts.fileDev), toStoredInteger(facts.fileIno), mintedAt, generation);
2045
+ db.query("INSERT OR REPLACE INTO store_meta (id, store_id, project_root_commit, project_remote," + " host_id, store_path, file_dev, file_ino, minted_at, generation) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(storeId, project.rootCommit, project.remote, facts.hostId, facts.storePath, toStoredInteger(facts.fileDev), toStoredInteger(facts.fileIno), mintedAt.toISOString(), generation);
2002
2046
  return storeId;
2003
2047
  }
2048
+ function settleReplacedIds(db, outgoing, reasons, mintedAt) {
2049
+ if (reasons.some((reason) => COPY_REASONS.includes(reason))) {
2050
+ clearReplacedStoreIds(db);
2051
+ return;
2052
+ }
2053
+ if (outgoing === null)
2054
+ return;
2055
+ recordReplacedStoreId(db, outgoing.storeId, mintedAt);
2056
+ }
2004
2057
  function settleUnderLock(db, observe, prefetched, seams) {
2005
2058
  const current = readStoreMeta(db);
2006
2059
  const facts = observe();
@@ -2016,7 +2069,9 @@ function settleUnderLock(db, observe, prefetched, seams) {
2016
2069
  const project = prefetched ?? projectFor(facts.storePath, current, seams);
2017
2070
  if (project === null)
2018
2071
  return { action: "no-project", reasons: decision.reasons };
2019
- const storeId = writeStoreMeta(db, facts, project, generation, seams);
2072
+ const mintedAt = seams.now?.() ?? new Date;
2073
+ settleReplacedIds(db, current, decision.reasons, mintedAt);
2074
+ const storeId = writeStoreMeta(db, facts, project, generation, mintedAt, seams);
2020
2075
  writeStoreGeneration(facts.storePath, generation);
2021
2076
  return { action: "mint", storeId, reasons: decision.reasons };
2022
2077
  }
@@ -2212,15 +2267,13 @@ function openSqliteStore(repoRoot) {
2212
2267
  }
2213
2268
 
2214
2269
  // src/effort/sync/file.ts
2215
- import { existsSync as existsSync12 } from "fs";
2216
- import { join as join12, resolve as resolve5 } from "path";
2217
-
2218
- // src/effort/store/fix-schema.ts
2219
- import { existsSync as existsSync8 } from "fs";
2220
- import { Database as Database4 } from "bun:sqlite";
2270
+ import { existsSync as existsSync9 } from "fs";
2271
+ import { join as join12, resolve as resolve4 } from "path";
2221
2272
 
2222
- // src/effort/store/rebuild-aside.ts
2223
- import { closeSync, existsSync as existsSync7, fsyncSync, openSync, renameSync as renameSync5, rmSync as rmSync6 } from "fs";
2273
+ // src/effort/file-stamp.ts
2274
+ function fileStamp(date) {
2275
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
2276
+ }
2224
2277
 
2225
2278
  // src/effort/store/copy.ts
2226
2279
  import { copyFileSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync2, rmSync as rmSync5, statSync as statSync2 } from "fs";
@@ -2311,7 +2364,18 @@ function copyEffortStore(request) {
2311
2364
  return Object.freeze({ source, directory: target, files: Object.freeze(files) });
2312
2365
  }
2313
2366
 
2367
+ // src/effort/store/merge-store.ts
2368
+ import { randomUUID as randomUUID5 } from "crypto";
2369
+ import { existsSync as existsSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
2370
+ import { tmpdir as tmpdir3 } from "os";
2371
+ import { dirname as dirname11, join as join11 } from "path";
2372
+ import { Database as Database5, SQLiteError as SQLiteError2 } from "bun:sqlite";
2373
+
2374
+ // src/effort/store/fix-schema.ts
2375
+ import { Database as Database4 } from "bun:sqlite";
2376
+
2314
2377
  // src/effort/store/rebuild-aside.ts
2378
+ import { closeSync, existsSync as existsSync7, fsyncSync, openSync, renameSync as renameSync5, rmSync as rmSync6 } from "fs";
2315
2379
  class RebuildRefusal extends Error {
2316
2380
  constructor(message, options) {
2317
2381
  super(message, options);
@@ -2442,22 +2506,10 @@ function rebuildAside(options) {
2442
2506
  swapIn(path, parallelPath, backupPath);
2443
2507
  return { built, backupPath };
2444
2508
  }
2445
-
2446
2509
  // src/effort/store/fix-schema.ts
2447
- var REPAIRABLE_REASONS = [
2448
- "pre-log-unreleased",
2449
- "gate-mismatch",
2450
- "edited",
2451
- "unknown-breaks-readers",
2452
- "unknown-breaks-writers"
2453
- ];
2454
- var LIVE = "live";
2455
2510
  function tableNames(db, schema) {
2456
2511
  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);
2457
2512
  }
2458
- function columnNames(db, schema, table) {
2459
- return db.query("SELECT name FROM pragma_table_info(?, ?)").all(table, schema).map((row) => row.name);
2460
- }
2461
2513
  function readOnlySchema(path) {
2462
2514
  const db = new Database4(path, { readonly: true });
2463
2515
  try {
@@ -2466,38 +2518,6 @@ function readOnlySchema(path) {
2466
2518
  db.close();
2467
2519
  }
2468
2520
  }
2469
- function refuseRemoved(db, known, live) {
2470
- for (const table of known) {
2471
- if (!live.has(table)) {
2472
- throw new RebuildRefusal(`the store holds no table ${table}, which this rafa writes; its newer schema is not additive`);
2473
- }
2474
- const liveColumns = new Set(columnNames(db, LIVE, table));
2475
- const missing = columnNames(db, "main", table).filter((column) => !liveColumns.has(column));
2476
- if (missing.length > 0) {
2477
- throw new RebuildRefusal(`the store holds no column ${missing.map((column) => `${table}.${column}`).join(", ")}, which this rafa` + " writes; its newer schema is not additive");
2478
- }
2479
- }
2480
- }
2481
- function copyKnown(db, known) {
2482
- db.transaction(() => {
2483
- for (const table of known) {
2484
- const columns = columnNames(db, "main", table).map(quoted).join(", ");
2485
- db.run(`INSERT INTO main.${quoted(table)} (${columns}) SELECT ${columns} FROM ${LIVE}.${quoted(table)}`);
2486
- }
2487
- })();
2488
- }
2489
- function leftBehind(db, known, live) {
2490
- const leftTables = live.filter((table) => !known.has(table)).map((table) => ({ table, rows: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)}`) }));
2491
- const leftColumns = live.filter((table) => known.has(table)).flatMap((table) => {
2492
- const kept = new Set(columnNames(db, "main", table));
2493
- return columnNames(db, LIVE, table).filter((column) => !kept.has(column)).map((column) => ({
2494
- table,
2495
- column,
2496
- values: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)} WHERE ${quoted(column)} IS NOT NULL`)
2497
- }));
2498
- });
2499
- return { leftTables, leftColumns };
2500
- }
2501
2521
  function refuseUnusable(db, parallelPath, migrations) {
2502
2522
  const plan = planSchema(readStoreSchema(db, parallelPath), sqliteCatalogue(migrations), "write", "open");
2503
2523
  if (plan.verdict === "refuse")
@@ -2506,363 +2526,6 @@ function refuseUnusable(db, parallelPath, migrations) {
2506
2526
  if (needs.length > 0)
2507
2527
  throw new RebuildRefusal(`the rebuilt store still needs ${needs.join(", ")}`);
2508
2528
  }
2509
- function buildParallel(path, parallelPath, migrations, stamp) {
2510
- const db = new Database4(parallelPath, { create: true, readwrite: true });
2511
- try {
2512
- bringForward(db, parallelPath, "write", "open", { migrations, ...stamp, builtAside: true });
2513
- db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
2514
- const known = tableNames(db, "main");
2515
- const live = tableNames(db, LIVE);
2516
- refuseRemoved(db, known, new Set(live));
2517
- copyKnown(db, known);
2518
- const kept = checkedCounts(db, known, LIVE);
2519
- const left = leftBehind(db, new Set(known), live);
2520
- db.run(`DETACH DATABASE ${LIVE}`);
2521
- refuseCorrupt(db);
2522
- refuseUnusable(db, parallelPath, migrations);
2523
- return { kept, ...left };
2524
- } finally {
2525
- db.close();
2526
- }
2527
- }
2528
- function asidePaths(path, version2, stamp) {
2529
- return { parallelPath: `${path}.fix-${stamp}`, backupPath: `${path}.v${String(version2)}-${stamp}.bak` };
2530
- }
2531
- function isRepairable(reason) {
2532
- return REPAIRABLE_REASONS.includes(reason);
2533
- }
2534
- function refuseDevelopmentSwap(path, reason, identity) {
2535
- if (identity.kind !== "development")
2536
- return;
2537
- 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");
2538
- }
2539
- function fixStoreSchema(options) {
2540
- const { path, dryRun, stamp } = options;
2541
- guardTestProcess(path);
2542
- const migrations = options.migrations ?? SQLITE_MIGRATIONS;
2543
- const identity = options.identity ?? readRuntimeIdentity();
2544
- const appliedBy = appliedByName(identity);
2545
- const known = migrations.map(({ id }) => id);
2546
- const nothing = { known, appliedBy, pending: [], kept: [], leftTables: [], leftColumns: [], backupPath: null };
2547
- if (!existsSync8(path)) {
2548
- return { path, status: "missing", storeVersion: null, logged: false, reason: null, refusal: null, unknown: [], ...nothing };
2549
- }
2550
- refuseInFlight(path);
2551
- const store = readOnlySchema(path);
2552
- const knownIds = new Set(known);
2553
- const found = {
2554
- path,
2555
- storeVersion: store.userVersion,
2556
- logged: store.log !== null,
2557
- unknown: (store.log ?? []).filter(({ id }) => !knownIds.has(id))
2558
- };
2559
- const plan = planSchema(store, sqliteCatalogue(migrations), "write", "open");
2560
- if (plan.verdict === "use") {
2561
- const pending = writeNames(plan);
2562
- const status2 = pending.length === 0 ? "current" : "behind";
2563
- return { ...found, ...nothing, status: status2, reason: null, refusal: null, pending };
2564
- }
2565
- if (!isRepairable(plan.reason)) {
2566
- throw new RebuildRefusal(`a rebuild at this rafa's migrations does not repair ${plan.reason}. ${plan.message}`);
2567
- }
2568
- if (!dryRun)
2569
- refuseDevelopmentSwap(path, plan.reason, identity);
2570
- const { built, backupPath } = rebuildAside({
2571
- path,
2572
- ...asidePaths(path, store.userVersion, stamp),
2573
- dryRun,
2574
- build: (parallelPath) => buildParallel(path, parallelPath, migrations, {
2575
- appliedBy,
2576
- now: options.now ?? (() => new Date)
2577
- })
2578
- });
2579
- const status = dryRun ? "would-rebuild" : "rebuilt";
2580
- const refusal = withoutWayOut(plan.message, plan.nextStep);
2581
- return { ...found, ...nothing, status, reason: plan.reason, refusal, ...built, backupPath };
2582
- }
2583
- function withoutWayOut(message, nextStep) {
2584
- const ending = ` Next safe step: ${nextStep}`;
2585
- return message.endsWith(ending) ? message.slice(0, -ending.length) : message;
2586
- }
2587
-
2588
- // src/commands/plan/plan-files.ts
2589
- import { statSync as statSync3 } from "fs";
2590
- import { resolve as resolve4 } from "path";
2591
-
2592
- // src/config-load.ts
2593
- import { existsSync as existsSync9, readFileSync as readFileSync8, realpathSync as realpathSync5 } from "fs";
2594
- import { isAbsolute as isAbsolute6 } from "path";
2595
- function readConfigFile(root) {
2596
- const path = configFilePath(root);
2597
- if (!existsSync9(path))
2598
- return null;
2599
- let text;
2600
- try {
2601
- text = readFileSync8(path, "utf8");
2602
- } catch (error) {
2603
- const problem = `${path}: cannot be read (${messageOf(error)})`;
2604
- throw new ConfigError([problem], { cause: error });
2605
- }
2606
- return parseConfigText(text, path);
2607
- }
2608
- function isSameDirectory(first, second) {
2609
- try {
2610
- return realpathSync5(first) === realpathSync5(second);
2611
- } catch {
2612
- return false;
2613
- }
2614
- }
2615
- function readUserConfigFile({ root, home }) {
2616
- if (!isAbsolute6(home)) {
2617
- throw new ConfigError([
2618
- `home directory is ${describeValue(home)}, expected an absolute path`
2619
- ]);
2620
- }
2621
- return isSameDirectory(root, home) ? null : readConfigFile(home);
2622
- }
2623
- function printWarning(message) {
2624
- activeOutput().warn(message);
2625
- }
2626
- function loadConfig(roots, cli = {}, warn = printWarning) {
2627
- const user = readUserConfigFile(roots);
2628
- const file = readConfigFile(roots.root);
2629
- const resolved = resolveConfig({ cli, file, user });
2630
- for (const warning of resolved.warnings)
2631
- warn(warning);
2632
- setActiveStoreSettings({ busyTimeoutMs: resolved.config.effortBusyTimeoutMs });
2633
- return resolved;
2634
- }
2635
-
2636
- // src/commands/plan/plan-files.ts
2637
- function plansDirAt(root, planDir) {
2638
- return { label: planDir, path: resolve4(root, planDir) };
2639
- }
2640
- function requireProject(context, command) {
2641
- if (context.project === null)
2642
- throw new Error(`${command} runs inside a project, and was handed none`);
2643
- return context.project;
2644
- }
2645
- function resolvePlansDir(project, command, warn) {
2646
- return plansDirAt(project.root, resolveProjectConfig(project, command, warn).planDir);
2647
- }
2648
- function resolveProjectConfig(project, command, warn) {
2649
- try {
2650
- return loadConfig({ root: project.root, home: project.home }, {}, warn).config;
2651
- } catch (error) {
2652
- if (!(error instanceof ConfigError))
2653
- throw error;
2654
- throw new CommandExit(1, [
2655
- `\u274C ${command}: the config cannot be used:`,
2656
- ...error.problems.map((problem) => ` ${problem}`)
2657
- ].join(`
2658
- `));
2659
- }
2660
- }
2661
- var CHECKBOXES = {
2662
- unchecked: "[ ]",
2663
- blocked: "[BLOCKED]",
2664
- done: "[x]"
2665
- };
2666
- function countTasks(tasks) {
2667
- const done = tasks.filter((task) => task.status === "done").length;
2668
- const blocked = tasks.filter((task) => task.status === "blocked").length;
2669
- return { total: tasks.length, done, blocked, open: tasks.length - done - blocked };
2670
- }
2671
- function formatCounts(counts) {
2672
- return `${counts.done}/${counts.total} done, ${counts.blocked} blocked, ${counts.open} open`;
2673
- }
2674
- function checkbox(status) {
2675
- return CHECKBOXES[status];
2676
- }
2677
- function planFileName(stub, tracker) {
2678
- return tracker ? `PLAN_TRACKER-${stub}.md` : `PLAN-${stub}.md`;
2679
- }
2680
- function stubOfPlanFile(name2) {
2681
- const stub = /^PLAN-(.+)\.md$/.exec(name2)?.[1];
2682
- return stub !== undefined && isStampableStub(stub) ? stub : null;
2683
- }
2684
- function isFile(path) {
2685
- return statSync3(path, { throwIfNoEntry: false })?.isFile() ?? false;
2686
- }
2687
- function issueLine(file, issue) {
2688
- return `${file}:${issue.line}: ${issue.reason}: ${issue.text}`;
2689
- }
2690
- function plural(count2, noun) {
2691
- return count2 === 1 ? `${count2} ${noun}` : `${count2} ${noun}s`;
2692
- }
2693
- function expectNoArgument(args, usage) {
2694
- if (args.length === 0)
2695
- return;
2696
- throw new CommandExit(1, `\u274C Expected no argument, got ${args.length}: ${args.join(" ")}
2697
- Usage: ${usage}`);
2698
- }
2699
- function expectOneArgument(args, usage) {
2700
- const [only] = args;
2701
- if (args.length === 1 && only !== undefined)
2702
- return only;
2703
- const got = args.length === 0 ? "none" : `${args.length}: ${args.join(" ")}`;
2704
- throw new CommandExit(1, `\u274C Expected one argument, got ${got}
2705
- Usage: ${usage}`);
2706
- }
2707
- function expectAtMostOneArgument(args, usage) {
2708
- const [only] = args;
2709
- if (only === undefined)
2710
- return null;
2711
- if (args.length === 1)
2712
- return only;
2713
- throw new CommandExit(1, `\u274C Expected at most one argument, got ${args.length}: ${args.join(" ")}
2714
- Usage: ${usage}`);
2715
- }
2716
- function readSwitch(name2, value, hint) {
2717
- if (value === undefined || value === false || value === "false")
2718
- return false;
2719
- if (value === true || value === "true")
2720
- return true;
2721
- throw new CommandExit(1, `\u274C --${name2} takes no value, and read "${value}" as one. ${hint}`);
2722
- }
2723
-
2724
- // src/commands/effort/fix-schema.ts
2725
- var COMMAND_NAME = "rafa effort fix-schema";
2726
- var FIX_SCHEMA_USAGE = "rafa effort fix-schema [--dry-run]";
2727
- var DRY_RUN_FLAG = "dry-run";
2728
- function fileStamp(date) {
2729
- return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
2730
- }
2731
- function keepsIdLine(swapped) {
2732
- return `The ${swapped} keeps the store's id; the backup is a copy, so renamed back it takes a new id on its` + " next write.";
2733
- }
2734
- function refuseLiveLoop(root, isAlive) {
2735
- let records;
2736
- try {
2737
- records = readSessions(root, isAlive === undefined ? {} : { isAlive });
2738
- } catch (error) {
2739
- 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)}`);
2740
- }
2741
- const live = records.find((record) => record.state === "running" || record.state === "paused");
2742
- if (live === undefined)
2743
- return;
2744
- 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.`);
2745
- }
2746
- function migrationNames(result) {
2747
- return result.unknown.map(({ id, appliedBy }) => `${id} (applied by ${appliedBy})`).join(", ");
2748
- }
2749
- function rebuildLines(result) {
2750
- const rows = result.kept.reduce((sum, table) => sum + table.rows, 0);
2751
- const kept = result.kept.map((table) => `${table.table} ${String(table.rows)}`).join(", ");
2752
- const left = [
2753
- ...result.unknown.map(({ id, appliedBy }) => `migration ${id} (applied by ${appliedBy})`),
2754
- ...result.leftTables.map((table) => `table ${table.table} (${String(table.rows)} rows)`),
2755
- ...result.leftColumns.map((column) => `column ${column.table}.${column.column} (${String(column.values)} values)`)
2756
- ];
2757
- 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."];
2758
- return [
2759
- `Refused as ${String(result.reason)}: ${String(result.refusal)}`,
2760
- `Rebuilds it at the ${String(result.known.length)} migrations this rafa knows, logged as applied by ${result.appliedBy}.`,
2761
- `Keeps ${String(result.kept.length)} tables, ${String(rows)} rows: ${kept}.`,
2762
- left.length === 0 ? "Leaves nothing behind." : `Leaves behind, kept only in the backup: ${left.join("; ")}.`,
2763
- ...newer
2764
- ];
2765
- }
2766
- function unknownAdditiveLines(result) {
2767
- 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."];
2768
- }
2769
- function renderFixSchema(result) {
2770
- switch (result.status) {
2771
- case "missing":
2772
- return [`No effort store at ${result.path}: nothing to repair.`];
2773
- case "current":
2774
- return [
2775
- `\u2705 ${result.path} is current: this rafa reads and writes it as it is. Nothing to repair.`,
2776
- ...unknownAdditiveLines(result)
2777
- ];
2778
- case "behind":
2779
- return [
2780
- `${result.path} is behind this rafa's migrations; the next command that opens it records` + ` ${result.pending.join(", ")}. Nothing to repair.`,
2781
- ...unknownAdditiveLines(result)
2782
- ];
2783
- case "would-rebuild":
2784
- return [
2785
- ...rebuildLines(result),
2786
- "\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."
2787
- ];
2788
- case "rebuilt":
2789
- return [
2790
- ...rebuildLines(result),
2791
- `\u2705 Rebuilt at this rafa's migrations. Every row the original held is copied to ${String(result.backupPath)}.` + ` ${keepsIdLine("rebuild")}`
2792
- ];
2793
- }
2794
- }
2795
- function runFixSchema(context, seams) {
2796
- expectNoArgument(context.args, FIX_SCHEMA_USAGE);
2797
- const dryRun = readSwitch(DRY_RUN_FLAG, context.flags[DRY_RUN_FLAG], `Usage: ${FIX_SCHEMA_USAGE}`);
2798
- if (context.project === null)
2799
- throw new Error(`${COMMAND_NAME} runs inside a project, and was handed none`);
2800
- const root = context.project.root;
2801
- if (!dryRun)
2802
- refuseLiveLoop(root, seams.isAlive);
2803
- const now = seams.now ?? (() => new Date);
2804
- let result;
2805
- try {
2806
- result = fixStoreSchema({
2807
- path: sqliteStorePath(root),
2808
- dryRun,
2809
- stamp: fileStamp(now()),
2810
- now,
2811
- ...seams.identity === undefined ? {} : { identity: seams.identity }
2812
- });
2813
- } catch (error) {
2814
- if (error instanceof RebuildRefusal)
2815
- throw new CommandExit(1, `\u274C ${COMMAND_NAME}: ${error.message}`);
2816
- throw error;
2817
- }
2818
- if (context.outputMode === "json") {
2819
- context.output.result(result);
2820
- return;
2821
- }
2822
- for (const line of renderFixSchema(result))
2823
- context.output.info(line);
2824
- }
2825
- function createFixSchemaCommand(seams = {}) {
2826
- const command = {
2827
- name: "effort fix-schema",
2828
- subject: "effort",
2829
- action: "fix-schema",
2830
- summary: "rebuild an effort store this rafa refuses at the migrations it knows, keeping the original",
2831
- description: "Repairs `.rafa/effort/effort.sqlite`, or the store under `RAFA_EFFORT_DIR`, when this rafa refuses" + " it for a reason a rebuild repairs: no migration log past the legacy entries (pre-log-unreleased), a log" + " with a schema version it does not match (gate-mismatch), a logged checksum this rafa holds otherwise" + " (edited), or a logged migration this rafa does not know that breaks older readers or writers" + " (unknown-breaks-readers, unknown-breaks-writers). Installing a rafa that knows the store's migrations" + " comes first where one exists. It builds a parallel store beside it at this rafa's migrations, with a" + " migration log naming this rafa, copies every table and column this rafa knows, checks the row counts," + " SQLite's integrity_check and the schema plan, and lists what only the newer schema holds, unknown" + " migrations included. Then it writes the original to `effort.sqlite.v<user_version>-<stamp>.bak`," + " whole, and moves the rebuild into its place. The rebuild keeps the store's id; the backup is a copy," + " so renamed back it takes a new id on its next write. A store this rafa uses, current or behind, or no store, is" + " left alone. It refuses, changing nothing, while a loop session under the project is running or paused," + " from a development build, while a journal beside the store shows a write in flight, when the newer" + " schema lacks a table or column this rafa writes, and on a refusal a rebuild does not repair. With" + " `--output=json` the outcome is the data of the terminal result event. Starts no session.",
2832
- args: [],
2833
- flags: [
2834
- {
2835
- name: DRY_RUN_FLAG,
2836
- 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.",
2837
- type: "boolean"
2838
- }
2839
- ],
2840
- examples: [
2841
- {
2842
- cmd: "rafa effort fix-schema --dry-run",
2843
- note: "Prints what a rebuild would keep and leave behind, changing nothing."
2844
- },
2845
- {
2846
- cmd: "rafa effort fix-schema",
2847
- note: "Swaps the rebuild in and prints where the original was kept."
2848
- }
2849
- ],
2850
- outputs: ["text", "json"],
2851
- run: async (context) => {
2852
- runFixSchema(context, seams);
2853
- await Promise.resolve();
2854
- }
2855
- };
2856
- return Object.freeze(command);
2857
- }
2858
- var fix_schema_default = createFixSchemaCommand();
2859
-
2860
- // src/effort/store/merge-store.ts
2861
- import { randomUUID as randomUUID5 } from "crypto";
2862
- import { existsSync as existsSync11, mkdtempSync, rmSync as rmSync7 } from "fs";
2863
- import { tmpdir as tmpdir3 } from "os";
2864
- import { dirname as dirname11, join as join11 } from "path";
2865
- import { Database as Database6, SQLiteError as SQLiteError2 } from "bun:sqlite";
2866
2529
 
2867
2530
  // src/effort/commits.ts
2868
2531
  import { execFileSync } from "child_process";
@@ -3092,6 +2755,7 @@ var MERGE_RULES = {
3092
2755
  plan_ci: appendOnly("pr", "head_sha", "read_at"),
3093
2756
  schema_migrations: { scope: "local" },
3094
2757
  store_meta: { scope: "local" },
2758
+ store_replaced: { scope: "local" },
3095
2759
  merges: { scope: "local" },
3096
2760
  merge_conflicts: { scope: "local" }
3097
2761
  };
@@ -3270,14 +2934,7 @@ function settleTable(local, other, union, trail) {
3270
2934
  function settleMatches(local, other, unions, trail) {
3271
2935
  return local.transaction(() => unions.map((union) => settleTable(local, other, union, trail)))();
3272
2936
  }
3273
-
3274
2937
  // src/effort/store/migrate.ts
3275
- import { existsSync as existsSync10 } from "fs";
3276
- import { Database as Database5 } from "bun:sqlite";
3277
- var LIVE2 = "live";
3278
- function asidePaths2(path, first, stamp) {
3279
- return { parallelPath: `${path}.migrate-${stamp}`, backupPath: `${path}.before-${first}-${stamp}.bak` };
3280
- }
3281
2938
  function liveLoop(path, options, nothingDone = "Nothing was migrated.", ownSessionId = null) {
3282
2939
  const root = storeProjectRoot(path);
3283
2940
  if (root === null)
@@ -3290,71 +2947,6 @@ function liveLoop(path, options, nothingDone = "Nothing was migrated.", ownSessi
3290
2947
  }
3291
2948
  return records.find((record) => (record.state === "running" || record.state === "paused") && record.sessionId !== ownSessionId);
3292
2949
  }
3293
- function refuseLiveLoop2(path, breaking, options) {
3294
- const loop = liveLoop(path, options);
3295
- if (loop === undefined)
3296
- return;
3297
- const named = `loop ${loop.sessionId} (pid ${String(loop.pid)}, plan ${loop.planStub ?? loop.plan}) is running on this store`;
3298
- const [first] = breaking;
3299
- 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}.`;
3300
- throw new RebuildRefusal(`${opening} Nothing was migrated. Finish or stop that loop, then run it again.`);
3301
- }
3302
- function buildMigrated(path, parallelPath, migrations, stamp) {
3303
- vacuumInto(path, parallelPath);
3304
- const db = new Database5(parallelPath, { readwrite: true });
3305
- try {
3306
- const brought = bringForward(db, parallelPath, "write", "migrate", { migrations, ...stamp, builtAside: true });
3307
- db.run(`ATTACH DATABASE ? AS ${LIVE2}`, [path]);
3308
- const live = new Set(tableNames(db, LIVE2));
3309
- const counted = checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE2);
3310
- db.run(`DETACH DATABASE ${LIVE2}`);
3311
- refuseCorrupt(db);
3312
- refuseUnusable(db, parallelPath, migrations);
3313
- return { userVersion: brought.userVersion, counted };
3314
- } finally {
3315
- db.close();
3316
- }
3317
- }
3318
- function migrateStore(options) {
3319
- const { path, dryRun, stamp } = options;
3320
- guardTestProcess(path);
3321
- const migrations = options.migrations ?? SQLITE_MIGRATIONS;
3322
- const appliedBy = appliedByName(options.identity ?? readRuntimeIdentity());
3323
- const nothing = { applying: [], breaking: [], unknown: [], userVersion: null, counted: [], backupPath: null };
3324
- if (!existsSync10(path)) {
3325
- return { path, status: "missing", storeVersion: null, logged: false, appliedBy, ...nothing };
3326
- }
3327
- refuseInFlight(path);
3328
- const store = readOnlySchema(path);
3329
- const found = { path, storeVersion: store.userVersion, logged: store.log !== null, appliedBy };
3330
- const plan = planSchema(store, sqliteCatalogue(migrations), "write", "migrate");
3331
- if (plan.verdict === "refuse")
3332
- throw new RebuildRefusal(plan.message);
3333
- const applying = writeNames(plan);
3334
- const planned = {
3335
- applying,
3336
- breaking: plan.pending.filter(({ breaks }) => breaks.length > 0).map(({ id }) => id),
3337
- unknown: plan.unknown
3338
- };
3339
- if (applying.length === 0)
3340
- return { ...found, ...nothing, ...planned, status: "current" };
3341
- if (!dryRun) {
3342
- refuseUnownedDevelopmentWrite(path, applying, options);
3343
- refuseLiveLoop2(path, planned.breaking, options);
3344
- }
3345
- const first = plan.pending[0]?.id ?? ADOPTION_NAME;
3346
- const { built, backupPath } = rebuildAside({
3347
- path,
3348
- ...asidePaths2(path, first, stamp),
3349
- dryRun,
3350
- build: (parallelPath) => buildMigrated(path, parallelPath, migrations, {
3351
- appliedBy,
3352
- now: options.now ?? (() => new Date)
3353
- })
3354
- });
3355
- const status = dryRun ? "would-migrate" : "migrated";
3356
- return { ...found, ...planned, ...built, status, backupPath };
3357
- }
3358
2950
 
3359
2951
  // src/effort/store/merge-store.ts
3360
2952
  var MOVE_TO_SQLITE = "rafa effort move --to=sqlite";
@@ -3367,7 +2959,7 @@ class MergeRefusal extends RebuildRefusal {
3367
2959
  this.reason = reason;
3368
2960
  }
3369
2961
  }
3370
- var LIVE3 = "live";
2962
+ var LIVE = "live";
3371
2963
  var MERGES_TABLE = "merges";
3372
2964
  var INSERT_MERGE = `INSERT INTO merges (id, other_store, merged_at, rows_added, rows_skipped, rows_in_conflict)
3373
2965
  VALUES (?, ?, ?, ?, ?, ?)`;
@@ -3378,14 +2970,14 @@ function refuseNdjson(backend) {
3378
2970
  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}`);
3379
2971
  }
3380
2972
  function refuseMissing(path, which) {
3381
- if (existsSync11(path))
2973
+ if (existsSync8(path))
3382
2974
  return;
3383
2975
  throw new MergeRefusal("missing", `REFUSED \u2014 ${which} ${path} is not there. ${NOTHING_MERGED}`);
3384
2976
  }
3385
2977
  function refuseDamaged(path) {
3386
2978
  let answers;
3387
2979
  try {
3388
- const db = new Database6(path, { readonly: true });
2980
+ const db = new Database5(path, { readonly: true });
3389
2981
  try {
3390
2982
  answers = db.query("PRAGMA integrity_check").all().map((row) => row.integrity_check);
3391
2983
  } finally {
@@ -3407,7 +2999,7 @@ function writablePlan(path, migrations) {
3407
2999
  return plan;
3408
3000
  }
3409
3001
  function recordedMeta(path) {
3410
- const db = new Database6(path, { readonly: true });
3002
+ const db = new Database5(path, { readonly: true });
3411
3003
  try {
3412
3004
  const hasTable = db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
3413
3005
  return hasTable ? readStoreMeta(db) : null;
@@ -3424,7 +3016,7 @@ function refuseOtherProject(options) {
3424
3016
  }
3425
3017
  return other?.storeId ?? null;
3426
3018
  }
3427
- function refuseLiveLoop3(path, options, sessionId) {
3019
+ function refuseLiveLoop(path, options, sessionId) {
3428
3020
  const loop = liveLoop(path, options, NOTHING_MERGED, sessionId);
3429
3021
  if (loop === undefined)
3430
3022
  return;
@@ -3433,7 +3025,7 @@ function refuseLiveLoop3(path, options, sessionId) {
3433
3025
  function openOther(otherPath, plan, stamp) {
3434
3026
  const needs = writeNames(plan);
3435
3027
  if (needs.length === 0) {
3436
- const db = new Database6(otherPath, { readonly: true });
3028
+ const db = new Database5(otherPath, { readonly: true });
3437
3029
  return { db, broughtForward: [], release: () => db.close() };
3438
3030
  }
3439
3031
  const dir = mkdtempSync(join11(tmpdir3(), "rafa-merge-other-"));
@@ -3444,7 +3036,7 @@ function openOther(otherPath, plan, stamp) {
3444
3036
  try {
3445
3037
  const copyPath = join11(dir, "other.sqlite");
3446
3038
  vacuumInto(otherPath, copyPath);
3447
- const db = new Database6(copyPath, { readwrite: true });
3039
+ const db = new Database5(copyPath, { readwrite: true });
3448
3040
  try {
3449
3041
  bringForward(db, copyPath, "write", "open", { ...stamp, builtAside: true });
3450
3042
  } catch (error) {
@@ -3488,14 +3080,14 @@ function addedRows(merged) {
3488
3080
  }
3489
3081
  function buildMerged(path, parallelPath, other, merge2) {
3490
3082
  vacuumInto(path, parallelPath);
3491
- const db = new Database6(parallelPath, { readwrite: true });
3083
+ const db = new Database5(parallelPath, { readwrite: true });
3492
3084
  try {
3493
3085
  bringForward(db, parallelPath, "write", "open", { ...merge2.stamp, builtAside: true });
3494
3086
  const merged = mergeInto(db, other, merge2.id, merge2.otherStore, merge2.stamp.now().toISOString());
3495
- db.run(`ATTACH DATABASE ? AS ${LIVE3}`, [path]);
3496
- const live = new Set(tableNames(db, LIVE3));
3497
- checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE3, addedRows(merged));
3498
- db.run(`DETACH DATABASE ${LIVE3}`);
3087
+ db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
3088
+ const live = new Set(tableNames(db, LIVE));
3089
+ checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE, addedRows(merged));
3090
+ db.run(`DETACH DATABASE ${LIVE}`);
3499
3091
  refuseCorrupt(db);
3500
3092
  refuseUnusable(db, parallelPath, merge2.stamp.migrations);
3501
3093
  return merged;
@@ -3516,7 +3108,7 @@ function refuseUnmergeable(options, migrations) {
3516
3108
  const otherStore = refuseOtherProject(options);
3517
3109
  if (!options.dryRun) {
3518
3110
  refuseUnownedDevelopmentWrite(path, [...writeNames(localPlan), MERGES_TABLE], options);
3519
- refuseLiveLoop3(path, options, options.sessionId ?? null);
3111
+ refuseLiveLoop(path, options, options.sessionId ?? null);
3520
3112
  }
3521
3113
  return { otherPlan, otherStore };
3522
3114
  }
@@ -3564,7 +3156,7 @@ function refuseUncarriable(backend, source) {
3564
3156
  if (backend === "ndjson") {
3565
3157
  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}`);
3566
3158
  }
3567
- if (!existsSync12(join12(source, SQLITE_STORE_FILE_NAME))) {
3159
+ if (!existsSync9(join12(source, SQLITE_STORE_FILE_NAME))) {
3568
3160
  throw new FileSyncRefusal(`${REFUSAL}: no ${SQLITE_STORE_FILE_NAME} in ${source}. ${NOTHING_COPIED}`);
3569
3161
  }
3570
3162
  }
@@ -3578,14 +3170,14 @@ function createFileSync(options) {
3578
3170
  const now = options.now ?? (() => new Date);
3579
3171
  const storeDir = () => effortStoreDir(repoRoot, options.env);
3580
3172
  const push = async ({ to }) => {
3581
- const target = resolve5(repoRoot, requirePath("to", to));
3173
+ const target = resolve4(repoRoot, requirePath("to", to));
3582
3174
  const source = storeDir();
3583
3175
  refuseUncarriable(backend, source);
3584
3176
  const copied = copyEffortStore({ source, target });
3585
3177
  return Object.freeze({ status: "pushed", path: join12(copied.directory, SQLITE_STORE_FILE_NAME) });
3586
3178
  };
3587
3179
  const pull = async ({ from, dryRun, sessionId = null }) => {
3588
- const otherPath = resolve5(repoRoot, requirePath("from", from));
3180
+ const otherPath = resolve4(repoRoot, requirePath("from", from));
3589
3181
  const path = join12(storeDir(), SQLITE_STORE_FILE_NAME);
3590
3182
  const stamp = fileStamp(now());
3591
3183
  const merge2 = mergeStore({ ...mergeSeams(options), path, otherPath, backend, dryRun, stamp, now, sessionId });
@@ -3657,271 +3249,28 @@ function selectSync(repoRoot, config, registry = CORE_ADAPTER_REGISTRY) {
3657
3249
  import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
3658
3250
  import { join as join15 } from "path";
3659
3251
 
3660
- // src/commands/instinct/instinct-records.ts
3661
- import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
3662
- import { join as join14 } from "path";
3663
-
3664
- // src/check/layout.ts
3665
- import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3666
- import { join as join13, relative as relative2 } from "path";
3667
- var SKILL_FILE = "SKILL.md";
3668
- var MARKDOWN_EXTENSION = ".md";
3669
- var LAYOUT_SEVERITY = Object.freeze({
3670
- "flat-grouped-layout": "failure",
3671
- "nested-group-layout": "failure",
3672
- "loose-file": "failure",
3673
- "missing-skill-file": "failure",
3674
- "nested-instinct-file": "failure",
3675
- "unreadable-entry": "failure",
3676
- "name-mismatch": "failure",
3677
- "dotfile-only-directory": "warning"
3678
- });
3679
- function layoutIssue(code, message) {
3680
- return { code, severity: LAYOUT_SEVERITY[code], message };
3252
+ // src/schema/frontmatter.ts
3253
+ var FRONTMATTER_FENCE = "---";
3254
+ var FRONTMATTER_INDENT = 2;
3255
+ function scanLine(text, offset) {
3256
+ const index = text.indexOf(`
3257
+ `, offset);
3258
+ if (index === -1) {
3259
+ return { line: text.slice(offset), newline: "", next: text.length };
3260
+ }
3261
+ const carriage = index > offset && text[index - 1] === "\r";
3262
+ const end = carriage ? index - 1 : index;
3263
+ return {
3264
+ line: text.slice(offset, end),
3265
+ newline: text.slice(end, index + 1),
3266
+ next: index + 1
3267
+ };
3681
3268
  }
3682
- function entryKind(path) {
3683
- try {
3684
- const stats = statSync4(path);
3685
- if (stats.isFile())
3686
- return "file";
3687
- if (stats.isDirectory())
3688
- return "directory";
3689
- return null;
3690
- } catch {
3691
- return null;
3692
- }
3693
- }
3694
- function visibleNames(dir) {
3695
- return readdirSync3(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
3696
- }
3697
- function isMarkdown(name2) {
3698
- return name2.endsWith(MARKDOWN_EXTENSION);
3699
- }
3700
- function markdownStem(name2) {
3701
- return name2.slice(0, -MARKDOWN_EXTENSION.length);
3702
- }
3703
- function shown(root, path) {
3704
- return relative2(root, path) || ".";
3705
- }
3706
- function dotfileOnlyEntry(kind, root, dir) {
3707
- return {
3708
- kind,
3709
- path: dir,
3710
- isFile: false,
3711
- shape: "dotfile-only",
3712
- name: null,
3713
- issues: [layoutIssue("dotfile-only-directory", `${shown(root, dir)}/ holds nothing but dotfiles, so it registers nothing`)]
3714
- };
3715
- }
3716
- function unreadableEntry(kind, root, path) {
3717
- return {
3718
- kind,
3719
- path,
3720
- isFile: false,
3721
- shape: "unreadable",
3722
- name: null,
3723
- issues: [layoutIssue("unreadable-entry", `${shown(root, path)} cannot be read (a broken symlink, or a path stat refuses)`)]
3724
- };
3725
- }
3726
- function skillFileEntry(path, name2) {
3727
- return { kind: "skill", path, isFile: true, shape: "skill-file", name: name2, issues: [] };
3728
- }
3729
- function flatGroupedEntry(root, path, name2) {
3730
- return {
3731
- kind: "skill",
3732
- path,
3733
- isFile: true,
3734
- shape: "flat-grouped",
3735
- name: name2,
3736
- issues: [layoutIssue("flat-grouped-layout", `${shown(root, path)} is the flat grouped layout <dir>/<group>/<name>.md,` + ` which does not register; move it to <dir>/${name2}/${SKILL_FILE}`)]
3737
- };
3738
- }
3739
- function nestedGroupEntry(root, path, name2) {
3740
- return {
3741
- kind: "skill",
3742
- path,
3743
- isFile: true,
3744
- shape: "nested-group",
3745
- name: name2,
3746
- issues: [layoutIssue("nested-group-layout", `${shown(root, path)} is the nested layout <dir>/<group>/<name>/${SKILL_FILE},` + ` which does not register; move it to <dir>/${name2}/${SKILL_FILE}`)]
3747
- };
3748
- }
3749
- function looseFileEntry(root, path, name2) {
3750
- return {
3751
- kind: "skill",
3752
- path,
3753
- isFile: true,
3754
- shape: "loose-file",
3755
- name: name2,
3756
- issues: [layoutIssue("loose-file", `${shown(root, path)} sits directly under the skills directory, which registers` + ` only <dir>/<name>/${SKILL_FILE}; move it to <dir>/${name2}/${SKILL_FILE}`)]
3757
- };
3758
- }
3759
- function noSkillFileEntry(root, dir) {
3760
- return {
3761
- kind: "skill",
3762
- path: dir,
3763
- isFile: false,
3764
- shape: "no-skill-file",
3765
- name: null,
3766
- issues: [layoutIssue("missing-skill-file", `${shown(root, dir)}/ holds no ${SKILL_FILE}, so it registers no skill`)]
3767
- };
3768
- }
3769
- function nestedInstinctEntry(root, path, name2) {
3770
- return {
3771
- kind: "instinct",
3772
- path,
3773
- isFile: true,
3774
- shape: "nested-instinct",
3775
- name: name2,
3776
- issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${name2}${MARKDOWN_EXTENSION}`)]
3777
- };
3778
- }
3779
- function scanGroupMember(root, dir, name2) {
3780
- const children = visibleNames(dir);
3781
- if (children.length === 0)
3782
- return [dotfileOnlyEntry("skill", root, dir)];
3783
- const skillPath = join13(dir, SKILL_FILE);
3784
- if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
3785
- return [nestedGroupEntry(root, skillPath, name2)];
3786
- }
3787
- return [noSkillFileEntry(root, dir)];
3788
- }
3789
- function scanSkillGroup(root, dir, children) {
3790
- const entries = [];
3791
- for (const child of children) {
3792
- const path = join13(dir, child);
3793
- const found = entryKind(path);
3794
- if (found === null) {
3795
- entries.push(unreadableEntry("skill", root, path));
3796
- continue;
3797
- }
3798
- if (found === "directory") {
3799
- entries.push(...scanGroupMember(root, path, child));
3800
- continue;
3801
- }
3802
- if (isMarkdown(child))
3803
- entries.push(flatGroupedEntry(root, path, markdownStem(child)));
3804
- }
3805
- if (entries.length === 0)
3806
- return [noSkillFileEntry(root, dir)];
3807
- return entries;
3808
- }
3809
- function scanSkillDirectory(root, dir, name2) {
3810
- const children = visibleNames(dir);
3811
- if (children.length === 0)
3812
- return [dotfileOnlyEntry("skill", root, dir)];
3813
- const skillPath = join13(dir, SKILL_FILE);
3814
- if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
3815
- return [skillFileEntry(skillPath, name2)];
3816
- }
3817
- return scanSkillGroup(root, dir, children);
3818
- }
3819
- function scanSkillRoot(root) {
3820
- const entries = [];
3821
- for (const name2 of visibleNames(root)) {
3822
- const path = join13(root, name2);
3823
- const found = entryKind(path);
3824
- if (found === null) {
3825
- entries.push(unreadableEntry("skill", root, path));
3826
- continue;
3827
- }
3828
- if (found === "directory") {
3829
- entries.push(...scanSkillDirectory(root, path, name2));
3830
- continue;
3831
- }
3832
- if (isMarkdown(name2))
3833
- entries.push(looseFileEntry(root, path, markdownStem(name2)));
3834
- }
3835
- return entries;
3836
- }
3837
- function scanInstinctSubdirectory(root, dir) {
3838
- const children = visibleNames(dir);
3839
- if (children.length === 0)
3840
- return [dotfileOnlyEntry("instinct", root, dir)];
3841
- const entries = [];
3842
- for (const child of children) {
3843
- const path = join13(dir, child);
3844
- const found = entryKind(path);
3845
- if (found === null) {
3846
- entries.push(unreadableEntry("instinct", root, path));
3847
- continue;
3848
- }
3849
- if (found === "directory") {
3850
- entries.push(...scanInstinctSubdirectory(root, path));
3851
- continue;
3852
- }
3853
- if (isMarkdown(child))
3854
- entries.push(nestedInstinctEntry(root, path, markdownStem(child)));
3855
- }
3856
- return entries;
3857
- }
3858
- function scanInstinctRoot(root) {
3859
- const entries = [];
3860
- for (const name2 of visibleNames(root)) {
3861
- const path = join13(root, name2);
3862
- const found = entryKind(path);
3863
- if (found === null) {
3864
- entries.push(unreadableEntry("instinct", root, path));
3865
- continue;
3866
- }
3867
- if (found === "directory") {
3868
- entries.push(...scanInstinctSubdirectory(root, path));
3869
- continue;
3870
- }
3871
- if (!isMarkdown(name2))
3872
- continue;
3873
- entries.push({
3874
- kind: "instinct",
3875
- path,
3876
- isFile: true,
3877
- shape: "instinct-file",
3878
- name: markdownStem(name2),
3879
- issues: []
3880
- });
3881
- }
3882
- return entries;
3883
- }
3884
- function scanLayout(root, kind) {
3885
- if (entryKind(root) !== "directory") {
3886
- throw new Error(`check layout: no ${kind} directory at ${root}`);
3887
- }
3888
- const entries = kind === "skill" ? scanSkillRoot(root) : scanInstinctRoot(root);
3889
- return {
3890
- kind,
3891
- root,
3892
- entries: entries.sort((left, right) => left.path.localeCompare(right.path))
3893
- };
3894
- }
3895
- function declaredNameIssue(entry, declared) {
3896
- if (entry.name === null || declared === null || declared === entry.name)
3897
- return null;
3898
- const field2 = entry.kind === "skill" ? "name" : "id";
3899
- const source = entry.kind === "skill" ? "directory name" : "file name";
3900
- return layoutIssue("name-mismatch", `frontmatter ${field2} "${declared}" is not the ${source} "${entry.name}"`);
3901
- }
3902
-
3903
- // src/schema/frontmatter.ts
3904
- var FRONTMATTER_FENCE = "---";
3905
- var FRONTMATTER_INDENT = 2;
3906
- function scanLine(text, offset) {
3907
- const index = text.indexOf(`
3908
- `, offset);
3909
- if (index === -1) {
3910
- return { line: text.slice(offset), newline: "", next: text.length };
3911
- }
3912
- const carriage = index > offset && text[index - 1] === "\r";
3913
- const end = carriage ? index - 1 : index;
3914
- return {
3915
- line: text.slice(offset, end),
3916
- newline: text.slice(end, index + 1),
3917
- next: index + 1
3918
- };
3919
- }
3920
- function isRecord3(value) {
3921
- return typeof value === "object" && value !== null && !Array.isArray(value);
3922
- }
3923
- function parseBlock(yaml) {
3924
- let parsed;
3269
+ function isRecord3(value) {
3270
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3271
+ }
3272
+ function parseBlock(yaml) {
3273
+ let parsed;
3925
3274
  try {
3926
3275
  parsed = Bun.YAML.parse(yaml.replace(/\r\n/g, `
3927
3276
  `));
@@ -4200,105 +3549,348 @@ function sectionText(body, heading) {
4200
3549
  inside = line.trim() === heading;
4201
3550
  continue;
4202
3551
  }
4203
- if (inside)
4204
- collected.push(line);
3552
+ if (inside)
3553
+ collected.push(line);
3554
+ }
3555
+ return inside ? collected.join(`
3556
+ `).trim() : null;
3557
+ }
3558
+ function checkSection(body, heading) {
3559
+ const text = sectionText(body, heading);
3560
+ if (text === null) {
3561
+ return [issue("missing-section", heading, `the body carries no ${heading} section`)];
3562
+ }
3563
+ return text === "" ? [issue("missing-section", heading, `the ${heading} section is empty`)] : [];
3564
+ }
3565
+ function checkInstinctBody(body) {
3566
+ return [...checkSection(body, ACTION_HEADING), ...checkSection(body, CAUSE_HEADING)];
3567
+ }
3568
+ function parseInstinct(text) {
3569
+ const document = readFrontmatterDocument(text);
3570
+ if (document === null) {
3571
+ return {
3572
+ issues: [issue("missing-frontmatter", "frontmatter", "the file opens with no --- block holding a YAML mapping")],
3573
+ instinct: null
3574
+ };
3575
+ }
3576
+ const issues = [
3577
+ ...checkInstinctFrontmatter(document.data),
3578
+ ...checkInstinctBody(document.body)
3579
+ ];
3580
+ return issues.length > 0 ? { issues, instinct: null } : { issues, instinct: buildInstinct(document.data, document.body) };
3581
+ }
3582
+ function buildInstinct(data, body) {
3583
+ const action = sectionText(body, ACTION_HEADING) ?? "";
3584
+ const artifact = typeof data["artifact"] === "string" && data["artifact"].trim() !== "" ? data["artifact"] : null;
3585
+ return {
3586
+ id: data["id"],
3587
+ trigger: data["trigger"],
3588
+ kind: data["kind"],
3589
+ domain: data["domain"],
3590
+ confidence: data["confidence"],
3591
+ usageCount: typeof data["usage_count"] === "number" ? data["usage_count"] : DEFAULT_USAGE_COUNT,
3592
+ sources: Array.isArray(data["sources"]) ? data["sources"] : [],
3593
+ artifact,
3594
+ signal: data["signal"],
3595
+ scope: data["scope"],
3596
+ projectId: typeof data["project_id"] === "string" ? data["project_id"] : null,
3597
+ source: data["source"],
3598
+ evidence: Array.isArray(data["evidence"]) ? data["evidence"] : [],
3599
+ promotedTo: typeof data["promoted_to"] === "string" ? data["promoted_to"] : null,
3600
+ createdAt: data["created_at"],
3601
+ updatedAt: data["updated_at"],
3602
+ action,
3603
+ cause: sectionText(body, CAUSE_HEADING) ?? "",
3604
+ actionHash: actionHash(action)
3605
+ };
3606
+ }
3607
+ function instinctFrontmatter(instinct) {
3608
+ const written = {
3609
+ id: instinct.id,
3610
+ trigger: instinct.trigger,
3611
+ kind: instinct.kind,
3612
+ domain: instinct.domain,
3613
+ confidence: instinct.confidence,
3614
+ usage_count: instinct.usageCount,
3615
+ signal: instinct.signal,
3616
+ scope: instinct.scope,
3617
+ source: instinct.source,
3618
+ created_at: instinct.createdAt,
3619
+ updated_at: instinct.updatedAt
3620
+ };
3621
+ if (instinct.artifact !== null)
3622
+ written["artifact"] = instinct.artifact;
3623
+ if (instinct.projectId !== null)
3624
+ written["project_id"] = instinct.projectId;
3625
+ if (instinct.evidence.length > 0)
3626
+ written["evidence"] = instinct.evidence;
3627
+ if (instinct.sources.length > 0)
3628
+ written["sources"] = instinct.sources;
3629
+ if (instinct.promotedTo !== null)
3630
+ written["promoted_to"] = instinct.promotedTo;
3631
+ return Object.fromEntries(INSTINCT_KEY_ORDER.filter((key) => Object.hasOwn(written, key)).map((key) => [key, written[key]]));
3632
+ }
3633
+ function writeInstinct(instinct, newline = `
3634
+ `) {
3635
+ const block = renderFrontmatter(instinctFrontmatter(instinct), newline);
3636
+ return [
3637
+ "---",
3638
+ block,
3639
+ "---",
3640
+ "",
3641
+ ACTION_HEADING,
3642
+ instinct.action,
3643
+ "",
3644
+ CAUSE_HEADING,
3645
+ instinct.cause,
3646
+ ""
3647
+ ].join(newline);
3648
+ }
3649
+
3650
+ // src/schema/scope-records.ts
3651
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
3652
+ import { join as join14 } from "path";
3653
+
3654
+ // src/check/layout.ts
3655
+ import { readdirSync as readdirSync3, statSync as statSync3 } from "fs";
3656
+ import { join as join13, relative as relative2 } from "path";
3657
+ var SKILL_FILE = "SKILL.md";
3658
+ var MARKDOWN_EXTENSION = ".md";
3659
+ var LAYOUT_SEVERITY = Object.freeze({
3660
+ "flat-grouped-layout": "failure",
3661
+ "nested-group-layout": "failure",
3662
+ "loose-file": "failure",
3663
+ "missing-skill-file": "failure",
3664
+ "nested-instinct-file": "failure",
3665
+ "unreadable-entry": "failure",
3666
+ "name-mismatch": "failure",
3667
+ "dotfile-only-directory": "warning"
3668
+ });
3669
+ function layoutIssue(code, message) {
3670
+ return { code, severity: LAYOUT_SEVERITY[code], message };
3671
+ }
3672
+ function entryKind(path) {
3673
+ try {
3674
+ const stats = statSync3(path);
3675
+ if (stats.isFile())
3676
+ return "file";
3677
+ if (stats.isDirectory())
3678
+ return "directory";
3679
+ return null;
3680
+ } catch {
3681
+ return null;
3682
+ }
3683
+ }
3684
+ function visibleNames(dir) {
3685
+ return readdirSync3(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
3686
+ }
3687
+ function isMarkdown(name2) {
3688
+ return name2.endsWith(MARKDOWN_EXTENSION);
3689
+ }
3690
+ function markdownStem(name2) {
3691
+ return name2.slice(0, -MARKDOWN_EXTENSION.length);
3692
+ }
3693
+ function shown(root, path) {
3694
+ return relative2(root, path) || ".";
3695
+ }
3696
+ function dotfileOnlyEntry(kind, root, dir) {
3697
+ return {
3698
+ kind,
3699
+ path: dir,
3700
+ isFile: false,
3701
+ shape: "dotfile-only",
3702
+ name: null,
3703
+ issues: [layoutIssue("dotfile-only-directory", `${shown(root, dir)}/ holds nothing but dotfiles, so it registers nothing`)]
3704
+ };
3705
+ }
3706
+ function unreadableEntry(kind, root, path) {
3707
+ return {
3708
+ kind,
3709
+ path,
3710
+ isFile: false,
3711
+ shape: "unreadable",
3712
+ name: null,
3713
+ issues: [layoutIssue("unreadable-entry", `${shown(root, path)} cannot be read (a broken symlink, or a path stat refuses)`)]
3714
+ };
3715
+ }
3716
+ function skillFileEntry(path, name2) {
3717
+ return { kind: "skill", path, isFile: true, shape: "skill-file", name: name2, issues: [] };
3718
+ }
3719
+ function flatGroupedEntry(root, path, name2) {
3720
+ return {
3721
+ kind: "skill",
3722
+ path,
3723
+ isFile: true,
3724
+ shape: "flat-grouped",
3725
+ name: name2,
3726
+ issues: [layoutIssue("flat-grouped-layout", `${shown(root, path)} is the flat grouped layout <dir>/<group>/<name>.md,` + ` which does not register; move it to <dir>/${name2}/${SKILL_FILE}`)]
3727
+ };
3728
+ }
3729
+ function nestedGroupEntry(root, path, name2) {
3730
+ return {
3731
+ kind: "skill",
3732
+ path,
3733
+ isFile: true,
3734
+ shape: "nested-group",
3735
+ name: name2,
3736
+ issues: [layoutIssue("nested-group-layout", `${shown(root, path)} is the nested layout <dir>/<group>/<name>/${SKILL_FILE},` + ` which does not register; move it to <dir>/${name2}/${SKILL_FILE}`)]
3737
+ };
3738
+ }
3739
+ function looseFileEntry(root, path, name2) {
3740
+ return {
3741
+ kind: "skill",
3742
+ path,
3743
+ isFile: true,
3744
+ shape: "loose-file",
3745
+ name: name2,
3746
+ issues: [layoutIssue("loose-file", `${shown(root, path)} sits directly under the skills directory, which registers` + ` only <dir>/<name>/${SKILL_FILE}; move it to <dir>/${name2}/${SKILL_FILE}`)]
3747
+ };
3748
+ }
3749
+ function noSkillFileEntry(root, dir) {
3750
+ return {
3751
+ kind: "skill",
3752
+ path: dir,
3753
+ isFile: false,
3754
+ shape: "no-skill-file",
3755
+ name: null,
3756
+ issues: [layoutIssue("missing-skill-file", `${shown(root, dir)}/ holds no ${SKILL_FILE}, so it registers no skill`)]
3757
+ };
3758
+ }
3759
+ function nestedInstinctEntry(root, path, name2) {
3760
+ return {
3761
+ kind: "instinct",
3762
+ path,
3763
+ isFile: true,
3764
+ shape: "nested-instinct",
3765
+ name: name2,
3766
+ issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${name2}${MARKDOWN_EXTENSION}`)]
3767
+ };
3768
+ }
3769
+ function scanGroupMember(root, dir, name2) {
3770
+ const children = visibleNames(dir);
3771
+ if (children.length === 0)
3772
+ return [dotfileOnlyEntry("skill", root, dir)];
3773
+ const skillPath = join13(dir, SKILL_FILE);
3774
+ if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
3775
+ return [nestedGroupEntry(root, skillPath, name2)];
3776
+ }
3777
+ return [noSkillFileEntry(root, dir)];
3778
+ }
3779
+ function scanSkillGroup(root, dir, children) {
3780
+ const entries = [];
3781
+ for (const child of children) {
3782
+ const path = join13(dir, child);
3783
+ const found = entryKind(path);
3784
+ if (found === null) {
3785
+ entries.push(unreadableEntry("skill", root, path));
3786
+ continue;
3787
+ }
3788
+ if (found === "directory") {
3789
+ entries.push(...scanGroupMember(root, path, child));
3790
+ continue;
3791
+ }
3792
+ if (isMarkdown(child))
3793
+ entries.push(flatGroupedEntry(root, path, markdownStem(child)));
4205
3794
  }
4206
- return inside ? collected.join(`
4207
- `).trim() : null;
3795
+ if (entries.length === 0)
3796
+ return [noSkillFileEntry(root, dir)];
3797
+ return entries;
4208
3798
  }
4209
- function checkSection(body, heading) {
4210
- const text = sectionText(body, heading);
4211
- if (text === null) {
4212
- return [issue("missing-section", heading, `the body carries no ${heading} section`)];
3799
+ function scanSkillDirectory(root, dir, name2) {
3800
+ const children = visibleNames(dir);
3801
+ if (children.length === 0)
3802
+ return [dotfileOnlyEntry("skill", root, dir)];
3803
+ const skillPath = join13(dir, SKILL_FILE);
3804
+ if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
3805
+ return [skillFileEntry(skillPath, name2)];
4213
3806
  }
4214
- return text === "" ? [issue("missing-section", heading, `the ${heading} section is empty`)] : [];
3807
+ return scanSkillGroup(root, dir, children);
4215
3808
  }
4216
- function checkInstinctBody(body) {
4217
- return [...checkSection(body, ACTION_HEADING), ...checkSection(body, CAUSE_HEADING)];
3809
+ function scanSkillRoot(root) {
3810
+ const entries = [];
3811
+ for (const name2 of visibleNames(root)) {
3812
+ const path = join13(root, name2);
3813
+ const found = entryKind(path);
3814
+ if (found === null) {
3815
+ entries.push(unreadableEntry("skill", root, path));
3816
+ continue;
3817
+ }
3818
+ if (found === "directory") {
3819
+ entries.push(...scanSkillDirectory(root, path, name2));
3820
+ continue;
3821
+ }
3822
+ if (isMarkdown(name2))
3823
+ entries.push(looseFileEntry(root, path, markdownStem(name2)));
3824
+ }
3825
+ return entries;
4218
3826
  }
4219
- function parseInstinct(text) {
4220
- const document = readFrontmatterDocument(text);
4221
- if (document === null) {
4222
- return {
4223
- issues: [issue("missing-frontmatter", "frontmatter", "the file opens with no --- block holding a YAML mapping")],
4224
- instinct: null
4225
- };
3827
+ function scanInstinctSubdirectory(root, dir) {
3828
+ const children = visibleNames(dir);
3829
+ if (children.length === 0)
3830
+ return [dotfileOnlyEntry("instinct", root, dir)];
3831
+ const entries = [];
3832
+ for (const child of children) {
3833
+ const path = join13(dir, child);
3834
+ const found = entryKind(path);
3835
+ if (found === null) {
3836
+ entries.push(unreadableEntry("instinct", root, path));
3837
+ continue;
3838
+ }
3839
+ if (found === "directory") {
3840
+ entries.push(...scanInstinctSubdirectory(root, path));
3841
+ continue;
3842
+ }
3843
+ if (isMarkdown(child))
3844
+ entries.push(nestedInstinctEntry(root, path, markdownStem(child)));
4226
3845
  }
4227
- const issues = [
4228
- ...checkInstinctFrontmatter(document.data),
4229
- ...checkInstinctBody(document.body)
4230
- ];
4231
- return issues.length > 0 ? { issues, instinct: null } : { issues, instinct: buildInstinct(document.data, document.body) };
3846
+ return entries;
4232
3847
  }
4233
- function buildInstinct(data, body) {
4234
- const action = sectionText(body, ACTION_HEADING) ?? "";
4235
- const artifact = typeof data["artifact"] === "string" && data["artifact"].trim() !== "" ? data["artifact"] : null;
4236
- return {
4237
- id: data["id"],
4238
- trigger: data["trigger"],
4239
- kind: data["kind"],
4240
- domain: data["domain"],
4241
- confidence: data["confidence"],
4242
- usageCount: typeof data["usage_count"] === "number" ? data["usage_count"] : DEFAULT_USAGE_COUNT,
4243
- sources: Array.isArray(data["sources"]) ? data["sources"] : [],
4244
- artifact,
4245
- signal: data["signal"],
4246
- scope: data["scope"],
4247
- projectId: typeof data["project_id"] === "string" ? data["project_id"] : null,
4248
- source: data["source"],
4249
- evidence: Array.isArray(data["evidence"]) ? data["evidence"] : [],
4250
- promotedTo: typeof data["promoted_to"] === "string" ? data["promoted_to"] : null,
4251
- createdAt: data["created_at"],
4252
- updatedAt: data["updated_at"],
4253
- action,
4254
- cause: sectionText(body, CAUSE_HEADING) ?? "",
4255
- actionHash: actionHash(action)
4256
- };
3848
+ function scanInstinctRoot(root) {
3849
+ const entries = [];
3850
+ for (const name2 of visibleNames(root)) {
3851
+ const path = join13(root, name2);
3852
+ const found = entryKind(path);
3853
+ if (found === null) {
3854
+ entries.push(unreadableEntry("instinct", root, path));
3855
+ continue;
3856
+ }
3857
+ if (found === "directory") {
3858
+ entries.push(...scanInstinctSubdirectory(root, path));
3859
+ continue;
3860
+ }
3861
+ if (!isMarkdown(name2))
3862
+ continue;
3863
+ entries.push({
3864
+ kind: "instinct",
3865
+ path,
3866
+ isFile: true,
3867
+ shape: "instinct-file",
3868
+ name: markdownStem(name2),
3869
+ issues: []
3870
+ });
3871
+ }
3872
+ return entries;
4257
3873
  }
4258
- function instinctFrontmatter(instinct) {
4259
- const written = {
4260
- id: instinct.id,
4261
- trigger: instinct.trigger,
4262
- kind: instinct.kind,
4263
- domain: instinct.domain,
4264
- confidence: instinct.confidence,
4265
- usage_count: instinct.usageCount,
4266
- signal: instinct.signal,
4267
- scope: instinct.scope,
4268
- source: instinct.source,
4269
- created_at: instinct.createdAt,
4270
- updated_at: instinct.updatedAt
3874
+ function scanLayout(root, kind) {
3875
+ if (entryKind(root) !== "directory") {
3876
+ throw new Error(`check layout: no ${kind} directory at ${root}`);
3877
+ }
3878
+ const entries = kind === "skill" ? scanSkillRoot(root) : scanInstinctRoot(root);
3879
+ return {
3880
+ kind,
3881
+ root,
3882
+ entries: entries.sort((left, right) => left.path.localeCompare(right.path))
4271
3883
  };
4272
- if (instinct.artifact !== null)
4273
- written["artifact"] = instinct.artifact;
4274
- if (instinct.projectId !== null)
4275
- written["project_id"] = instinct.projectId;
4276
- if (instinct.evidence.length > 0)
4277
- written["evidence"] = instinct.evidence;
4278
- if (instinct.sources.length > 0)
4279
- written["sources"] = instinct.sources;
4280
- if (instinct.promotedTo !== null)
4281
- written["promoted_to"] = instinct.promotedTo;
4282
- return Object.fromEntries(INSTINCT_KEY_ORDER.filter((key) => Object.hasOwn(written, key)).map((key) => [key, written[key]]));
4283
3884
  }
4284
- function writeInstinct(instinct, newline = `
4285
- `) {
4286
- const block = renderFrontmatter(instinctFrontmatter(instinct), newline);
4287
- return [
4288
- "---",
4289
- block,
4290
- "---",
4291
- "",
4292
- ACTION_HEADING,
4293
- instinct.action,
4294
- "",
4295
- CAUSE_HEADING,
4296
- instinct.cause,
4297
- ""
4298
- ].join(newline);
3885
+ function declaredNameIssue(entry, declared) {
3886
+ if (entry.name === null || declared === null || declared === entry.name)
3887
+ return null;
3888
+ const field2 = entry.kind === "skill" ? "name" : "id";
3889
+ const source = entry.kind === "skill" ? "directory name" : "file name";
3890
+ return layoutIssue("name-mismatch", `frontmatter ${field2} "${declared}" is not the ${source} "${entry.name}"`);
4299
3891
  }
4300
3892
 
4301
- // src/commands/instinct/instinct-records.ts
3893
+ // src/schema/scope-records.ts
4302
3894
  function isRecordFile(name2) {
4303
3895
  return !name2.startsWith(".") && name2.endsWith(MARKDOWN_EXTENSION) && name2 !== MARKDOWN_EXTENSION;
4304
3896
  }
@@ -4311,7 +3903,7 @@ function unreadableIssue(message) {
4311
3903
  function readRecord(path, scope, id) {
4312
3904
  let text;
4313
3905
  try {
4314
- text = readFileSync9(path, "utf8");
3906
+ text = readFileSync8(path, "utf8");
4315
3907
  } catch (error) {
4316
3908
  const message = error instanceof Error ? error.message : String(error);
4317
3909
  return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
@@ -4319,15 +3911,15 @@ function readRecord(path, scope, id) {
4319
3911
  const parsed = parseInstinct(text);
4320
3912
  return { scope, path, id, instinct: parsed.instinct, issues: parsed.issues };
4321
3913
  }
4322
- function isFile2(path) {
3914
+ function isFile(path) {
4323
3915
  try {
4324
- return statSync5(path).isFile();
3916
+ return statSync4(path).isFile();
4325
3917
  } catch {
4326
3918
  return false;
4327
3919
  }
4328
3920
  }
4329
3921
  function recordFiles(dir) {
4330
- return readdirSync4(dir).filter((name2) => isRecordFile(name2) && isFile2(join14(dir, name2))).sort((left, right) => left.localeCompare(right));
3922
+ return readdirSync4(dir).filter((name2) => isRecordFile(name2) && isFile(join14(dir, name2))).sort((left, right) => left.localeCompare(right));
4331
3923
  }
4332
3924
  function readScope(scope, dir) {
4333
3925
  if (!tierExists(dir))
@@ -4342,17 +3934,6 @@ function readScope(scope, dir) {
4342
3934
  function readScopes(seams) {
4343
3935
  return resolveInstinctScopes(seams).map((location) => readScope(location.scope, location.dir));
4344
3936
  }
4345
- function allRecords(listings) {
4346
- return listings.flatMap((listing) => listing.records);
4347
- }
4348
- function findRecords(listings, id) {
4349
- return allRecords(listings).filter((entry) => entry.id === id);
4350
- }
4351
- function instinctProject(context, name2) {
4352
- if (context.project === null)
4353
- throw new Error(`${name2} runs inside a project, and was handed none`);
4354
- return context.project;
4355
- }
4356
3937
 
4357
3938
  // src/adapters/learning/held.ts
4358
3939
  function compareText2(left, right) {
@@ -4756,6 +4337,203 @@ function createLocalLearning(options) {
4756
4337
  return Object.freeze(learning);
4757
4338
  }
4758
4339
 
4340
+ // src/adapters/logger/console.ts
4341
+ import { basename as basename4 } from "path";
4342
+
4343
+ // src/adapters/logger/api-filter.ts
4344
+ var REDACTED = "[redacted]";
4345
+ var UNREADABLE_URL = "[unreadable url]";
4346
+ var API_HEADER_ALLOW_LIST = Object.freeze([
4347
+ "content-type",
4348
+ "content-length",
4349
+ "retry-after",
4350
+ "x-request-id",
4351
+ "x-ratelimit-limit",
4352
+ "x-ratelimit-remaining",
4353
+ "x-ratelimit-reset"
4354
+ ]);
4355
+ var SENSITIVE_NAME = /token|key|secret|sig|passw|pwd|auth|code|jwt|session|credential/i;
4356
+ var QUERY_SEPARATOR = /([&;])/;
4357
+ var URL_IN_TEXT = /https?:\/\/[^\s"'<>)]+/gi;
4358
+ var READ_PROTOCOLS = ["http:", "https:"];
4359
+ function decodedName(name2) {
4360
+ try {
4361
+ return decodeURIComponent(name2.replace(/\+/g, " "));
4362
+ } catch {
4363
+ return null;
4364
+ }
4365
+ }
4366
+ function filterQueryPart(part) {
4367
+ if (part === "" || QUERY_SEPARATOR.test(part))
4368
+ return part;
4369
+ const equals = part.indexOf("=");
4370
+ if (equals === -1)
4371
+ return REDACTED;
4372
+ const name2 = part.slice(0, equals);
4373
+ const plain = decodedName(name2);
4374
+ return plain === null || SENSITIVE_NAME.test(plain) ? `${name2}=${REDACTED}` : part;
4375
+ }
4376
+ function filterUrl(url) {
4377
+ if (!URL.canParse(url))
4378
+ return UNREADABLE_URL;
4379
+ const parsed = new URL(url);
4380
+ if (!READ_PROTOCOLS.includes(parsed.protocol))
4381
+ return UNREADABLE_URL;
4382
+ const parts = parsed.search.slice(1).split(QUERY_SEPARATOR).map(filterQueryPart);
4383
+ const query = parsed.search === "" ? "" : `?${parts.join("")}`;
4384
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}${query}`;
4385
+ }
4386
+ function filterHeaders(headers) {
4387
+ return Object.freeze(Object.fromEntries(Object.entries(headers).map(([name2, value]) => [name2.toLowerCase(), value]).filter(([name2]) => API_HEADER_ALLOW_LIST.includes(name2))));
4388
+ }
4389
+ function scrubUrls(text) {
4390
+ return text.replace(URL_IN_TEXT, (url) => filterUrl(url));
4391
+ }
4392
+ function filterExchange(exchange) {
4393
+ const optional = Object.fromEntries(Object.entries({
4394
+ status: exchange.status,
4395
+ durationMs: exchange.durationMs,
4396
+ requestBytes: exchange.requestBytes,
4397
+ responseBytes: exchange.responseBytes,
4398
+ requestId: exchange.requestId,
4399
+ headers: exchange.headers === undefined ? undefined : filterHeaders(exchange.headers)
4400
+ }).filter(([, value]) => value !== undefined));
4401
+ return Object.freeze({
4402
+ service: exchange.service,
4403
+ method: exchange.method,
4404
+ url: filterUrl(exchange.url),
4405
+ ...optional
4406
+ });
4407
+ }
4408
+ function exchangeLine(exchange) {
4409
+ const outcome = exchange.status === undefined ? "no response" : String(exchange.status);
4410
+ const took = exchange.durationMs === undefined ? "" : ` in ${Math.round(exchange.durationMs)} ms`;
4411
+ return `${exchange.service} ${exchange.method.toUpperCase()} ${filterUrl(exchange.url)} \u2192 ${outcome}${took}`;
4412
+ }
4413
+
4414
+ // src/adapters/logger/colour.ts
4415
+ var LEVEL_CODES = Object.freeze({
4416
+ error: 31,
4417
+ warn: 33,
4418
+ debug: 2,
4419
+ api: 36
4420
+ });
4421
+ var RESET = "\x1B[0m";
4422
+ function paint(level, text, theme, colour) {
4423
+ const code = LEVEL_CODES[level];
4424
+ return colour && theme === "default" && code !== undefined ? `\x1B[${code}m${text}${RESET}` : text;
4425
+ }
4426
+
4427
+ // src/adapters/logger/console.ts
4428
+ var FRAME_LOCATION = /\(?([^()\s]+):(\d+):\d+\)?$/;
4429
+ var NATIVE_FRAME = "native";
4430
+ var FRAME_OPENING = "at ";
4431
+ var API_EVENT_LEVEL = "debug";
4432
+ function labelOf(entry) {
4433
+ return [entry.module, entry.action].filter((part) => part !== undefined && part !== "").join(" ");
4434
+ }
4435
+ function messageIn(entry) {
4436
+ return entry.level === "api" ? scrubUrls(entry.message) : entry.message;
4437
+ }
4438
+ function bodyOf2(entry) {
4439
+ const message = messageIn(entry);
4440
+ if (entry.api === undefined)
4441
+ return message;
4442
+ return message === "" ? exchangeLine(entry.api) : `${exchangeLine(entry.api)} \u2014 ${message}`;
4443
+ }
4444
+ function errorIn(entry) {
4445
+ if (entry.error === undefined)
4446
+ return;
4447
+ return entry.level === "api" ? scrubUrls(messageOf(entry.error)) : messageOf(entry.error);
4448
+ }
4449
+ function writableAsJson(value) {
4450
+ try {
4451
+ JSON.stringify(value);
4452
+ return true;
4453
+ } catch {
4454
+ return false;
4455
+ }
4456
+ }
4457
+ function labelled(entry) {
4458
+ const label = labelOf(entry);
4459
+ return label === "" ? bodyOf2(entry) : `${label}: ${bodyOf2(entry)}`;
4460
+ }
4461
+ function textLine(entry, site, theme, colour) {
4462
+ const code = entry.code === undefined ? "" : ` [${entry.code}]`;
4463
+ const where = site === null ? "" : ` (${site})`;
4464
+ return `${paint(entry.level, `${entry.level}:`, theme, colour)} ${labelled(entry)}${code}${where}`;
4465
+ }
4466
+ function fieldsOf(entry, site) {
4467
+ const dataWritable = entry.data === undefined || writableAsJson(entry.data);
4468
+ const known = [
4469
+ ["type", entry.level === "api" ? "api" : undefined],
4470
+ ["module", entry.module],
4471
+ ["action", entry.action],
4472
+ ["code", entry.code],
4473
+ ["hint", entry.hint],
4474
+ ["data", dataWritable ? entry.data : undefined],
4475
+ ["dataUnwritable", dataWritable ? undefined : true],
4476
+ ["error", errorIn(entry)],
4477
+ ["api", entry.api === undefined ? undefined : filterExchange(entry.api)],
4478
+ ["site", site ?? undefined]
4479
+ ].filter(([, value]) => value !== undefined);
4480
+ return known.length === 0 ? null : Object.freeze(Object.fromEntries(known));
4481
+ }
4482
+ function eventOf(entry, site) {
4483
+ const fields = fieldsOf(entry, site);
4484
+ return {
4485
+ type: "log",
4486
+ level: entry.level === "api" ? API_EVENT_LEVEL : entry.level,
4487
+ message: labelled(entry),
4488
+ ...fields === null ? {} : { fields },
4489
+ ts: new Date().toISOString()
4490
+ };
4491
+ }
4492
+ function callSiteOf(below) {
4493
+ const holder = {};
4494
+ Error.captureStackTrace(holder, below);
4495
+ const located = (holder.stack ?? "").split(`
4496
+ `).map((line) => line.trim()).filter((line) => line.startsWith(FRAME_OPENING)).map((line) => FRAME_LOCATION.exec(line)).find((match) => match !== null && match[1] !== NATIVE_FRAME);
4497
+ return located === undefined || located === null ? null : `${basename4(located[1] ?? "")}:${located[2] ?? ""}`;
4498
+ }
4499
+ function write(env, entry, settings, site) {
4500
+ const output = env.output();
4501
+ if (env.mode() !== "text") {
4502
+ output.emit(eventOf(entry, site));
4503
+ return;
4504
+ }
4505
+ output.info(textLine(entry, site, settings.theme, env.colour));
4506
+ if (entry.hint !== undefined)
4507
+ output.info(` hint: ${entry.hint}`);
4508
+ }
4509
+ function definedOf(entry) {
4510
+ return Object.fromEntries(Object.entries(entry).filter(([, value]) => value !== undefined));
4511
+ }
4512
+ function loggerWith(env, bindings) {
4513
+ const log = (given) => {
4514
+ const entry = { ...bindings, ...definedOf(given) };
4515
+ const settings = env.settings();
4516
+ if (!levelEnabled(entry.level, entry.module, settings, env.verbosity))
4517
+ return;
4518
+ const site = settings.callSite && entry.level === "debug" ? callSiteOf(log) : null;
4519
+ write(env, entry, settings, site);
4520
+ };
4521
+ return Object.freeze({
4522
+ log,
4523
+ child: (more) => loggerWith(env, { ...bindings, ...more }),
4524
+ enabled: (level, module = bindings.module) => levelEnabled(level, module, env.settings(), env.verbosity)
4525
+ });
4526
+ }
4527
+ function createConsoleLogger(options = {}) {
4528
+ return loggerWith({
4529
+ verbosity: options.verbosity ?? 0,
4530
+ colour: options.colour ?? false,
4531
+ settings: options.settings ?? activeLoggerSettings,
4532
+ output: options.output ?? activeOutput,
4533
+ mode: options.mode ?? activeOutputMode
4534
+ }, {});
4535
+ }
4536
+
4759
4537
  // src/adapters/output/events.ts
4760
4538
  var EVENT_PREFIX = "rafa\xB7 ";
4761
4539
  var KIND_COLUMN = 17;
@@ -4825,9 +4603,9 @@ function createJsonOutput({
4825
4603
  }
4826
4604
 
4827
4605
  // src/adapters/planner/claude.ts
4828
- import { existsSync as existsSync13 } from "fs";
4606
+ import { existsSync as existsSync10 } from "fs";
4829
4607
  import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
4830
- import { posix, resolve as resolve6 } from "path";
4608
+ import { posix, resolve as resolve5 } from "path";
4831
4609
 
4832
4610
  // src/board/spec-review.ts
4833
4611
  var SPEC_REVIEW_KIND = "spec-review";
@@ -4992,10 +4770,36 @@ function parseSpecReview(output) {
4992
4770
  return judged(verdict, readGaps(document, issues), block, issues);
4993
4771
  }
4994
4772
 
4773
+ // src/cli/spends.ts
4774
+ var SPENDS_GLYPH = "\uD83E\uDE99";
4775
+
4776
+ // src/cli/command.ts
4777
+ var MAX_EXIT_CODE = 255;
4778
+
4779
+ class CommandExit extends Error {
4780
+ exitCode;
4781
+ constructor(exitCode, message = "") {
4782
+ super(message);
4783
+ if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > MAX_EXIT_CODE) {
4784
+ throw new TypeError(`CommandExit: exit code is ${describeValue(exitCode)}, expected a whole number from 0 to ${MAX_EXIT_CODE}`);
4785
+ }
4786
+ this.name = "CommandExit";
4787
+ this.exitCode = exitCode;
4788
+ }
4789
+ }
4790
+ function isTopLevel(command) {
4791
+ return command.subject === command.action;
4792
+ }
4793
+ function commandSpelling(command) {
4794
+ return isTopLevel(command) ? command.subject : `${command.subject} ${command.action}`;
4795
+ }
4796
+
4995
4797
  // src/cli/running.ts
4798
+ import { AsyncLocalStorage } from "async_hooks";
4799
+ var scopes = new AsyncLocalStorage;
4996
4800
  var running = null;
4997
4801
  function runningCommand() {
4998
- return running;
4802
+ return scopes.getStore() ?? running;
4999
4803
  }
5000
4804
 
5001
4805
  // src/utils/session-env.ts
@@ -5087,12 +4891,12 @@ function runClaude(prompt, settingSources, flags = [], spawn = spawnClaude, serv
5087
4891
  function cwdOption(options) {
5088
4892
  return options.cwd === undefined ? {} : { cwd: options.cwd };
5089
4893
  }
5090
- function writeWholeLines(text, write) {
4894
+ function writeWholeLines(text, write2) {
5091
4895
  const lines = text.split(`
5092
4896
  `);
5093
4897
  const rest = lines.pop() ?? "";
5094
4898
  for (const line of lines)
5095
- write(line);
4899
+ write2(line);
5096
4900
  return rest;
5097
4901
  }
5098
4902
  async function teeToOperator(stream) {
@@ -5163,24 +4967,26 @@ class ClaudePlannerError extends Error {
5163
4967
  }
5164
4968
  function createClaudePlanner(options) {
5165
4969
  const { repoRoot, planDir, settingSources, buildPrompt, spawn = spawnClaudeCaptured } = options;
5166
- const root = resolve6(repoRoot);
4970
+ const root = resolve5(repoRoot);
5167
4971
  const create = async ({ specPath, stub }) => {
5168
4972
  const planPath = planFilePath(planDir, `PLAN-${stub}.md`);
5169
4973
  const prerequisitesPath = planFilePath(planDir, `PREREQUISITES-${stub}.md`);
5170
- const isWritten = (path) => existsSync13(resolve6(root, path));
4974
+ const isWritten = (path) => existsSync10(resolve5(root, path));
5171
4975
  if (isWritten(planPath)) {
5172
4976
  throw new ClaudePlannerError(`${planPath} already exists`, FAILURE_EXIT_CODE);
5173
4977
  }
5174
- const specContent = await readFile2(resolve6(root, specPath), "utf8");
5175
- await mkdir2(resolve6(root, planDir), { recursive: true });
5176
- const session = await spawn(claudeArgs(settingSources), buildPrompt(specContent, stub), { cwd: root });
4978
+ const specContent = await readFile2(resolve5(root, specPath), "utf8");
4979
+ const absolutePlanDir = resolve5(root, planDir);
4980
+ await mkdir2(absolutePlanDir, { recursive: true });
4981
+ const prompt = buildPrompt(specContent, stub, absolutePlanDir);
4982
+ const session = await spawn(claudeArgs(settingSources), prompt, { cwd: root });
5177
4983
  const review = parseSpecReview(session.stdout);
5178
4984
  const { exitCode } = session;
5179
4985
  if (exitCode !== 0) {
5180
4986
  throw new ClaudePlannerError(`Plan generation failed (exit ${exitCode}).`, exitCode, review);
5181
4987
  }
5182
4988
  if (!isWritten(planPath)) {
5183
- throw new ClaudePlannerError(`The session finished but ${planPath} was not created under ${root} \u2014 inspect the output above.`, FAILURE_EXIT_CODE, review);
4989
+ throw new ClaudePlannerError(`The session finished but ${planPath} was not created under ${root}: no file at` + ` ${resolve5(root, planPath)} \u2014 inspect the output above.`, FAILURE_EXIT_CODE, review);
5184
4990
  }
5185
4991
  return {
5186
4992
  planPath,
@@ -5285,8 +5091,11 @@ var GITHUB_LABELS = Object.freeze({
5285
5091
  priorityPrefix: "priority:",
5286
5092
  needsTriage: "needs-triage",
5287
5093
  blockedByPrefix: "blocked-by:OPT-",
5288
- specBlocked: "spec:blocked"
5094
+ specBlocked: "spec:blocked",
5095
+ causePrefix: "cause:"
5289
5096
  });
5097
+ var GITHUB_LABEL_MAX_LENGTH = 50;
5098
+ var CAUSE_MAX_LENGTH = GITHUB_LABEL_MAX_LENGTH - GITHUB_LABELS.causePrefix.length;
5290
5099
  var VIEW_FIELDS = "number,title,body,state,stateReason,labels,url";
5291
5100
  var LIST_COMMAND = "gh issue list";
5292
5101
  var LIST_FIELDS = "number,url,labels";
@@ -5344,8 +5153,8 @@ async function withDeadline(work, timeoutMs) {
5344
5153
  if (timeoutMs === undefined)
5345
5154
  return work;
5346
5155
  let timer;
5347
- const deadline = new Promise((resolve7) => {
5348
- timer = setTimeout(() => resolve7(TIMED_OUT), timeoutMs);
5156
+ const deadline = new Promise((resolve6) => {
5157
+ timer = setTimeout(() => resolve6(TIMED_OUT), timeoutMs);
5349
5158
  });
5350
5159
  try {
5351
5160
  return await Promise.race([work, deadline]);
@@ -5372,6 +5181,9 @@ function isRepoName(value) {
5372
5181
  function isLabelPart(value) {
5373
5182
  return typeof value === "string" && !value.includes(",");
5374
5183
  }
5184
+ function isCauseLabelPart(value) {
5185
+ return value === undefined || isLabelPart(value) && value.length <= CAUSE_MAX_LENGTH;
5186
+ }
5375
5187
  function optionalFlag2(name2, value) {
5376
5188
  return value === undefined ? [] : [name2, value];
5377
5189
  }
@@ -5395,7 +5207,8 @@ var DRAFT_CHECKS = [
5395
5207
  (value) => Array.isArray(value) && value.every((item) => Number.isSafeInteger(item) && item > 0),
5396
5208
  "a list of positive whole numbers"
5397
5209
  ],
5398
- ["specBlocked", (value) => value === undefined || typeof value === "boolean", "absent or a boolean"]
5210
+ ["specBlocked", (value) => value === undefined || typeof value === "boolean", "absent or a boolean"],
5211
+ ["cause", isCauseLabelPart, `absent or a string holding no comma, at most ${CAUSE_MAX_LENGTH} characters`]
5399
5212
  ];
5400
5213
  function draftProblem(draft) {
5401
5214
  if (!isMapping(draft))
@@ -5410,6 +5223,7 @@ function labelsFor(draft) {
5410
5223
  `${GITHUB_LABELS.typePrefix}${draft.type}`,
5411
5224
  ...draft.blockedBy.map((opt) => `${GITHUB_LABELS.blockedByPrefix}${opt}`),
5412
5225
  ...draft.specBlocked === true ? [GITHUB_LABELS.specBlocked] : [],
5226
+ ...draft.cause === undefined ? [] : [`${GITHUB_LABELS.causePrefix}${draft.cause}`],
5413
5227
  priority
5414
5228
  ];
5415
5229
  }
@@ -5724,6 +5538,7 @@ var FIELD_CHECKS = {
5724
5538
  title: [isString2, "a string"],
5725
5539
  blockedBy: [listOf(isFiniteNumber), "a list of numbers"],
5726
5540
  specBlocked: [orAbsent(isBoolean), "absent or a boolean"],
5541
+ cause: [orAbsent(isString2), "absent or a string"],
5727
5542
  state: [oneOf2(ISSUE_STATES), `one of: ${ISSUE_STATES.join(", ")}`],
5728
5543
  fallbackReason: [orNull(isString2), "a string or null"],
5729
5544
  capturedAt: [isString2, "a string"],
@@ -5780,6 +5595,7 @@ function renderLocalIssue(record) {
5780
5595
  title: draft.title,
5781
5596
  blockedBy: draft.blockedBy,
5782
5597
  ...draft.specBlocked === undefined ? {} : { specBlocked: draft.specBlocked },
5598
+ ...draft.cause === undefined ? {} : { cause: draft.cause },
5783
5599
  state: record.state,
5784
5600
  fallbackReason: record.fallbackReason,
5785
5601
  capturedAt: record.capturedAt,
@@ -5822,7 +5638,8 @@ function parseLocalIssue(contents, sourcePath) {
5822
5638
  priority: fields.priority,
5823
5639
  project: fields.project,
5824
5640
  blockedBy: fields.blockedBy,
5825
- ...fields.specBlocked === undefined ? {} : { specBlocked: fields.specBlocked }
5641
+ ...fields.specBlocked === undefined ? {} : { specBlocked: fields.specBlocked },
5642
+ ...fields.cause === undefined ? {} : { cause: fields.cause }
5826
5643
  },
5827
5644
  fallbackReason: fields.fallbackReason,
5828
5645
  capturedAt: fields.capturedAt,
@@ -5986,7 +5803,8 @@ var PORT_VERSIONS = Object.freeze({
5986
5803
  learning: 1,
5987
5804
  output: 1,
5988
5805
  planner: 1,
5989
- sync: 1
5806
+ sync: 1,
5807
+ logger: 1
5990
5808
  });
5991
5809
  var SERVED_VERSIONS = new Map(Object.entries(PORT_VERSIONS));
5992
5810
  var PORT_TYPE_LIST = [...SERVED_VERSIONS.keys()].join(", ");
@@ -6025,7 +5843,8 @@ function indexByPort(adapters) {
6025
5843
  learning: only("learning"),
6026
5844
  output: only("output"),
6027
5845
  planner: only("planner"),
6028
- sync: only("sync")
5846
+ sync: only("sync"),
5847
+ logger: only("logger")
6029
5848
  };
6030
5849
  }
6031
5850
  function createAdapterRegistry(adapters = []) {
@@ -6079,6 +5898,12 @@ var CORE_ADAPTERS = [
6079
5898
  portVersion: PORT_VERSIONS.output,
6080
5899
  create: ({ stream = process.stdout }) => createEventsOutput({ stream })
6081
5900
  },
5901
+ {
5902
+ port: "logger",
5903
+ kind: "console",
5904
+ portVersion: PORT_VERSIONS.logger,
5905
+ create: ({ verbosity = 0, colour = false }) => createConsoleLogger({ verbosity, colour })
5906
+ },
6082
5907
  {
6083
5908
  port: "tracker",
6084
5909
  kind: "local",
@@ -6261,65 +6086,11 @@ function requireTrustedAuthor(item, reading) {
6261
6086
  return;
6262
6087
  throw new CommandExit(TRUST_REFUSAL_EXIT, trustRefusalMessage(item, reading));
6263
6088
  }
6264
- var USER_COMMAND = "gh api user";
6265
- function noLogin(detail) {
6266
- return { login: null, detail };
6267
- }
6268
- function createGhEditorLogin(options) {
6269
- const { gh } = options;
6270
- return async () => {
6271
- const result = await gh(["api", "user"]);
6272
- if (!result.ok)
6273
- return noLogin(detailOf2(result, USER_COMMAND));
6274
- let payload;
6275
- try {
6276
- payload = JSON.parse(result.stdout);
6277
- } catch (error) {
6278
- return noLogin(`${USER_COMMAND} wrote output that is not JSON: ${messageOf(error)}`);
6279
- }
6280
- if (!isMapping(payload)) {
6281
- return noLogin(`${USER_COMMAND} answered ${describeValue(payload)}, expected a mapping`);
6282
- }
6283
- const login = payload["login"];
6284
- return typeof login === "string" && isGitHubLogin(login) ? { login, detail: "" } : noLogin(`${USER_COMMAND} answered ${describeValue(login)} as its login, which is not a GitHub login`);
6285
- };
6286
- }
6287
- async function readEditorTrust(trust, editor) {
6288
- const read = await editor();
6289
- if (read.login === null) {
6290
- return { login: null, trusted: false, refusal: "ownership-unknown", reading: null, detail: read.detail };
6291
- }
6292
- const reading = await readBoardTrust(trust, read.login);
6293
- return {
6294
- login: read.login,
6295
- trusted: reading.trusted,
6296
- refusal: editorRefusalOf(reading),
6297
- reading,
6298
- detail: ""
6299
- };
6300
- }
6301
- function editorRefusalOf(reading) {
6302
- if (reading.trusted)
6303
- return null;
6304
- return reading.refusal === "lookup-failed" ? "ownership-unknown" : "ownership";
6305
- }
6306
- function editorRefusalMessage(item, reading) {
6307
- const what = `${item.kind} #${String(item.number)}`;
6308
- if (reading.reading !== null) {
6309
- const because = trustRefusalClause(item.repo, reading.reading);
6310
- return `${what} cannot be edited by ${reading.reading.login}, ${because}; a member must make the edit`;
6311
- }
6312
- if (reading.trusted) {
6313
- throw new TypeError("board trust: a trusted editor has no refusal to name");
6314
- }
6315
- const detail = reading.detail === "" ? UNREPORTED : reading.detail;
6316
- return `${what} cannot be edited: the account running the edit could not be read (${detail}); ` + "log gh in with `gh auth login` and run it again";
6317
- }
6318
6089
  // src/effort/sync/wire.ts
6319
- import { existsSync as existsSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
6090
+ import { existsSync as existsSync11, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
6320
6091
  import { tmpdir as tmpdir4 } from "os";
6321
6092
  import { join as join17 } from "path";
6322
- import { Database as Database7, SQLiteError as SQLiteError3 } from "bun:sqlite";
6093
+ import { Database as Database6, SQLiteError as SQLiteError3 } from "bun:sqlite";
6323
6094
  var WIRE_FORMAT = "rafa-effort-wire";
6324
6095
  var WIRE_VERSION = 1;
6325
6096
 
@@ -6479,9 +6250,9 @@ function exportWirePayload(options) {
6479
6250
  const { path } = options;
6480
6251
  const cursor = checkCursor(options.cursor ?? {});
6481
6252
  guardTestProcess(path);
6482
- if (!existsSync14(path))
6253
+ if (!existsSync11(path))
6483
6254
  throw new WireExportRefusal(`wire: ${path} is not there`);
6484
- const db = new Database7(path, { readonly: true, safeIntegers: true });
6255
+ const db = new Database6(path, { readonly: true, safeIntegers: true });
6485
6256
  try {
6486
6257
  db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
6487
6258
  return db.transaction(() => readPayload(db, path, cursor))();
@@ -6520,7 +6291,7 @@ function insertRows(db, table, rows) {
6520
6291
  }
6521
6292
  function buildStore(path, payload, options) {
6522
6293
  const migrations = namedMigrations(payload.migrations, options.migrations ?? SQLITE_MIGRATIONS);
6523
- const db = new Database7(path, { create: true, readwrite: true });
6294
+ const db = new Database6(path, { create: true, readwrite: true });
6524
6295
  try {
6525
6296
  bringForward(db, path, "write", "open", {
6526
6297
  migrations,
@@ -6568,4 +6339,4 @@ function selectEffortStore(repoRoot, config) {
6568
6339
  return adapter.create({ repoRoot });
6569
6340
  }
6570
6341
 
6571
- 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, RECORD_EXTENSION, SessionRecordError, isSessionId, isPositiveWhole, sessionDecisions, sessionPhase, parseSessionRecord, SessionConflictError, SessionStateError, runsDir, sessionFilePath, errorCode, isPidAlive, readState, readSessions, readSession, samePlan, beginSession, updateSession, DEVELOPMENT_NEXT_STEP, DEVELOPMENT_MARK, DevelopmentBuildRefusedError, refuseUnownedDevelopmentWrite, LEGACY_GATE_OPEN, LEGACY_GATE_CLOSED, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, SchemaRefusedError, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, activeStoreSettings, normalizeRemote, gitRemoteUrl, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, EffortCopyRefusal, EffortCopyFailure, copyEffortStore, RebuildRefusal, readConfigFile, loadConfig, plansDirAt, requireProject, resolvePlansDir, resolveProjectConfig, countTasks, formatCounts, checkbox, planFileName, stubOfPlanFile, isFile, issueLine, plural, expectNoArgument, expectOneArgument, expectAtMostOneArgument, readSwitch, fileStamp, keepsIdLine, fix_schema_default, minutesBetween, readCommitLog, UnionSchemaMismatch, migrateStore, MOVE_TO_SQLITE, MergeRefusal, mergeStore, createFileSync, SyncModuleMissing, selectSync, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, renderFrontmatter, mergeFrontmatter, writeFrontmatter, ACTION_HEADING, CAUSE_HEADING, parseInstinct, writeInstinct, readScopes, allRecords, findRecords, instinctProject, toHeldRecords, EVENT_PREFIX, padKind, oneLine, sessionSpawnEnv, CLAUDE_BIN, claudeArgs, interruptClaudeSessions, spawnClaude, runClaude, spawnClaudeCaptured, runClaudeCaptured, planFilePath, ClaudePlannerError, ISSUE_TYPES, ISSUE_PRIORITIES, ISSUE_STATES, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createGithubTracker, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, TRUSTED_PERMISSIONS, TRUST_REFUSAL_EXIT, createGhPermissions, readAuthorTrust, ghBoardTrust, readBoardTrust, requireTrustedBoardAuthor, trustRefusalClause, trustRefusalMessage, requireTrustedAuthor, createGhEditorLogin, readEditorTrust, editorRefusalMessage, WIRE_FORMAT, WIRE_VERSION, WireFormatError, WireExportRefusal, encodeWirePayload, decodeWirePayload, exportWirePayload, materialiseWirePayload, selectEffortStore };
6342
+ export { EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, version, RAFA_VERSION, createGitRunner, gitSaid, NOT_A_REPOSITORY, mainCheckoutOf, SCOPE_DIR, INIT_COMMAND, DISK_FILE_SYSTEM, ScopeError, scopeAt, initHint, selfAndAncestors, resolveScope, positionFilePath, readPositionFile, writePositionFile, positionAt, goHome, hopFilePath, readHopRecord, writeHopRecord, staleAgainst, SessionRecordError, isSessionId, sessionSteps, sessionDecisions, SessionConflictError, runsDir, sessionFilePath, errorCode, isPidAlive, readSessions, readSession, samePlan, beginSession, updateSession, DEVELOPMENT_NEXT_STEP, DevelopmentBuildRefusedError, refuseUnownedDevelopmentWrite, SQLITE_MIGRATIONS, sqliteCatalogue, planSchema, readStoreSchema, writeNames, ACROSS_STORES_ORDER, STAMPED_COLUMNS, stampedValues, setActiveStoreSettings, activeStoreSettings, normalizeRemote, gitRemoteUrl, readReplacedStoreIds, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, minutesBetween, readCommitLog, MOVE_TO_SQLITE, mergeStore, SyncModuleMissing, selectSync, readFrontmatterDocument, readFrontmatter, mergeFrontmatter, writeFrontmatter, parseInstinct, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readScopes, createConsoleLogger, padKind, oneLine, SPENDS_GLYPH, CommandExit, commandSpelling, interruptClaudeSessions, spawnClaude, runClaude, runClaudeCaptured, planFilePath, ClaudePlannerError, GITHUB_LABELS, createGhRunner, typeOfLabels, moduleOfLabels, createLocalTracker, PORT_VERSIONS, createAdapterRegistry, CORE_ADAPTER_REGISTRY, TRUSTED_PERMISSIONS, ghBoardTrust, readBoardTrust, requireTrustedBoardAuthor, trustRefusalClause, WIRE_FORMAT, WIRE_VERSION, WireFormatError, WireExportRefusal, encodeWirePayload, decodeWirePayload, exportWirePayload, materialiseWirePayload, selectEffortStore };