@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.
- package/dist/bundled/agents/qa-bug-reporter.md +50 -12
- package/dist/bundled/operators/skills/rafa-stretch-gap-log/SKILL.md +5 -1
- package/dist/cli.js +33280 -30966
- package/dist/effort/store/index.js +2 -2
- package/dist/{index-7ycam8pq.js → index-57mmzkcn.js} +1 -1
- package/dist/{index-q0dmfp8m.js → index-6q30dtgm.js} +790 -875
- package/dist/{index-bceachy6.js → index-cknayrpc.js} +484 -9
- package/dist/index.js +9466 -10153
- package/dist/plan/index.js +2 -2
- package/package.json +1 -1
|
@@ -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-
|
|
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.
|
|
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({
|
|
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
|
-
|
|
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
|
|
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
|
|
2197
|
-
import { join as
|
|
2270
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2271
|
+
import { join as join12, resolve as resolve4 } from "path";
|
|
2198
2272
|
|
|
2199
|
-
// src/
|
|
2200
|
-
|
|
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
|
|
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 (
|
|
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)(
|
|
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
|
|
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(
|
|
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 =
|
|
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 ${
|
|
3417
|
-
const live = new Set(tableNames(db,
|
|
3418
|
-
checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)),
|
|
3419
|
-
db.run(`DETACH DATABASE ${
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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:
|
|
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 =
|
|
3510
|
-
const path =
|
|
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/
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
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
|
|
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
|
-
|
|
3606
|
-
|
|
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
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
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
|
|
3622
|
-
return
|
|
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
|
|
4110
|
-
const
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
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 (
|
|
4125
|
-
|
|
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
|
|
4128
|
-
`).trim() : null;
|
|
3825
|
+
return entries;
|
|
4129
3826
|
}
|
|
4130
|
-
function
|
|
4131
|
-
const
|
|
4132
|
-
if (
|
|
4133
|
-
return [
|
|
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
|
|
4136
|
-
}
|
|
4137
|
-
function checkInstinctBody(body) {
|
|
4138
|
-
return [...checkSection(body, ACTION_HEADING), ...checkSection(body, CAUSE_HEADING)];
|
|
3846
|
+
return entries;
|
|
4139
3847
|
}
|
|
4140
|
-
function
|
|
4141
|
-
const
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
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
|
-
|
|
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
|
|
4155
|
-
|
|
4156
|
-
|
|
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
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
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
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
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/
|
|
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 =
|
|
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
|
|
3914
|
+
function isFile(path) {
|
|
4244
3915
|
try {
|
|
4245
|
-
return
|
|
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) &&
|
|
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(
|
|
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 =
|
|
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
|
|
4253
|
+
return join15(repoRoot, RAFA_INSTINCTS_PATH);
|
|
4583
4254
|
}
|
|
4584
4255
|
function userInstinctsDir(home) {
|
|
4585
|
-
return
|
|
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 =
|
|
4593
|
-
const flagsPath =
|
|
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
|
|
4606
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4739
4607
|
import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
4740
|
-
import { posix, resolve as
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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) =>
|
|
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(
|
|
5085
|
-
|
|
5086
|
-
|
|
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((
|
|
5258
|
-
timer = setTimeout(() =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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(
|
|
6312
|
+
const directory = mkdtempSync2(join17(options.tempDir ?? tmpdir4(), "rafa-wire-"));
|
|
6398
6313
|
const release = () => rmSync8(directory, { recursive: true, force: true });
|
|
6399
|
-
const path =
|
|
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,
|
|
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 };
|