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