@open-tomato/rafa 0.42.0 → 0.44.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.
@@ -4,7 +4,6 @@ import {
4
4
  BUSY_TIMEOUT_MIN_MS,
5
5
  CONFIG_DEFAULTS,
6
6
  CONFIG_FILE,
7
- ConfigError,
8
7
  FINDING_DOMAINS,
9
8
  FINDING_KINDS,
10
9
  FINDING_SIGNALS,
@@ -12,6 +11,7 @@ import {
12
11
  STORE_BACKENDS,
13
12
  SYNC_STRATEGIES,
14
13
  TEST_SCOPES,
14
+ activeLoggerSettings,
15
15
  activeOutput,
16
16
  activeOutputMode,
17
17
  bundledBinDirectory,
@@ -23,13 +23,12 @@ import {
23
23
  isGitHubLogin,
24
24
  isMapping,
25
25
  isStampableStub,
26
+ levelEnabled,
26
27
  messageOf,
27
- parseConfigText,
28
28
  readRafaBlocks,
29
- resolveConfig,
30
29
  resolveInstinctScopes,
31
30
  tierExists
32
- } from "./index-bceachy6.js";
31
+ } from "./index-cknayrpc.js";
33
32
  import {
34
33
  bless,
35
34
  merge
@@ -253,7 +252,7 @@ import { Database as Database2 } from "bun:sqlite";
253
252
 
254
253
  // package.json
255
254
  var name = "@open-tomato/rafa";
256
- var version = "0.42.0";
255
+ var version = "0.44.0";
257
256
 
258
257
  // src/cli/version.ts
259
258
  var RAFA_VERSION = version;
@@ -832,8 +831,37 @@ function freezeHop(value) {
832
831
  const hop = asHopRecord(value);
833
832
  return Object.freeze({ ...hop, home: Object.freeze(hop.home), from: Object.freeze(hop.from) });
834
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]) } : {};
837
+ }
835
838
  function freezeFailures(failures) {
836
- 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
+ }))) };
837
865
  }
838
866
  function isSessionStepReason(value) {
839
867
  return SESSION_STEP_REASONS.includes(value);
@@ -851,6 +879,7 @@ function freezeStep(step) {
851
879
  summary: step.summary,
852
880
  failures: freezeFailures(step.failures),
853
881
  newFailures: freezeFailures(step.newFailures),
882
+ ...stepOnlyEntry(field(step, "stepOnly")),
854
883
  ...step.interrupted === true ? { interrupted: true } : {}
855
884
  });
856
885
  }
@@ -1542,6 +1571,16 @@ var SQLITE_MIGRATIONS = [
1542
1571
  sql: `
1543
1572
  ALTER TABLE sessions ADD COLUMN worktree TEXT;
1544
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
+ `
1545
1584
  }
1546
1585
  ];
1547
1586
  var SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.length;
@@ -1940,8 +1979,33 @@ function readProjectIdentity(dir, git = createGitRunner(dir)) {
1940
1979
  return { rootCommit: smallestRootCommit(git), remote: normalisedRemote(git) };
1941
1980
  }
1942
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
+
1943
2006
  // src/effort/store/store-meta.ts
1944
2007
  var STORED_INTEGER_BITS = 64;
2008
+ var COPY_REASONS = ["host", "path"];
1945
2009
  function toStoredInteger(value) {
1946
2010
  return BigInt.asIntN(STORED_INTEGER_BITS, value);
1947
2011
  }
@@ -1976,12 +2040,20 @@ function projectFor(storePath, recorded, seams) {
1976
2040
  return null;
1977
2041
  return { rootCommit: recorded.projectRootCommit, remote: recorded.projectRemote };
1978
2042
  }
1979
- function writeStoreMeta(db, facts, project, generation, seams) {
2043
+ function writeStoreMeta(db, facts, project, generation, mintedAt, seams) {
1980
2044
  const storeId = (seams.newStoreId ?? randomUUID4)();
1981
- const mintedAt = (seams.now?.() ?? new Date).toISOString();
1982
- 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);
1983
2046
  return storeId;
1984
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
+ }
1985
2057
  function settleUnderLock(db, observe, prefetched, seams) {
1986
2058
  const current = readStoreMeta(db);
1987
2059
  const facts = observe();
@@ -1997,7 +2069,9 @@ function settleUnderLock(db, observe, prefetched, seams) {
1997
2069
  const project = prefetched ?? projectFor(facts.storePath, current, seams);
1998
2070
  if (project === null)
1999
2071
  return { action: "no-project", reasons: decision.reasons };
2000
- 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);
2001
2075
  writeStoreGeneration(facts.storePath, generation);
2002
2076
  return { action: "mint", storeId, reasons: decision.reasons };
2003
2077
  }
@@ -2193,40 +2267,14 @@ function openSqliteStore(repoRoot) {
2193
2267
  }
2194
2268
 
2195
2269
  // src/effort/sync/file.ts
2196
- import { existsSync as existsSync11 } from "fs";
2197
- import { join as join13, resolve as resolve5 } from "path";
2270
+ import { existsSync as existsSync9 } from "fs";
2271
+ import { join as join12, resolve as resolve4 } from "path";
2198
2272
 
2199
- // src/cli/spends.ts
2200
- var SPENDS_GLYPH = "\uD83E\uDE99";
2201
-
2202
- // src/cli/command.ts
2203
- var MAX_EXIT_CODE = 255;
2204
-
2205
- class CommandExit extends Error {
2206
- exitCode;
2207
- constructor(exitCode, message = "") {
2208
- super(message);
2209
- if (!Number.isInteger(exitCode) || exitCode < 0 || exitCode > MAX_EXIT_CODE) {
2210
- throw new TypeError(`CommandExit: exit code is ${describeValue(exitCode)}, expected a whole number from 0 to ${MAX_EXIT_CODE}`);
2211
- }
2212
- this.name = "CommandExit";
2213
- this.exitCode = exitCode;
2214
- }
2215
- }
2216
- function isTopLevel(command) {
2217
- return command.subject === command.action;
2218
- }
2219
- function commandSpelling(command) {
2220
- return isTopLevel(command) ? command.subject : `${command.subject} ${command.action}`;
2273
+ // src/effort/file-stamp.ts
2274
+ function fileStamp(date) {
2275
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
2221
2276
  }
2222
2277
 
2223
- // src/effort/store/fix-schema.ts
2224
- import { existsSync as existsSync8 } from "fs";
2225
- import { Database as Database4 } from "bun:sqlite";
2226
-
2227
- // src/effort/store/rebuild-aside.ts
2228
- import { closeSync, existsSync as existsSync7, fsyncSync, openSync, renameSync as renameSync5, rmSync as rmSync6 } from "fs";
2229
-
2230
2278
  // src/effort/store/copy.ts
2231
2279
  import { copyFileSync, constants, existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync2, rmSync as rmSync5, statSync as statSync2 } from "fs";
2232
2280
  import { join as join10, resolve as resolve3 } from "path";
@@ -2316,7 +2364,18 @@ function copyEffortStore(request) {
2316
2364
  return Object.freeze({ source, directory: target, files: Object.freeze(files) });
2317
2365
  }
2318
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
+
2319
2377
  // src/effort/store/rebuild-aside.ts
2378
+ import { closeSync, existsSync as existsSync7, fsyncSync, openSync, renameSync as renameSync5, rmSync as rmSync6 } from "fs";
2320
2379
  class RebuildRefusal extends Error {
2321
2380
  constructor(message, options) {
2322
2381
  super(message, options);
@@ -2447,22 +2506,10 @@ function rebuildAside(options) {
2447
2506
  swapIn(path, parallelPath, backupPath);
2448
2507
  return { built, backupPath };
2449
2508
  }
2450
-
2451
2509
  // src/effort/store/fix-schema.ts
2452
- var REPAIRABLE_REASONS = [
2453
- "pre-log-unreleased",
2454
- "gate-mismatch",
2455
- "edited",
2456
- "unknown-breaks-readers",
2457
- "unknown-breaks-writers"
2458
- ];
2459
- var LIVE = "live";
2460
2510
  function tableNames(db, schema) {
2461
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);
2462
2512
  }
2463
- function columnNames(db, schema, table) {
2464
- return db.query("SELECT name FROM pragma_table_info(?, ?)").all(table, schema).map((row) => row.name);
2465
- }
2466
2513
  function readOnlySchema(path) {
2467
2514
  const db = new Database4(path, { readonly: true });
2468
2515
  try {
@@ -2471,38 +2518,6 @@ function readOnlySchema(path) {
2471
2518
  db.close();
2472
2519
  }
2473
2520
  }
2474
- function refuseRemoved(db, known, live) {
2475
- for (const table of known) {
2476
- if (!live.has(table)) {
2477
- throw new RebuildRefusal(`the store holds no table ${table}, which this rafa writes; its newer schema is not additive`);
2478
- }
2479
- const liveColumns = new Set(columnNames(db, LIVE, table));
2480
- const missing = columnNames(db, "main", table).filter((column) => !liveColumns.has(column));
2481
- if (missing.length > 0) {
2482
- throw new RebuildRefusal(`the store holds no column ${missing.map((column) => `${table}.${column}`).join(", ")}, which this rafa` + " writes; its newer schema is not additive");
2483
- }
2484
- }
2485
- }
2486
- function copyKnown(db, known) {
2487
- db.transaction(() => {
2488
- for (const table of known) {
2489
- const columns = columnNames(db, "main", table).map(quoted).join(", ");
2490
- db.run(`INSERT INTO main.${quoted(table)} (${columns}) SELECT ${columns} FROM ${LIVE}.${quoted(table)}`);
2491
- }
2492
- })();
2493
- }
2494
- function leftBehind(db, known, live) {
2495
- const leftTables = live.filter((table) => !known.has(table)).map((table) => ({ table, rows: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)}`) }));
2496
- const leftColumns = live.filter((table) => known.has(table)).flatMap((table) => {
2497
- const kept = new Set(columnNames(db, "main", table));
2498
- return columnNames(db, LIVE, table).filter((column) => !kept.has(column)).map((column) => ({
2499
- table,
2500
- column,
2501
- values: count(db, `SELECT count(*) AS n FROM ${LIVE}.${quoted(table)} WHERE ${quoted(column)} IS NOT NULL`)
2502
- }));
2503
- });
2504
- return { leftTables, leftColumns };
2505
- }
2506
2521
  function refuseUnusable(db, parallelPath, migrations) {
2507
2522
  const plan = planSchema(readStoreSchema(db, parallelPath), sqliteCatalogue(migrations), "write", "open");
2508
2523
  if (plan.verdict === "refuse")
@@ -2511,351 +2526,6 @@ function refuseUnusable(db, parallelPath, migrations) {
2511
2526
  if (needs.length > 0)
2512
2527
  throw new RebuildRefusal(`the rebuilt store still needs ${needs.join(", ")}`);
2513
2528
  }
2514
- function buildParallel(path, parallelPath, migrations, stamp) {
2515
- const db = new Database4(parallelPath, { create: true, readwrite: true });
2516
- try {
2517
- bringForward(db, parallelPath, "write", "open", { migrations, ...stamp, builtAside: true });
2518
- db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
2519
- const known = tableNames(db, "main");
2520
- const live = tableNames(db, LIVE);
2521
- refuseRemoved(db, known, new Set(live));
2522
- copyKnown(db, known);
2523
- const kept = checkedCounts(db, known, LIVE);
2524
- const left = leftBehind(db, new Set(known), live);
2525
- db.run(`DETACH DATABASE ${LIVE}`);
2526
- refuseCorrupt(db);
2527
- refuseUnusable(db, parallelPath, migrations);
2528
- return { kept, ...left };
2529
- } finally {
2530
- db.close();
2531
- }
2532
- }
2533
- function asidePaths(path, version2, stamp) {
2534
- return { parallelPath: `${path}.fix-${stamp}`, backupPath: `${path}.v${String(version2)}-${stamp}.bak` };
2535
- }
2536
- function isRepairable(reason) {
2537
- return REPAIRABLE_REASONS.includes(reason);
2538
- }
2539
- function refuseDevelopmentSwap(path, reason, identity) {
2540
- if (identity.kind !== "development")
2541
- return;
2542
- 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");
2543
- }
2544
- function fixStoreSchema(options) {
2545
- const { path, dryRun, stamp } = options;
2546
- guardTestProcess(path);
2547
- const migrations = options.migrations ?? SQLITE_MIGRATIONS;
2548
- const identity = options.identity ?? readRuntimeIdentity();
2549
- const appliedBy = appliedByName(identity);
2550
- const known = migrations.map(({ id }) => id);
2551
- const nothing = { known, appliedBy, pending: [], kept: [], leftTables: [], leftColumns: [], backupPath: null };
2552
- if (!existsSync8(path)) {
2553
- return { path, status: "missing", storeVersion: null, logged: false, reason: null, refusal: null, unknown: [], ...nothing };
2554
- }
2555
- refuseInFlight(path);
2556
- const store = readOnlySchema(path);
2557
- const knownIds = new Set(known);
2558
- const found = {
2559
- path,
2560
- storeVersion: store.userVersion,
2561
- logged: store.log !== null,
2562
- unknown: (store.log ?? []).filter(({ id }) => !knownIds.has(id))
2563
- };
2564
- const plan = planSchema(store, sqliteCatalogue(migrations), "write", "open");
2565
- if (plan.verdict === "use") {
2566
- const pending = writeNames(plan);
2567
- const status2 = pending.length === 0 ? "current" : "behind";
2568
- return { ...found, ...nothing, status: status2, reason: null, refusal: null, pending };
2569
- }
2570
- if (!isRepairable(plan.reason)) {
2571
- throw new RebuildRefusal(`a rebuild at this rafa's migrations does not repair ${plan.reason}. ${plan.message}`);
2572
- }
2573
- if (!dryRun)
2574
- refuseDevelopmentSwap(path, plan.reason, identity);
2575
- const { built, backupPath } = rebuildAside({
2576
- path,
2577
- ...asidePaths(path, store.userVersion, stamp),
2578
- dryRun,
2579
- build: (parallelPath) => buildParallel(path, parallelPath, migrations, {
2580
- appliedBy,
2581
- now: options.now ?? (() => new Date)
2582
- })
2583
- });
2584
- const status = dryRun ? "would-rebuild" : "rebuilt";
2585
- const refusal = withoutWayOut(plan.message, plan.nextStep);
2586
- return { ...found, ...nothing, status, reason: plan.reason, refusal, ...built, backupPath };
2587
- }
2588
- function withoutWayOut(message, nextStep) {
2589
- const ending = ` Next safe step: ${nextStep}`;
2590
- return message.endsWith(ending) ? message.slice(0, -ending.length) : message;
2591
- }
2592
-
2593
- // src/commands/plan/plan-files.ts
2594
- import { statSync as statSync3 } from "fs";
2595
- import { basename as basename4, dirname as dirname11, join as join11, resolve as resolve4 } from "path";
2596
-
2597
- // src/config-load.ts
2598
- import { existsSync as existsSync9, readFileSync as readFileSync8, realpathSync as realpathSync5 } from "fs";
2599
- import { isAbsolute as isAbsolute6 } from "path";
2600
- function readConfigFile(root) {
2601
- const path = configFilePath(root);
2602
- if (!existsSync9(path))
2603
- return null;
2604
- let text;
2605
- try {
2606
- text = readFileSync8(path, "utf8");
2607
- } catch (error) {
2608
- const problem = `${path}: cannot be read (${messageOf(error)})`;
2609
- throw new ConfigError([problem], { cause: error });
2610
- }
2611
- return parseConfigText(text, path);
2612
- }
2613
- function isSameDirectory(first, second) {
2614
- try {
2615
- return realpathSync5(first) === realpathSync5(second);
2616
- } catch {
2617
- return false;
2618
- }
2619
- }
2620
- function readUserConfigFile({ root, home }) {
2621
- if (!isAbsolute6(home)) {
2622
- throw new ConfigError([
2623
- `home directory is ${describeValue(home)}, expected an absolute path`
2624
- ]);
2625
- }
2626
- return isSameDirectory(root, home) ? null : readConfigFile(home);
2627
- }
2628
- function printWarning(message) {
2629
- activeOutput().warn(message);
2630
- }
2631
- function loadConfig(roots, cli = {}, warn = printWarning) {
2632
- const user = readUserConfigFile(roots);
2633
- const file = readConfigFile(roots.root);
2634
- const resolved = resolveConfig({ cli, file, user });
2635
- for (const warning of resolved.warnings)
2636
- warn(warning);
2637
- setActiveStoreSettings({ busyTimeoutMs: resolved.config.effortBusyTimeoutMs });
2638
- return resolved;
2639
- }
2640
-
2641
- // src/commands/plan/plan-files.ts
2642
- function plansDirAt(root, planDir) {
2643
- return { label: planDir, path: resolve4(root, planDir) };
2644
- }
2645
- function requireProject(context, command) {
2646
- if (context.project === null)
2647
- throw new Error(`${command} runs inside a project, and was handed none`);
2648
- return context.project;
2649
- }
2650
- function resolvePlansDir(project, command, warn) {
2651
- return plansDirAt(project.root, resolveProjectConfig(project, command, warn).planDir);
2652
- }
2653
- function resolveProjectConfig(project, command, warn) {
2654
- try {
2655
- return loadConfig({ root: project.root, home: project.home }, {}, warn).config;
2656
- } catch (error) {
2657
- if (!(error instanceof ConfigError))
2658
- throw error;
2659
- throw new CommandExit(1, [
2660
- `\u274C ${command}: the config cannot be used:`,
2661
- ...error.problems.map((problem) => ` ${problem}`)
2662
- ].join(`
2663
- `));
2664
- }
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 planFileName(stub, tracker) {
2675
- return tracker ? `PLAN_TRACKER-${stub}.md` : `PLAN-${stub}.md`;
2676
- }
2677
- function stubOfPlanFile(name2) {
2678
- const stub = /^PLAN-(.+)\.md$/.exec(name2)?.[1];
2679
- return stub !== undefined && isStampableStub(stub) ? stub : null;
2680
- }
2681
- function isFile(path) {
2682
- return statSync3(path, { throwIfNoEntry: false })?.isFile() ?? false;
2683
- }
2684
- var REJECTED_DIR = "rejected";
2685
- function rejectedPath(path) {
2686
- const parent = dirname11(path);
2687
- return parent === "." ? join11(REJECTED_DIR, basename4(path)) : join11(parent, REJECTED_DIR, basename4(path));
2688
- }
2689
- function issueLine(file, issue) {
2690
- return `${file}:${issue.line}: ${issue.reason}: ${issue.text}`;
2691
- }
2692
- function plural(count2, noun) {
2693
- return count2 === 1 ? `${count2} ${noun}` : `${count2} ${noun}s`;
2694
- }
2695
- function expectNoArgument(args, usage) {
2696
- if (args.length === 0)
2697
- return;
2698
- throw new CommandExit(1, `\u274C Expected no argument, got ${args.length}: ${args.join(" ")}
2699
- Usage: ${usage}`);
2700
- }
2701
- function expectOneArgument(args, usage) {
2702
- const [only] = args;
2703
- if (args.length === 1 && only !== undefined)
2704
- return only;
2705
- const got = args.length === 0 ? "none" : `${args.length}: ${args.join(" ")}`;
2706
- throw new CommandExit(1, `\u274C Expected one argument, got ${got}
2707
- Usage: ${usage}`);
2708
- }
2709
- function readSwitch(name2, value, hint) {
2710
- if (value === undefined || value === false || value === "false")
2711
- return false;
2712
- if (value === true || value === "true")
2713
- return true;
2714
- throw new CommandExit(1, `\u274C --${name2} takes no value, and read "${value}" as one. ${hint}`);
2715
- }
2716
-
2717
- // src/commands/effort/fix-schema.ts
2718
- var COMMAND_NAME = "rafa effort fix-schema";
2719
- var FIX_SCHEMA_USAGE = "rafa effort fix-schema [--dry-run]";
2720
- var DRY_RUN_FLAG = "dry-run";
2721
- function fileStamp(date) {
2722
- return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
2723
- }
2724
- function keepsIdLine(swapped) {
2725
- 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.";
2726
- }
2727
- function refuseLiveLoop(root, isAlive) {
2728
- let records;
2729
- try {
2730
- records = readSessions(root, isAlive === undefined ? {} : { isAlive });
2731
- } catch (error) {
2732
- 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)}`);
2733
- }
2734
- const live = records.find((record) => record.state === "running" || record.state === "paused");
2735
- if (live === undefined)
2736
- return;
2737
- 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.`);
2738
- }
2739
- function migrationNames(result) {
2740
- return result.unknown.map(({ id, appliedBy }) => `${id} (applied by ${appliedBy})`).join(", ");
2741
- }
2742
- function rebuildLines(result) {
2743
- const rows = result.kept.reduce((sum, table) => sum + table.rows, 0);
2744
- const kept = result.kept.map((table) => `${table.table} ${String(table.rows)}`).join(", ");
2745
- const left = [
2746
- ...result.unknown.map(({ id, appliedBy }) => `migration ${id} (applied by ${appliedBy})`),
2747
- ...result.leftTables.map((table) => `table ${table.table} (${String(table.rows)} rows)`),
2748
- ...result.leftColumns.map((column) => `column ${column.table}.${column.column} (${String(column.values)} values)`)
2749
- ];
2750
- 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."];
2751
- return [
2752
- `Refused as ${String(result.reason)}: ${String(result.refusal)}`,
2753
- `Rebuilds it at the ${String(result.known.length)} migrations this rafa knows, logged as applied by ${result.appliedBy}.`,
2754
- `Keeps ${String(result.kept.length)} tables, ${String(rows)} rows: ${kept}.`,
2755
- left.length === 0 ? "Leaves nothing behind." : `Leaves behind, kept only in the backup: ${left.join("; ")}.`,
2756
- ...newer
2757
- ];
2758
- }
2759
- function unknownAdditiveLines(result) {
2760
- 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."];
2761
- }
2762
- function renderFixSchema(result) {
2763
- switch (result.status) {
2764
- case "missing":
2765
- return [`No effort store at ${result.path}: nothing to repair.`];
2766
- case "current":
2767
- return [
2768
- `\u2705 ${result.path} is current: this rafa reads and writes it as it is. Nothing to repair.`,
2769
- ...unknownAdditiveLines(result)
2770
- ];
2771
- case "behind":
2772
- return [
2773
- `${result.path} is behind this rafa's migrations; the next command that opens it records` + ` ${result.pending.join(", ")}. Nothing to repair.`,
2774
- ...unknownAdditiveLines(result)
2775
- ];
2776
- case "would-rebuild":
2777
- return [
2778
- ...rebuildLines(result),
2779
- "\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."
2780
- ];
2781
- case "rebuilt":
2782
- return [
2783
- ...rebuildLines(result),
2784
- `\u2705 Rebuilt at this rafa's migrations. Every row the original held is copied to ${String(result.backupPath)}.` + ` ${keepsIdLine("rebuild")}`
2785
- ];
2786
- }
2787
- }
2788
- function runFixSchema(context, seams) {
2789
- expectNoArgument(context.args, FIX_SCHEMA_USAGE);
2790
- const dryRun = readSwitch(DRY_RUN_FLAG, context.flags[DRY_RUN_FLAG], `Usage: ${FIX_SCHEMA_USAGE}`);
2791
- if (context.project === null)
2792
- throw new Error(`${COMMAND_NAME} runs inside a project, and was handed none`);
2793
- const root = context.project.root;
2794
- if (!dryRun)
2795
- refuseLiveLoop(root, seams.isAlive);
2796
- const now = seams.now ?? (() => new Date);
2797
- let result;
2798
- try {
2799
- result = fixStoreSchema({
2800
- path: sqliteStorePath(root),
2801
- dryRun,
2802
- stamp: fileStamp(now()),
2803
- now,
2804
- ...seams.identity === undefined ? {} : { identity: seams.identity }
2805
- });
2806
- } catch (error) {
2807
- if (error instanceof RebuildRefusal)
2808
- throw new CommandExit(1, `\u274C ${COMMAND_NAME}: ${error.message}`);
2809
- throw error;
2810
- }
2811
- if (context.outputMode === "json") {
2812
- context.output.result(result);
2813
- return;
2814
- }
2815
- for (const line of renderFixSchema(result))
2816
- context.output.info(line);
2817
- }
2818
- function createFixSchemaCommand(seams = {}) {
2819
- const command = {
2820
- name: "effort fix-schema",
2821
- subject: "effort",
2822
- action: "fix-schema",
2823
- summary: "rebuild an effort store this rafa refuses at the migrations it knows, keeping the original",
2824
- 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.",
2825
- args: [],
2826
- flags: [
2827
- {
2828
- name: DRY_RUN_FLAG,
2829
- 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.",
2830
- type: "boolean"
2831
- }
2832
- ],
2833
- examples: [
2834
- {
2835
- cmd: "rafa effort fix-schema --dry-run",
2836
- note: "Prints what a rebuild would keep and leave behind, changing nothing."
2837
- },
2838
- {
2839
- cmd: "rafa effort fix-schema",
2840
- note: "Swaps the rebuild in and prints where the original was kept."
2841
- }
2842
- ],
2843
- outputs: ["text", "json"],
2844
- run: async (context) => {
2845
- runFixSchema(context, seams);
2846
- await Promise.resolve();
2847
- }
2848
- };
2849
- return Object.freeze(command);
2850
- }
2851
- var fix_schema_default = createFixSchemaCommand();
2852
-
2853
- // src/effort/store/merge-store.ts
2854
- import { randomUUID as randomUUID5 } from "crypto";
2855
- import { existsSync as existsSync10, mkdtempSync, rmSync as rmSync7 } from "fs";
2856
- import { tmpdir as tmpdir3 } from "os";
2857
- import { dirname as dirname12, join as join12 } from "path";
2858
- import { Database as Database5, SQLiteError as SQLiteError2 } from "bun:sqlite";
2859
2529
 
2860
2530
  // src/effort/commits.ts
2861
2531
  import { execFileSync } from "child_process";
@@ -3085,6 +2755,7 @@ var MERGE_RULES = {
3085
2755
  plan_ci: appendOnly("pr", "head_sha", "read_at"),
3086
2756
  schema_migrations: { scope: "local" },
3087
2757
  store_meta: { scope: "local" },
2758
+ store_replaced: { scope: "local" },
3088
2759
  merges: { scope: "local" },
3089
2760
  merge_conflicts: { scope: "local" }
3090
2761
  };
@@ -3288,7 +2959,7 @@ class MergeRefusal extends RebuildRefusal {
3288
2959
  this.reason = reason;
3289
2960
  }
3290
2961
  }
3291
- var LIVE2 = "live";
2962
+ var LIVE = "live";
3292
2963
  var MERGES_TABLE = "merges";
3293
2964
  var INSERT_MERGE = `INSERT INTO merges (id, other_store, merged_at, rows_added, rows_skipped, rows_in_conflict)
3294
2965
  VALUES (?, ?, ?, ?, ?, ?)`;
@@ -3299,7 +2970,7 @@ function refuseNdjson(backend) {
3299
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}`);
3300
2971
  }
3301
2972
  function refuseMissing(path, which) {
3302
- if (existsSync10(path))
2973
+ if (existsSync8(path))
3303
2974
  return;
3304
2975
  throw new MergeRefusal("missing", `REFUSED \u2014 ${which} ${path} is not there. ${NOTHING_MERGED}`);
3305
2976
  }
@@ -3338,14 +3009,14 @@ function recordedMeta(path) {
3338
3009
  }
3339
3010
  function refuseOtherProject(options) {
3340
3011
  const other = recordedMeta(options.otherPath);
3341
- const here = recordedMeta(options.path)?.projectRootCommit ?? (options.readProject ?? readProjectIdentity)(dirname12(options.path)).rootCommit;
3012
+ const here = recordedMeta(options.path)?.projectRootCommit ?? (options.readProject ?? readProjectIdentity)(dirname11(options.path)).rootCommit;
3342
3013
  const there = other?.projectRootCommit ?? null;
3343
3014
  if (here !== null && there !== null && here !== there) {
3344
3015
  throw new MergeRefusal("other-project", `REFUSED \u2014 ${options.otherPath} is the effort store of another project (root commit ${there});` + ` this store's project has root commit ${here}. ${NOTHING_MERGED}`);
3345
3016
  }
3346
3017
  return other?.storeId ?? null;
3347
3018
  }
3348
- function refuseLiveLoop2(path, options, sessionId) {
3019
+ function refuseLiveLoop(path, options, sessionId) {
3349
3020
  const loop = liveLoop(path, options, NOTHING_MERGED, sessionId);
3350
3021
  if (loop === undefined)
3351
3022
  return;
@@ -3357,13 +3028,13 @@ function openOther(otherPath, plan, stamp) {
3357
3028
  const db = new Database5(otherPath, { readonly: true });
3358
3029
  return { db, broughtForward: [], release: () => db.close() };
3359
3030
  }
3360
- const dir = mkdtempSync(join12(tmpdir3(), "rafa-merge-other-"));
3031
+ const dir = mkdtempSync(join11(tmpdir3(), "rafa-merge-other-"));
3361
3032
  const release = (db) => {
3362
3033
  db?.close();
3363
3034
  rmSync7(dir, { recursive: true, force: true });
3364
3035
  };
3365
3036
  try {
3366
- const copyPath = join12(dir, "other.sqlite");
3037
+ const copyPath = join11(dir, "other.sqlite");
3367
3038
  vacuumInto(otherPath, copyPath);
3368
3039
  const db = new Database5(copyPath, { readwrite: true });
3369
3040
  try {
@@ -3413,10 +3084,10 @@ function buildMerged(path, parallelPath, other, merge2) {
3413
3084
  try {
3414
3085
  bringForward(db, parallelPath, "write", "open", { ...merge2.stamp, builtAside: true });
3415
3086
  const merged = mergeInto(db, other, merge2.id, merge2.otherStore, merge2.stamp.now().toISOString());
3416
- db.run(`ATTACH DATABASE ? AS ${LIVE2}`, [path]);
3417
- const live = new Set(tableNames(db, LIVE2));
3418
- checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE2, addedRows(merged));
3419
- db.run(`DETACH DATABASE ${LIVE2}`);
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}`);
3420
3091
  refuseCorrupt(db);
3421
3092
  refuseUnusable(db, parallelPath, merge2.stamp.migrations);
3422
3093
  return merged;
@@ -3437,7 +3108,7 @@ function refuseUnmergeable(options, migrations) {
3437
3108
  const otherStore = refuseOtherProject(options);
3438
3109
  if (!options.dryRun) {
3439
3110
  refuseUnownedDevelopmentWrite(path, [...writeNames(localPlan), MERGES_TABLE], options);
3440
- refuseLiveLoop2(path, options, options.sessionId ?? null);
3111
+ refuseLiveLoop(path, options, options.sessionId ?? null);
3441
3112
  }
3442
3113
  return { otherPlan, otherStore };
3443
3114
  }
@@ -3485,7 +3156,7 @@ function refuseUncarriable(backend, source) {
3485
3156
  if (backend === "ndjson") {
3486
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}`);
3487
3158
  }
3488
- if (!existsSync11(join13(source, SQLITE_STORE_FILE_NAME))) {
3159
+ if (!existsSync9(join12(source, SQLITE_STORE_FILE_NAME))) {
3489
3160
  throw new FileSyncRefusal(`${REFUSAL}: no ${SQLITE_STORE_FILE_NAME} in ${source}. ${NOTHING_COPIED}`);
3490
3161
  }
3491
3162
  }
@@ -3499,15 +3170,15 @@ function createFileSync(options) {
3499
3170
  const now = options.now ?? (() => new Date);
3500
3171
  const storeDir = () => effortStoreDir(repoRoot, options.env);
3501
3172
  const push = async ({ to }) => {
3502
- const target = resolve5(repoRoot, requirePath("to", to));
3173
+ const target = resolve4(repoRoot, requirePath("to", to));
3503
3174
  const source = storeDir();
3504
3175
  refuseUncarriable(backend, source);
3505
3176
  const copied = copyEffortStore({ source, target });
3506
- return Object.freeze({ status: "pushed", path: join13(copied.directory, SQLITE_STORE_FILE_NAME) });
3177
+ return Object.freeze({ status: "pushed", path: join12(copied.directory, SQLITE_STORE_FILE_NAME) });
3507
3178
  };
3508
3179
  const pull = async ({ from, dryRun, sessionId = null }) => {
3509
- const otherPath = resolve5(repoRoot, requirePath("from", from));
3510
- const path = join13(storeDir(), SQLITE_STORE_FILE_NAME);
3180
+ const otherPath = resolve4(repoRoot, requirePath("from", from));
3181
+ const path = join12(storeDir(), SQLITE_STORE_FILE_NAME);
3511
3182
  const stamp = fileStamp(now());
3512
3183
  const merge2 = mergeStore({ ...mergeSeams(options), path, otherPath, backend, dryRun, stamp, now, sessionId });
3513
3184
  return Object.freeze({ status: "pulled", merge: merge2 });
@@ -3576,304 +3247,61 @@ function selectSync(repoRoot, config, registry = CORE_ADAPTER_REGISTRY) {
3576
3247
 
3577
3248
  // src/adapters/learning/local.ts
3578
3249
  import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
3579
- import { join as join16 } from "path";
3580
-
3581
- // src/commands/instinct/instinct-records.ts
3582
- import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync5 } from "fs";
3583
3250
  import { join as join15 } from "path";
3584
3251
 
3585
- // src/check/layout.ts
3586
- import { readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3587
- import { join as join14, relative as relative2 } from "path";
3588
- var SKILL_FILE = "SKILL.md";
3589
- var MARKDOWN_EXTENSION = ".md";
3590
- var LAYOUT_SEVERITY = Object.freeze({
3591
- "flat-grouped-layout": "failure",
3592
- "nested-group-layout": "failure",
3593
- "loose-file": "failure",
3594
- "missing-skill-file": "failure",
3595
- "nested-instinct-file": "failure",
3596
- "unreadable-entry": "failure",
3597
- "name-mismatch": "failure",
3598
- "dotfile-only-directory": "warning"
3599
- });
3600
- function layoutIssue(code, message) {
3601
- 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
+ };
3602
3268
  }
3603
- function entryKind(path) {
3269
+ function isRecord3(value) {
3270
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3271
+ }
3272
+ function parseBlock(yaml) {
3273
+ let parsed;
3604
3274
  try {
3605
- const stats = statSync4(path);
3606
- if (stats.isFile())
3607
- return "file";
3608
- if (stats.isDirectory())
3609
- return "directory";
3610
- return null;
3275
+ parsed = Bun.YAML.parse(yaml.replace(/\r\n/g, `
3276
+ `));
3611
3277
  } catch {
3612
3278
  return null;
3613
3279
  }
3280
+ return isRecord3(parsed) ? parsed : null;
3614
3281
  }
3615
- function visibleNames(dir) {
3616
- return readdirSync3(dir).filter((name2) => !name2.startsWith(".")).sort((left, right) => left.localeCompare(right));
3617
- }
3618
- function isMarkdown(name2) {
3619
- return name2.endsWith(MARKDOWN_EXTENSION);
3282
+ function readFrontmatterDocument(text) {
3283
+ const opening = scanLine(text, 0);
3284
+ if (opening.line !== FRONTMATTER_FENCE || opening.newline === "")
3285
+ return null;
3286
+ let offset = opening.next;
3287
+ while (offset < text.length) {
3288
+ const scanned = scanLine(text, offset);
3289
+ if (scanned.line === FRONTMATTER_FENCE) {
3290
+ const data = parseBlock(text.slice(opening.next, offset));
3291
+ return data === null ? null : {
3292
+ data,
3293
+ yaml: text.slice(opening.next, offset),
3294
+ body: text.slice(scanned.next),
3295
+ newline: opening.newline,
3296
+ closingNewline: scanned.newline
3297
+ };
3298
+ }
3299
+ offset = scanned.next;
3300
+ }
3301
+ return null;
3620
3302
  }
3621
- function markdownStem(name2) {
3622
- return name2.slice(0, -MARKDOWN_EXTENSION.length);
3623
- }
3624
- function shown(root, path) {
3625
- return relative2(root, path) || ".";
3626
- }
3627
- function dotfileOnlyEntry(kind, root, dir) {
3628
- return {
3629
- kind,
3630
- path: dir,
3631
- isFile: false,
3632
- shape: "dotfile-only",
3633
- name: null,
3634
- issues: [layoutIssue("dotfile-only-directory", `${shown(root, dir)}/ holds nothing but dotfiles, so it registers nothing`)]
3635
- };
3636
- }
3637
- function unreadableEntry(kind, root, path) {
3638
- return {
3639
- kind,
3640
- path,
3641
- isFile: false,
3642
- shape: "unreadable",
3643
- name: null,
3644
- issues: [layoutIssue("unreadable-entry", `${shown(root, path)} cannot be read (a broken symlink, or a path stat refuses)`)]
3645
- };
3646
- }
3647
- function skillFileEntry(path, name2) {
3648
- return { kind: "skill", path, isFile: true, shape: "skill-file", name: name2, issues: [] };
3649
- }
3650
- function flatGroupedEntry(root, path, name2) {
3651
- return {
3652
- kind: "skill",
3653
- path,
3654
- isFile: true,
3655
- shape: "flat-grouped",
3656
- name: name2,
3657
- 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}`)]
3658
- };
3659
- }
3660
- function nestedGroupEntry(root, path, name2) {
3661
- return {
3662
- kind: "skill",
3663
- path,
3664
- isFile: true,
3665
- shape: "nested-group",
3666
- name: name2,
3667
- 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}`)]
3668
- };
3669
- }
3670
- function looseFileEntry(root, path, name2) {
3671
- return {
3672
- kind: "skill",
3673
- path,
3674
- isFile: true,
3675
- shape: "loose-file",
3676
- name: name2,
3677
- 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}`)]
3678
- };
3679
- }
3680
- function noSkillFileEntry(root, dir) {
3681
- return {
3682
- kind: "skill",
3683
- path: dir,
3684
- isFile: false,
3685
- shape: "no-skill-file",
3686
- name: null,
3687
- issues: [layoutIssue("missing-skill-file", `${shown(root, dir)}/ holds no ${SKILL_FILE}, so it registers no skill`)]
3688
- };
3689
- }
3690
- function nestedInstinctEntry(root, path, name2) {
3691
- return {
3692
- kind: "instinct",
3693
- path,
3694
- isFile: true,
3695
- shape: "nested-instinct",
3696
- name: name2,
3697
- issues: [layoutIssue("nested-instinct-file", `${shown(root, path)} sits below the instincts directory, which is flat;` + ` move it to <dir>/${name2}${MARKDOWN_EXTENSION}`)]
3698
- };
3699
- }
3700
- function scanGroupMember(root, dir, name2) {
3701
- const children = visibleNames(dir);
3702
- if (children.length === 0)
3703
- return [dotfileOnlyEntry("skill", root, dir)];
3704
- const skillPath = join14(dir, SKILL_FILE);
3705
- if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
3706
- return [nestedGroupEntry(root, skillPath, name2)];
3707
- }
3708
- return [noSkillFileEntry(root, dir)];
3709
- }
3710
- function scanSkillGroup(root, dir, children) {
3711
- const entries = [];
3712
- for (const child of children) {
3713
- const path = join14(dir, child);
3714
- const found = entryKind(path);
3715
- if (found === null) {
3716
- entries.push(unreadableEntry("skill", root, path));
3717
- continue;
3718
- }
3719
- if (found === "directory") {
3720
- entries.push(...scanGroupMember(root, path, child));
3721
- continue;
3722
- }
3723
- if (isMarkdown(child))
3724
- entries.push(flatGroupedEntry(root, path, markdownStem(child)));
3725
- }
3726
- if (entries.length === 0)
3727
- return [noSkillFileEntry(root, dir)];
3728
- return entries;
3729
- }
3730
- function scanSkillDirectory(root, dir, name2) {
3731
- const children = visibleNames(dir);
3732
- if (children.length === 0)
3733
- return [dotfileOnlyEntry("skill", root, dir)];
3734
- const skillPath = join14(dir, SKILL_FILE);
3735
- if (children.includes(SKILL_FILE) && entryKind(skillPath) === "file") {
3736
- return [skillFileEntry(skillPath, name2)];
3737
- }
3738
- return scanSkillGroup(root, dir, children);
3739
- }
3740
- function scanSkillRoot(root) {
3741
- const entries = [];
3742
- for (const name2 of visibleNames(root)) {
3743
- const path = join14(root, name2);
3744
- const found = entryKind(path);
3745
- if (found === null) {
3746
- entries.push(unreadableEntry("skill", root, path));
3747
- continue;
3748
- }
3749
- if (found === "directory") {
3750
- entries.push(...scanSkillDirectory(root, path, name2));
3751
- continue;
3752
- }
3753
- if (isMarkdown(name2))
3754
- entries.push(looseFileEntry(root, path, markdownStem(name2)));
3755
- }
3756
- return entries;
3757
- }
3758
- function scanInstinctSubdirectory(root, dir) {
3759
- const children = visibleNames(dir);
3760
- if (children.length === 0)
3761
- return [dotfileOnlyEntry("instinct", root, dir)];
3762
- const entries = [];
3763
- for (const child of children) {
3764
- const path = join14(dir, child);
3765
- const found = entryKind(path);
3766
- if (found === null) {
3767
- entries.push(unreadableEntry("instinct", root, path));
3768
- continue;
3769
- }
3770
- if (found === "directory") {
3771
- entries.push(...scanInstinctSubdirectory(root, path));
3772
- continue;
3773
- }
3774
- if (isMarkdown(child))
3775
- entries.push(nestedInstinctEntry(root, path, markdownStem(child)));
3776
- }
3777
- return entries;
3778
- }
3779
- function scanInstinctRoot(root) {
3780
- const entries = [];
3781
- for (const name2 of visibleNames(root)) {
3782
- const path = join14(root, name2);
3783
- const found = entryKind(path);
3784
- if (found === null) {
3785
- entries.push(unreadableEntry("instinct", root, path));
3786
- continue;
3787
- }
3788
- if (found === "directory") {
3789
- entries.push(...scanInstinctSubdirectory(root, path));
3790
- continue;
3791
- }
3792
- if (!isMarkdown(name2))
3793
- continue;
3794
- entries.push({
3795
- kind: "instinct",
3796
- path,
3797
- isFile: true,
3798
- shape: "instinct-file",
3799
- name: markdownStem(name2),
3800
- issues: []
3801
- });
3802
- }
3803
- return entries;
3804
- }
3805
- function scanLayout(root, kind) {
3806
- if (entryKind(root) !== "directory") {
3807
- throw new Error(`check layout: no ${kind} directory at ${root}`);
3808
- }
3809
- const entries = kind === "skill" ? scanSkillRoot(root) : scanInstinctRoot(root);
3810
- return {
3811
- kind,
3812
- root,
3813
- entries: entries.sort((left, right) => left.path.localeCompare(right.path))
3814
- };
3815
- }
3816
- function declaredNameIssue(entry, declared) {
3817
- if (entry.name === null || declared === null || declared === entry.name)
3818
- return null;
3819
- const field2 = entry.kind === "skill" ? "name" : "id";
3820
- const source = entry.kind === "skill" ? "directory name" : "file name";
3821
- return layoutIssue("name-mismatch", `frontmatter ${field2} "${declared}" is not the ${source} "${entry.name}"`);
3822
- }
3823
-
3824
- // src/schema/frontmatter.ts
3825
- var FRONTMATTER_FENCE = "---";
3826
- var FRONTMATTER_INDENT = 2;
3827
- function scanLine(text, offset) {
3828
- const index = text.indexOf(`
3829
- `, offset);
3830
- if (index === -1) {
3831
- return { line: text.slice(offset), newline: "", next: text.length };
3832
- }
3833
- const carriage = index > offset && text[index - 1] === "\r";
3834
- const end = carriage ? index - 1 : index;
3835
- return {
3836
- line: text.slice(offset, end),
3837
- newline: text.slice(end, index + 1),
3838
- next: index + 1
3839
- };
3840
- }
3841
- function isRecord3(value) {
3842
- return typeof value === "object" && value !== null && !Array.isArray(value);
3843
- }
3844
- function parseBlock(yaml) {
3845
- let parsed;
3846
- try {
3847
- parsed = Bun.YAML.parse(yaml.replace(/\r\n/g, `
3848
- `));
3849
- } catch {
3850
- return null;
3851
- }
3852
- return isRecord3(parsed) ? parsed : null;
3853
- }
3854
- function readFrontmatterDocument(text) {
3855
- const opening = scanLine(text, 0);
3856
- if (opening.line !== FRONTMATTER_FENCE || opening.newline === "")
3857
- return null;
3858
- let offset = opening.next;
3859
- while (offset < text.length) {
3860
- const scanned = scanLine(text, offset);
3861
- if (scanned.line === FRONTMATTER_FENCE) {
3862
- const data = parseBlock(text.slice(opening.next, offset));
3863
- return data === null ? null : {
3864
- data,
3865
- yaml: text.slice(opening.next, offset),
3866
- body: text.slice(scanned.next),
3867
- newline: opening.newline,
3868
- closingNewline: scanned.newline
3869
- };
3870
- }
3871
- offset = scanned.next;
3872
- }
3873
- return null;
3874
- }
3875
- function readFrontmatter(text) {
3876
- return readFrontmatterDocument(text)?.data ?? null;
3303
+ function readFrontmatter(text) {
3304
+ return readFrontmatterDocument(text)?.data ?? null;
3877
3305
  }
3878
3306
  function renderFrontmatter(data, newline = `
3879
3307
  `) {
@@ -4103,123 +3531,366 @@ function checkInstinctFrontmatter(data) {
4103
3531
  ...checkStoredHash(data)
4104
3532
  ];
4105
3533
  }
4106
- function isFence(line) {
4107
- return /^\s*(?:```|~~~)/.test(line);
3534
+ function isFence(line) {
3535
+ return /^\s*(?:```|~~~)/.test(line);
3536
+ }
3537
+ function sectionText(body, heading) {
3538
+ const lines = body.split(`
3539
+ `).map((line) => line.replace(/\r$/, ""));
3540
+ const collected = [];
3541
+ let fenced = false;
3542
+ let inside = false;
3543
+ for (const line of lines) {
3544
+ if (isFence(line))
3545
+ fenced = !fenced;
3546
+ else if (!fenced && /^##\s/.test(line)) {
3547
+ if (inside)
3548
+ break;
3549
+ inside = line.trim() === heading;
3550
+ continue;
3551
+ }
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)));
3794
+ }
3795
+ if (entries.length === 0)
3796
+ return [noSkillFileEntry(root, dir)];
3797
+ return entries;
3798
+ }
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)];
3806
+ }
3807
+ return scanSkillGroup(root, dir, children);
4108
3808
  }
4109
- function sectionText(body, heading) {
4110
- const lines = body.split(`
4111
- `).map((line) => line.replace(/\r$/, ""));
4112
- const collected = [];
4113
- let fenced = false;
4114
- let inside = false;
4115
- for (const line of lines) {
4116
- if (isFence(line))
4117
- fenced = !fenced;
4118
- else if (!fenced && /^##\s/.test(line)) {
4119
- if (inside)
4120
- break;
4121
- inside = line.trim() === 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));
4122
3816
  continue;
4123
3817
  }
4124
- if (inside)
4125
- collected.push(line);
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)));
4126
3824
  }
4127
- return inside ? collected.join(`
4128
- `).trim() : null;
3825
+ return entries;
4129
3826
  }
4130
- function checkSection(body, heading) {
4131
- const text = sectionText(body, heading);
4132
- if (text === null) {
4133
- return [issue("missing-section", heading, `the body carries no ${heading} section`)];
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)));
4134
3845
  }
4135
- return text === "" ? [issue("missing-section", heading, `the ${heading} section is empty`)] : [];
4136
- }
4137
- function checkInstinctBody(body) {
4138
- return [...checkSection(body, ACTION_HEADING), ...checkSection(body, CAUSE_HEADING)];
3846
+ return entries;
4139
3847
  }
4140
- function parseInstinct(text) {
4141
- const document = readFrontmatterDocument(text);
4142
- if (document === null) {
4143
- return {
4144
- issues: [issue("missing-frontmatter", "frontmatter", "the file opens with no --- block holding a YAML mapping")],
4145
- instinct: null
4146
- };
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
+ });
4147
3871
  }
4148
- const issues = [
4149
- ...checkInstinctFrontmatter(document.data),
4150
- ...checkInstinctBody(document.body)
4151
- ];
4152
- return issues.length > 0 ? { issues, instinct: null } : { issues, instinct: buildInstinct(document.data, document.body) };
3872
+ return entries;
4153
3873
  }
4154
- function buildInstinct(data, body) {
4155
- const action = sectionText(body, ACTION_HEADING) ?? "";
4156
- const artifact = typeof data["artifact"] === "string" && data["artifact"].trim() !== "" ? data["artifact"] : null;
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);
4157
3879
  return {
4158
- id: data["id"],
4159
- trigger: data["trigger"],
4160
- kind: data["kind"],
4161
- domain: data["domain"],
4162
- confidence: data["confidence"],
4163
- usageCount: typeof data["usage_count"] === "number" ? data["usage_count"] : DEFAULT_USAGE_COUNT,
4164
- sources: Array.isArray(data["sources"]) ? data["sources"] : [],
4165
- artifact,
4166
- signal: data["signal"],
4167
- scope: data["scope"],
4168
- projectId: typeof data["project_id"] === "string" ? data["project_id"] : null,
4169
- source: data["source"],
4170
- evidence: Array.isArray(data["evidence"]) ? data["evidence"] : [],
4171
- promotedTo: typeof data["promoted_to"] === "string" ? data["promoted_to"] : null,
4172
- createdAt: data["created_at"],
4173
- updatedAt: data["updated_at"],
4174
- action,
4175
- cause: sectionText(body, CAUSE_HEADING) ?? "",
4176
- actionHash: actionHash(action)
4177
- };
4178
- }
4179
- function instinctFrontmatter(instinct) {
4180
- const written = {
4181
- id: instinct.id,
4182
- trigger: instinct.trigger,
4183
- kind: instinct.kind,
4184
- domain: instinct.domain,
4185
- confidence: instinct.confidence,
4186
- usage_count: instinct.usageCount,
4187
- signal: instinct.signal,
4188
- scope: instinct.scope,
4189
- source: instinct.source,
4190
- created_at: instinct.createdAt,
4191
- updated_at: instinct.updatedAt
3880
+ kind,
3881
+ root,
3882
+ entries: entries.sort((left, right) => left.path.localeCompare(right.path))
4192
3883
  };
4193
- if (instinct.artifact !== null)
4194
- written["artifact"] = instinct.artifact;
4195
- if (instinct.projectId !== null)
4196
- written["project_id"] = instinct.projectId;
4197
- if (instinct.evidence.length > 0)
4198
- written["evidence"] = instinct.evidence;
4199
- if (instinct.sources.length > 0)
4200
- written["sources"] = instinct.sources;
4201
- if (instinct.promotedTo !== null)
4202
- written["promoted_to"] = instinct.promotedTo;
4203
- return Object.fromEntries(INSTINCT_KEY_ORDER.filter((key) => Object.hasOwn(written, key)).map((key) => [key, written[key]]));
4204
3884
  }
4205
- function writeInstinct(instinct, newline = `
4206
- `) {
4207
- const block = renderFrontmatter(instinctFrontmatter(instinct), newline);
4208
- return [
4209
- "---",
4210
- block,
4211
- "---",
4212
- "",
4213
- ACTION_HEADING,
4214
- instinct.action,
4215
- "",
4216
- CAUSE_HEADING,
4217
- instinct.cause,
4218
- ""
4219
- ].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}"`);
4220
3891
  }
4221
3892
 
4222
- // src/commands/instinct/instinct-records.ts
3893
+ // src/schema/scope-records.ts
4223
3894
  function isRecordFile(name2) {
4224
3895
  return !name2.startsWith(".") && name2.endsWith(MARKDOWN_EXTENSION) && name2 !== MARKDOWN_EXTENSION;
4225
3896
  }
@@ -4232,7 +3903,7 @@ function unreadableIssue(message) {
4232
3903
  function readRecord(path, scope, id) {
4233
3904
  let text;
4234
3905
  try {
4235
- text = readFileSync9(path, "utf8");
3906
+ text = readFileSync8(path, "utf8");
4236
3907
  } catch (error) {
4237
3908
  const message = error instanceof Error ? error.message : String(error);
4238
3909
  return { scope, path, id, instinct: null, issues: [unreadableIssue(message)] };
@@ -4240,15 +3911,15 @@ function readRecord(path, scope, id) {
4240
3911
  const parsed = parseInstinct(text);
4241
3912
  return { scope, path, id, instinct: parsed.instinct, issues: parsed.issues };
4242
3913
  }
4243
- function isFile2(path) {
3914
+ function isFile(path) {
4244
3915
  try {
4245
- return statSync5(path).isFile();
3916
+ return statSync4(path).isFile();
4246
3917
  } catch {
4247
3918
  return false;
4248
3919
  }
4249
3920
  }
4250
3921
  function recordFiles(dir) {
4251
- return readdirSync4(dir).filter((name2) => isRecordFile(name2) && isFile2(join15(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));
4252
3923
  }
4253
3924
  function readScope(scope, dir) {
4254
3925
  if (!tierExists(dir))
@@ -4257,7 +3928,7 @@ function readScope(scope, dir) {
4257
3928
  scope,
4258
3929
  dir,
4259
3930
  exists: true,
4260
- records: recordFiles(dir).map((name2) => readRecord(join15(dir, name2), scope, recordId(name2)))
3931
+ records: recordFiles(dir).map((name2) => readRecord(join14(dir, name2), scope, recordId(name2)))
4261
3932
  };
4262
3933
  }
4263
3934
  function readScopes(seams) {
@@ -4549,7 +4220,7 @@ function heldChanges(dir, held, result, described) {
4549
4220
  if (issue2 !== undefined) {
4550
4221
  throw new Error(`${PUSH_REFUSAL}: ${file} would not read back: ${issue2.field}: ${issue2.message}`);
4551
4222
  }
4552
- const path = join16(dir, file);
4223
+ const path = join15(dir, file);
4553
4224
  if (held.unreadable.has(path)) {
4554
4225
  throw new Error(`${PUSH_REFUSAL}: it would overwrite ${path}, which could not be read`);
4555
4226
  }
@@ -4579,18 +4250,18 @@ function pushLogLines(sourceId, described, result) {
4579
4250
  });
4580
4251
  }
4581
4252
  function localInstinctsDir(repoRoot) {
4582
- return join16(repoRoot, RAFA_INSTINCTS_PATH);
4253
+ return join15(repoRoot, RAFA_INSTINCTS_PATH);
4583
4254
  }
4584
4255
  function userInstinctsDir(home) {
4585
- return join16(home, RAFA_INSTINCTS_PATH);
4256
+ return join15(home, RAFA_INSTINCTS_PATH);
4586
4257
  }
4587
4258
  function createLocalLearning(options) {
4588
4259
  const { instinctsDir, minConfidence } = options;
4589
4260
  const userDir = userInstinctsDir(options.home);
4590
4261
  const now = options.now ?? (() => new Date().toISOString());
4591
4262
  const warn = options.warn ?? warnThroughActiveOutput;
4592
- const instinctsPath = join16(instinctsDir, INSTINCTS_FILE);
4593
- const flagsPath = join16(instinctsDir, FLAGS_FILE);
4263
+ const instinctsPath = join15(instinctsDir, INSTINCTS_FILE);
4264
+ const flagsPath = join15(instinctsDir, FLAGS_FILE);
4594
4265
  function readHeld(scope, dir) {
4595
4266
  const { records } = readScope(scope, dir);
4596
4267
  const unreadable = new Set;
@@ -4666,6 +4337,203 @@ function createLocalLearning(options) {
4666
4337
  return Object.freeze(learning);
4667
4338
  }
4668
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
+
4669
4537
  // src/adapters/output/events.ts
4670
4538
  var EVENT_PREFIX = "rafa\xB7 ";
4671
4539
  var KIND_COLUMN = 17;
@@ -4735,9 +4603,9 @@ function createJsonOutput({
4735
4603
  }
4736
4604
 
4737
4605
  // src/adapters/planner/claude.ts
4738
- import { existsSync as existsSync12 } from "fs";
4606
+ import { existsSync as existsSync10 } from "fs";
4739
4607
  import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
4740
- import { posix, resolve as resolve6 } from "path";
4608
+ import { posix, resolve as resolve5 } from "path";
4741
4609
 
4742
4610
  // src/board/spec-review.ts
4743
4611
  var SPEC_REVIEW_KIND = "spec-review";
@@ -4902,10 +4770,36 @@ function parseSpecReview(output) {
4902
4770
  return judged(verdict, readGaps(document, issues), block, issues);
4903
4771
  }
4904
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
+
4905
4797
  // src/cli/running.ts
4798
+ import { AsyncLocalStorage } from "async_hooks";
4799
+ var scopes = new AsyncLocalStorage;
4906
4800
  var running = null;
4907
4801
  function runningCommand() {
4908
- return running;
4802
+ return scopes.getStore() ?? running;
4909
4803
  }
4910
4804
 
4911
4805
  // src/utils/session-env.ts
@@ -4997,12 +4891,12 @@ function runClaude(prompt, settingSources, flags = [], spawn = spawnClaude, serv
4997
4891
  function cwdOption(options) {
4998
4892
  return options.cwd === undefined ? {} : { cwd: options.cwd };
4999
4893
  }
5000
- function writeWholeLines(text, write) {
4894
+ function writeWholeLines(text, write2) {
5001
4895
  const lines = text.split(`
5002
4896
  `);
5003
4897
  const rest = lines.pop() ?? "";
5004
4898
  for (const line of lines)
5005
- write(line);
4899
+ write2(line);
5006
4900
  return rest;
5007
4901
  }
5008
4902
  async function teeToOperator(stream) {
@@ -5073,24 +4967,26 @@ class ClaudePlannerError extends Error {
5073
4967
  }
5074
4968
  function createClaudePlanner(options) {
5075
4969
  const { repoRoot, planDir, settingSources, buildPrompt, spawn = spawnClaudeCaptured } = options;
5076
- const root = resolve6(repoRoot);
4970
+ const root = resolve5(repoRoot);
5077
4971
  const create = async ({ specPath, stub }) => {
5078
4972
  const planPath = planFilePath(planDir, `PLAN-${stub}.md`);
5079
4973
  const prerequisitesPath = planFilePath(planDir, `PREREQUISITES-${stub}.md`);
5080
- const isWritten = (path) => existsSync12(resolve6(root, path));
4974
+ const isWritten = (path) => existsSync10(resolve5(root, path));
5081
4975
  if (isWritten(planPath)) {
5082
4976
  throw new ClaudePlannerError(`${planPath} already exists`, FAILURE_EXIT_CODE);
5083
4977
  }
5084
- const specContent = await readFile2(resolve6(root, specPath), "utf8");
5085
- await mkdir2(resolve6(root, planDir), { recursive: true });
5086
- 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 });
5087
4983
  const review = parseSpecReview(session.stdout);
5088
4984
  const { exitCode } = session;
5089
4985
  if (exitCode !== 0) {
5090
4986
  throw new ClaudePlannerError(`Plan generation failed (exit ${exitCode}).`, exitCode, review);
5091
4987
  }
5092
4988
  if (!isWritten(planPath)) {
5093
- 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);
5094
4990
  }
5095
4991
  return {
5096
4992
  planPath,
@@ -5195,8 +5091,11 @@ var GITHUB_LABELS = Object.freeze({
5195
5091
  priorityPrefix: "priority:",
5196
5092
  needsTriage: "needs-triage",
5197
5093
  blockedByPrefix: "blocked-by:OPT-",
5198
- specBlocked: "spec:blocked"
5094
+ specBlocked: "spec:blocked",
5095
+ causePrefix: "cause:"
5199
5096
  });
5097
+ var GITHUB_LABEL_MAX_LENGTH = 50;
5098
+ var CAUSE_MAX_LENGTH = GITHUB_LABEL_MAX_LENGTH - GITHUB_LABELS.causePrefix.length;
5200
5099
  var VIEW_FIELDS = "number,title,body,state,stateReason,labels,url";
5201
5100
  var LIST_COMMAND = "gh issue list";
5202
5101
  var LIST_FIELDS = "number,url,labels";
@@ -5254,8 +5153,8 @@ async function withDeadline(work, timeoutMs) {
5254
5153
  if (timeoutMs === undefined)
5255
5154
  return work;
5256
5155
  let timer;
5257
- const deadline = new Promise((resolve7) => {
5258
- timer = setTimeout(() => resolve7(TIMED_OUT), timeoutMs);
5156
+ const deadline = new Promise((resolve6) => {
5157
+ timer = setTimeout(() => resolve6(TIMED_OUT), timeoutMs);
5259
5158
  });
5260
5159
  try {
5261
5160
  return await Promise.race([work, deadline]);
@@ -5282,6 +5181,9 @@ function isRepoName(value) {
5282
5181
  function isLabelPart(value) {
5283
5182
  return typeof value === "string" && !value.includes(",");
5284
5183
  }
5184
+ function isCauseLabelPart(value) {
5185
+ return value === undefined || isLabelPart(value) && value.length <= CAUSE_MAX_LENGTH;
5186
+ }
5285
5187
  function optionalFlag2(name2, value) {
5286
5188
  return value === undefined ? [] : [name2, value];
5287
5189
  }
@@ -5305,7 +5207,8 @@ var DRAFT_CHECKS = [
5305
5207
  (value) => Array.isArray(value) && value.every((item) => Number.isSafeInteger(item) && item > 0),
5306
5208
  "a list of positive whole numbers"
5307
5209
  ],
5308
- ["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`]
5309
5212
  ];
5310
5213
  function draftProblem(draft) {
5311
5214
  if (!isMapping(draft))
@@ -5320,6 +5223,7 @@ function labelsFor(draft) {
5320
5223
  `${GITHUB_LABELS.typePrefix}${draft.type}`,
5321
5224
  ...draft.blockedBy.map((opt) => `${GITHUB_LABELS.blockedByPrefix}${opt}`),
5322
5225
  ...draft.specBlocked === true ? [GITHUB_LABELS.specBlocked] : [],
5226
+ ...draft.cause === undefined ? [] : [`${GITHUB_LABELS.causePrefix}${draft.cause}`],
5323
5227
  priority
5324
5228
  ];
5325
5229
  }
@@ -5598,7 +5502,7 @@ function createGithubTracker(options) {
5598
5502
 
5599
5503
  // src/adapters/tracker/local.ts
5600
5504
  import { mkdir as mkdir3, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
5601
- import { join as join17 } from "path";
5505
+ import { join as join16 } from "path";
5602
5506
  var PREFIX4 = "local tracker";
5603
5507
  var FRONTMATTER = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
5604
5508
  var ISSUE_NUMBER2 = /^[1-9]\d*$/;
@@ -5634,6 +5538,7 @@ var FIELD_CHECKS = {
5634
5538
  title: [isString2, "a string"],
5635
5539
  blockedBy: [listOf(isFiniteNumber), "a list of numbers"],
5636
5540
  specBlocked: [orAbsent(isBoolean), "absent or a boolean"],
5541
+ cause: [orAbsent(isString2), "absent or a string"],
5637
5542
  state: [oneOf2(ISSUE_STATES), `one of: ${ISSUE_STATES.join(", ")}`],
5638
5543
  fallbackReason: [orNull(isString2), "a string or null"],
5639
5544
  capturedAt: [isString2, "a string"],
@@ -5663,7 +5568,7 @@ function issueNumberOfFile(name2) {
5663
5568
  return name2.endsWith(ISSUE_EXTENSION) ? parseIssueNumber(name2.slice(0, -ISSUE_EXTENSION.length)) : null;
5664
5569
  }
5665
5570
  function issuePath(issuesDir, number) {
5666
- return join17(issuesDir, `${number}${ISSUE_EXTENSION}`);
5571
+ return join16(issuesDir, `${number}${ISSUE_EXTENSION}`);
5667
5572
  }
5668
5573
  async function issueNumbers(issuesDir) {
5669
5574
  let names;
@@ -5677,7 +5582,7 @@ async function issueNumbers(issuesDir) {
5677
5582
  return names.map(issueNumberOfFile).filter((number) => number !== null).sort((a, b) => a - b);
5678
5583
  }
5679
5584
  function localIssuesDir(repoRoot) {
5680
- return join17(repoRoot, ".rafa", "issues");
5585
+ return join16(repoRoot, ".rafa", "issues");
5681
5586
  }
5682
5587
  function renderLocalIssue(record) {
5683
5588
  const { draft } = record;
@@ -5690,6 +5595,7 @@ function renderLocalIssue(record) {
5690
5595
  title: draft.title,
5691
5596
  blockedBy: draft.blockedBy,
5692
5597
  ...draft.specBlocked === undefined ? {} : { specBlocked: draft.specBlocked },
5598
+ ...draft.cause === undefined ? {} : { cause: draft.cause },
5693
5599
  state: record.state,
5694
5600
  fallbackReason: record.fallbackReason,
5695
5601
  capturedAt: record.capturedAt,
@@ -5732,7 +5638,8 @@ function parseLocalIssue(contents, sourcePath) {
5732
5638
  priority: fields.priority,
5733
5639
  project: fields.project,
5734
5640
  blockedBy: fields.blockedBy,
5735
- ...fields.specBlocked === undefined ? {} : { specBlocked: fields.specBlocked }
5641
+ ...fields.specBlocked === undefined ? {} : { specBlocked: fields.specBlocked },
5642
+ ...fields.cause === undefined ? {} : { cause: fields.cause }
5736
5643
  },
5737
5644
  fallbackReason: fields.fallbackReason,
5738
5645
  capturedAt: fields.capturedAt,
@@ -5896,7 +5803,8 @@ var PORT_VERSIONS = Object.freeze({
5896
5803
  learning: 1,
5897
5804
  output: 1,
5898
5805
  planner: 1,
5899
- sync: 1
5806
+ sync: 1,
5807
+ logger: 1
5900
5808
  });
5901
5809
  var SERVED_VERSIONS = new Map(Object.entries(PORT_VERSIONS));
5902
5810
  var PORT_TYPE_LIST = [...SERVED_VERSIONS.keys()].join(", ");
@@ -5935,7 +5843,8 @@ function indexByPort(adapters) {
5935
5843
  learning: only("learning"),
5936
5844
  output: only("output"),
5937
5845
  planner: only("planner"),
5938
- sync: only("sync")
5846
+ sync: only("sync"),
5847
+ logger: only("logger")
5939
5848
  };
5940
5849
  }
5941
5850
  function createAdapterRegistry(adapters = []) {
@@ -5989,6 +5898,12 @@ var CORE_ADAPTERS = [
5989
5898
  portVersion: PORT_VERSIONS.output,
5990
5899
  create: ({ stream = process.stdout }) => createEventsOutput({ stream })
5991
5900
  },
5901
+ {
5902
+ port: "logger",
5903
+ kind: "console",
5904
+ portVersion: PORT_VERSIONS.logger,
5905
+ create: ({ verbosity = 0, colour = false }) => createConsoleLogger({ verbosity, colour })
5906
+ },
5992
5907
  {
5993
5908
  port: "tracker",
5994
5909
  kind: "local",
@@ -6172,9 +6087,9 @@ function requireTrustedAuthor(item, reading) {
6172
6087
  throw new CommandExit(TRUST_REFUSAL_EXIT, trustRefusalMessage(item, reading));
6173
6088
  }
6174
6089
  // src/effort/sync/wire.ts
6175
- import { existsSync as existsSync13, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
6090
+ import { existsSync as existsSync11, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
6176
6091
  import { tmpdir as tmpdir4 } from "os";
6177
- import { join as join18 } from "path";
6092
+ import { join as join17 } from "path";
6178
6093
  import { Database as Database6, SQLiteError as SQLiteError3 } from "bun:sqlite";
6179
6094
  var WIRE_FORMAT = "rafa-effort-wire";
6180
6095
  var WIRE_VERSION = 1;
@@ -6335,7 +6250,7 @@ function exportWirePayload(options) {
6335
6250
  const { path } = options;
6336
6251
  const cursor = checkCursor(options.cursor ?? {});
6337
6252
  guardTestProcess(path);
6338
- if (!existsSync13(path))
6253
+ if (!existsSync11(path))
6339
6254
  throw new WireExportRefusal(`wire: ${path} is not there`);
6340
6255
  const db = new Database6(path, { readonly: true, safeIntegers: true });
6341
6256
  try {
@@ -6394,9 +6309,9 @@ function buildStore(path, payload, options) {
6394
6309
  }
6395
6310
  function materialiseWirePayload(payload, options = {}) {
6396
6311
  const checked = checkPayload(payload);
6397
- const directory = mkdtempSync2(join18(options.tempDir ?? tmpdir4(), "rafa-wire-"));
6312
+ const directory = mkdtempSync2(join17(options.tempDir ?? tmpdir4(), "rafa-wire-"));
6398
6313
  const release = () => rmSync8(directory, { recursive: true, force: true });
6399
- const path = join18(directory, SQLITE_STORE_FILE_NAME);
6314
+ const path = join17(directory, SQLITE_STORE_FILE_NAME);
6400
6315
  try {
6401
6316
  guardTestProcess(path);
6402
6317
  buildStore(path, checked, options);
@@ -6424,4 +6339,4 @@ function selectEffortStore(repoRoot, config) {
6424
6339
  return adapter.create({ repoRoot });
6425
6340
  }
6426
6341
 
6427
- export { EFFORT_DIR_VARIABLE, effortStoreDir, guardTestProcess, EFFORT_KEY_PROJECTIONS, openNdjsonStore, name, 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, 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, activeStoreSettings, normalizeRemote, gitRemoteUrl, readStoreMeta, SQLITE_STORE_FILE_NAME, sqliteStorePath, LONE_SURROGATE, withSqliteStore, writeSqliteStore, openSqliteStore, SPENDS_GLYPH, CommandExit, commandSpelling, readConfigFile, loadConfig, plansDirAt, requireProject, resolvePlansDir, countTasks, formatCounts, planFileName, stubOfPlanFile, isFile, rejectedPath, issueLine, plural, expectNoArgument, expectOneArgument, readSwitch, minutesBetween, readCommitLog, MOVE_TO_SQLITE, mergeStore, SyncModuleMissing, selectSync, SKILL_FILE, MARKDOWN_EXTENSION, scanLayout, declaredNameIssue, readFrontmatterDocument, readFrontmatter, mergeFrontmatter, writeFrontmatter, parseInstinct, readScopes, padKind, oneLine, 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 };
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 };