@massu/core 1.16.3 → 2.1.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/CHANGELOG.md +135 -0
- package/dist/cli.js +2453 -1187
- package/dist/hooks/auto-learning-pipeline.js +265 -48
- package/dist/hooks/classify-failure.js +264 -47
- package/dist/hooks/cost-tracker.js +261 -44
- package/dist/hooks/fix-detector.js +270 -53
- package/dist/hooks/incident-pipeline.js +259 -42
- package/dist/hooks/intent-suggester.js +261 -44
- package/dist/hooks/memory-recall.js +401 -50
- package/dist/hooks/post-edit-context.js +263 -46
- package/dist/hooks/post-tool-use.js +287 -170
- package/dist/hooks/pre-compact.js +261 -44
- package/dist/hooks/pre-delete-check.js +265 -48
- package/dist/hooks/pre-tool-use-gate.js +265 -48
- package/dist/hooks/quality-event.js +261 -44
- package/dist/hooks/rule-enforcement-pipeline.js +260 -43
- package/dist/hooks/security-gate.js +261 -44
- package/dist/hooks/session-end.js +1442 -339
- package/dist/hooks/session-start.js +618 -274
- package/dist/hooks/user-prompt.js +287 -70
- package/package.json +2 -2
|
@@ -423,6 +423,136 @@ var init_sqlite_loader = __esm({
|
|
|
423
423
|
}
|
|
424
424
|
});
|
|
425
425
|
|
|
426
|
+
// src/db-driver.ts
|
|
427
|
+
import { createRequire as createRequire2 } from "module";
|
|
428
|
+
import { closeSync, existsSync as existsSync3, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync3, fsyncSync } from "fs";
|
|
429
|
+
import { tmpdir } from "os";
|
|
430
|
+
import { join as join2 } from "path";
|
|
431
|
+
function resolveDbEngine() {
|
|
432
|
+
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
433
|
+
}
|
|
434
|
+
function nodeSqliteCtor() {
|
|
435
|
+
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
436
|
+
return _nodeCtor;
|
|
437
|
+
}
|
|
438
|
+
function plainRow(row) {
|
|
439
|
+
if (row == null || typeof row !== "object") return row;
|
|
440
|
+
return { ...row };
|
|
441
|
+
}
|
|
442
|
+
function openNodeSqlite(dbPath, opts) {
|
|
443
|
+
const Ctor = nodeSqliteCtor();
|
|
444
|
+
const raw = new Ctor(dbPath, {
|
|
445
|
+
open: true,
|
|
446
|
+
readOnly: !!opts.readonly,
|
|
447
|
+
// FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
|
|
448
|
+
// verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
|
|
449
|
+
// tested against that default, so the adapter must match it, or a store that relies
|
|
450
|
+
// on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
|
|
451
|
+
// Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
|
|
452
|
+
enableForeignKeyConstraints: true,
|
|
453
|
+
allowExtension: false
|
|
454
|
+
});
|
|
455
|
+
let savepointSeq = 0;
|
|
456
|
+
const wrapStmt = (sql) => {
|
|
457
|
+
const st = raw.prepare(sql);
|
|
458
|
+
return {
|
|
459
|
+
run: (...p) => {
|
|
460
|
+
const r = st.run(...p);
|
|
461
|
+
return {
|
|
462
|
+
changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
|
|
463
|
+
lastInsertRowid: r.lastInsertRowid
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
get: (...p) => plainRow(st.get(...p)),
|
|
467
|
+
all: (...p) => st.all(...p).map(plainRow)
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
const pragma = (source, options) => {
|
|
471
|
+
const s = String(source).trim();
|
|
472
|
+
if (s.includes("=")) {
|
|
473
|
+
raw.exec(`PRAGMA ${s}`);
|
|
474
|
+
return void 0;
|
|
475
|
+
}
|
|
476
|
+
const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
|
|
477
|
+
if (options?.simple) {
|
|
478
|
+
const first = rows[0];
|
|
479
|
+
return first ? Object.values(first)[0] : void 0;
|
|
480
|
+
}
|
|
481
|
+
return rows;
|
|
482
|
+
};
|
|
483
|
+
const transaction = (fn) => {
|
|
484
|
+
const run = (...args) => {
|
|
485
|
+
const nested = raw.isTransaction;
|
|
486
|
+
const name = `msp_${savepointSeq++}`;
|
|
487
|
+
if (nested) raw.exec(`SAVEPOINT ${name}`);
|
|
488
|
+
else raw.exec("BEGIN");
|
|
489
|
+
try {
|
|
490
|
+
const out = fn(...args);
|
|
491
|
+
if (nested) raw.exec(`RELEASE ${name}`);
|
|
492
|
+
else raw.exec("COMMIT");
|
|
493
|
+
return out;
|
|
494
|
+
} catch (e) {
|
|
495
|
+
if (nested) {
|
|
496
|
+
raw.exec(`ROLLBACK TO ${name}`);
|
|
497
|
+
raw.exec(`RELEASE ${name}`);
|
|
498
|
+
} else {
|
|
499
|
+
raw.exec("ROLLBACK");
|
|
500
|
+
}
|
|
501
|
+
throw e;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
return run;
|
|
505
|
+
};
|
|
506
|
+
const serialize = () => {
|
|
507
|
+
if (raw.isTransaction) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
"db-driver: .serialize() (node:sqlite engine) cannot run inside an open transaction \u2014 VACUUM is forbidden in a transaction. Serialize before BEGIN, or use MASSU_DB_ENGINE=better-sqlite3 for mid-transaction snapshots."
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
|
|
513
|
+
const out = join2(dir, "snapshot.db");
|
|
514
|
+
try {
|
|
515
|
+
raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
|
|
516
|
+
const fd = openSync(out, "r");
|
|
517
|
+
try {
|
|
518
|
+
fsyncSync(fd);
|
|
519
|
+
} finally {
|
|
520
|
+
closeSync(fd);
|
|
521
|
+
}
|
|
522
|
+
return readFileSync3(out);
|
|
523
|
+
} finally {
|
|
524
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
const handle = {
|
|
528
|
+
prepare: (sql) => wrapStmt(sql),
|
|
529
|
+
exec: (sql) => raw.exec(sql),
|
|
530
|
+
pragma,
|
|
531
|
+
transaction,
|
|
532
|
+
serialize,
|
|
533
|
+
close: () => raw.close()
|
|
534
|
+
};
|
|
535
|
+
return handle;
|
|
536
|
+
}
|
|
537
|
+
function openBetterSqlite3(dbPath, opts) {
|
|
538
|
+
return openDatabase(dbPath, opts);
|
|
539
|
+
}
|
|
540
|
+
function openDatabase2(dbPath, opts = {}) {
|
|
541
|
+
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
542
|
+
}
|
|
543
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
544
|
+
var init_db_driver = __esm({
|
|
545
|
+
"src/db-driver.ts"() {
|
|
546
|
+
"use strict";
|
|
547
|
+
init_sqlite_loader();
|
|
548
|
+
init_sqlite_loader();
|
|
549
|
+
req2 = createRequire2(import.meta.url);
|
|
550
|
+
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
551
|
+
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
552
|
+
_nodeCtor = null;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
426
556
|
// src/lib/memory-path.ts
|
|
427
557
|
function encodeMemoryDirName(projectRoot) {
|
|
428
558
|
return projectRoot.replace(/\//g, "-");
|
|
@@ -566,6 +696,32 @@ var init_config_memory_schema = __esm({
|
|
|
566
696
|
// turn of EVERY session, so an unbounded index is a permanent context tax — and the
|
|
567
697
|
// per-session cap bounds only the RATE, never the total.
|
|
568
698
|
indexMaxLines: z.number().int().min(1).default(50)
|
|
699
|
+
}).default({}),
|
|
700
|
+
// --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
|
|
701
|
+
// A decision made in one of your repos can surface in another — opt-in per
|
|
702
|
+
// decision AND opt-in per repo, signed, verified, and materialized ONLY on
|
|
703
|
+
// explicit human acceptance. Local transport is FREE and zero-network.
|
|
704
|
+
//
|
|
705
|
+
// ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
|
|
706
|
+
// keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
|
|
707
|
+
// • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
|
|
708
|
+
// • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
|
|
709
|
+
// `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
|
|
710
|
+
// — a repo you never named is a repo Massu never reads from.
|
|
711
|
+
share: z.object({
|
|
712
|
+
enabled: z.boolean().default(false),
|
|
713
|
+
subscribe: z.array(z.string()).default([]),
|
|
714
|
+
// C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
|
|
715
|
+
// CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
|
|
716
|
+
// default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
|
|
717
|
+
// to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
|
|
718
|
+
// per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
|
|
719
|
+
// cross-repo items (they are, by construction, less relevant than local ones).
|
|
720
|
+
recall: z.object({
|
|
721
|
+
enabled: z.boolean().default(true),
|
|
722
|
+
maxCrossRepoItems: z.number().int().min(0).default(1),
|
|
723
|
+
minScore: z.number().optional()
|
|
724
|
+
}).default({})
|
|
569
725
|
}).default({})
|
|
570
726
|
}).optional();
|
|
571
727
|
}
|
|
@@ -573,7 +729,7 @@ var init_config_memory_schema = __esm({
|
|
|
573
729
|
|
|
574
730
|
// src/config.ts
|
|
575
731
|
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
576
|
-
import { existsSync as
|
|
732
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
577
733
|
import { homedir as homedir2 } from "os";
|
|
578
734
|
import { parse as parseYaml } from "yaml";
|
|
579
735
|
import { z as z2 } from "zod";
|
|
@@ -581,7 +737,7 @@ function findProjectRoot() {
|
|
|
581
737
|
const cwd = process.cwd();
|
|
582
738
|
let dir = cwd;
|
|
583
739
|
while (true) {
|
|
584
|
-
if (
|
|
740
|
+
if (existsSync4(resolve2(dir, "massu.config.yaml"))) {
|
|
585
741
|
return dir;
|
|
586
742
|
}
|
|
587
743
|
const parent = dirname3(dir);
|
|
@@ -590,10 +746,10 @@ function findProjectRoot() {
|
|
|
590
746
|
}
|
|
591
747
|
dir = cwd;
|
|
592
748
|
while (true) {
|
|
593
|
-
if (
|
|
749
|
+
if (existsSync4(resolve2(dir, "package.json"))) {
|
|
594
750
|
return dir;
|
|
595
751
|
}
|
|
596
|
-
if (
|
|
752
|
+
if (existsSync4(resolve2(dir, ".git"))) {
|
|
597
753
|
return dir;
|
|
598
754
|
}
|
|
599
755
|
const parent = dirname3(dir);
|
|
@@ -613,8 +769,8 @@ function getConfig() {
|
|
|
613
769
|
const root = getProjectRoot();
|
|
614
770
|
const configPath = resolve2(root, "massu.config.yaml");
|
|
615
771
|
let rawYaml = {};
|
|
616
|
-
if (
|
|
617
|
-
const content =
|
|
772
|
+
if (existsSync4(configPath)) {
|
|
773
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
618
774
|
rawYaml = parseYaml(content) ?? {};
|
|
619
775
|
}
|
|
620
776
|
const result = RawConfigSchema.safeParse(rawYaml);
|
|
@@ -742,6 +898,7 @@ function getResolvedPaths() {
|
|
|
742
898
|
sessionStatePath: resolve2(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
743
899
|
sessionArchivePath: resolve2(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
744
900
|
mcpJsonPath: resolve2(root, ".mcp.json"),
|
|
901
|
+
settingsPath: resolve2(root, claudeDirName, "settings.json"),
|
|
745
902
|
settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
|
|
746
903
|
};
|
|
747
904
|
}
|
|
@@ -819,7 +976,6 @@ var init_config = __esm({
|
|
|
819
976
|
custom_patterns: z2.array(CustomPatternSchema).default([])
|
|
820
977
|
}).optional(),
|
|
821
978
|
adr: z2.object({
|
|
822
|
-
detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
|
|
823
979
|
template: z2.string().default("default"),
|
|
824
980
|
storage: z2.string().default("database"),
|
|
825
981
|
output_dir: z2.string().default("docs/adr")
|
|
@@ -1173,26 +1329,26 @@ var init_memory_vector = __esm({
|
|
|
1173
1329
|
});
|
|
1174
1330
|
|
|
1175
1331
|
// src/db-backup.ts
|
|
1176
|
-
import { existsSync as
|
|
1177
|
-
import { resolve as resolve3, join as
|
|
1332
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1333
|
+
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
1178
1334
|
import { homedir as homedir3 } from "os";
|
|
1179
1335
|
function dbBackupsRoot(home = homedir3()) {
|
|
1180
1336
|
return resolve3(home, ".massu", "db-backups");
|
|
1181
1337
|
}
|
|
1182
1338
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
1183
1339
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
1184
|
-
return
|
|
1340
|
+
return join3(dbBackupsRoot(home), slug);
|
|
1185
1341
|
}
|
|
1186
1342
|
function backupStamp(nowMs) {
|
|
1187
1343
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
1188
1344
|
}
|
|
1189
1345
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
1190
1346
|
const dir = projectBackupDir(projectRoot, home);
|
|
1191
|
-
if (!
|
|
1347
|
+
if (!existsSync5(dir)) return [];
|
|
1192
1348
|
const out = [];
|
|
1193
1349
|
for (const f of readdirSync(dir)) {
|
|
1194
1350
|
if (!f.endsWith(".db")) continue;
|
|
1195
|
-
const p =
|
|
1351
|
+
const p = join3(dir, f);
|
|
1196
1352
|
const st = statSync(p);
|
|
1197
1353
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
1198
1354
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -1205,13 +1361,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
1205
1361
|
if (backups.length === 0) return false;
|
|
1206
1362
|
const newest = backups[0];
|
|
1207
1363
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
1208
|
-
if (
|
|
1364
|
+
if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
1209
1365
|
return true;
|
|
1210
1366
|
}
|
|
1211
1367
|
function integrityOk(dbPath) {
|
|
1212
1368
|
let db = null;
|
|
1213
1369
|
try {
|
|
1214
|
-
db =
|
|
1370
|
+
db = openDatabase2(dbPath, { readonly: true });
|
|
1215
1371
|
const rows = db.pragma("integrity_check");
|
|
1216
1372
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
1217
1373
|
} catch {
|
|
@@ -1224,16 +1380,16 @@ function integrityOk(dbPath) {
|
|
|
1224
1380
|
}
|
|
1225
1381
|
}
|
|
1226
1382
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
1227
|
-
if (!
|
|
1383
|
+
if (!existsSync5(dbPath)) {
|
|
1228
1384
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
1229
1385
|
}
|
|
1230
1386
|
const dir = projectBackupDir(projectRoot, home);
|
|
1231
1387
|
mkdirSync4(dir, { recursive: true });
|
|
1232
1388
|
const name = basename(dbPath, ".db");
|
|
1233
|
-
const dest =
|
|
1389
|
+
const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
1234
1390
|
let src = null;
|
|
1235
1391
|
try {
|
|
1236
|
-
src =
|
|
1392
|
+
src = openDatabase2(dbPath, { readonly: true });
|
|
1237
1393
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
1238
1394
|
} catch (err) {
|
|
1239
1395
|
throw new DbBackupError(
|
|
@@ -1272,7 +1428,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
1272
1428
|
}
|
|
1273
1429
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
1274
1430
|
try {
|
|
1275
|
-
if (!
|
|
1431
|
+
if (!existsSync5(dbPath)) return null;
|
|
1276
1432
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
1277
1433
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
1278
1434
|
} catch (err) {
|
|
@@ -1284,7 +1440,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
1284
1440
|
var init_db_backup = __esm({
|
|
1285
1441
|
"src/db-backup.ts"() {
|
|
1286
1442
|
"use strict";
|
|
1287
|
-
|
|
1443
|
+
init_db_driver();
|
|
1288
1444
|
DEFAULT_RETENTION = 5;
|
|
1289
1445
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1290
1446
|
DbBackupError = class extends Error {
|
|
@@ -1372,8 +1528,8 @@ var init_rule_delivery = __esm({
|
|
|
1372
1528
|
});
|
|
1373
1529
|
|
|
1374
1530
|
// src/rule-candidate-store.ts
|
|
1375
|
-
import { existsSync as
|
|
1376
|
-
import { join as
|
|
1531
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
1532
|
+
import { join as join4 } from "node:path";
|
|
1377
1533
|
function ensureRuleCandidatesTable(db) {
|
|
1378
1534
|
db.exec(`
|
|
1379
1535
|
CREATE TABLE IF NOT EXISTS rule_candidates (
|
|
@@ -1393,7 +1549,7 @@ function ensureRuleCandidatesTable(db) {
|
|
|
1393
1549
|
`);
|
|
1394
1550
|
}
|
|
1395
1551
|
function sidecarPath(projectRoot, promptHash) {
|
|
1396
|
-
return
|
|
1552
|
+
return join4(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
1397
1553
|
}
|
|
1398
1554
|
function upsertCandidate(db, projectRoot, candidate, opts = {}) {
|
|
1399
1555
|
ensureRuleCandidatesTable(db);
|
|
@@ -1408,10 +1564,10 @@ function upsertCandidate(db, projectRoot, candidate, opts = {}) {
|
|
|
1408
1564
|
origin = excluded.origin,
|
|
1409
1565
|
updated_at = datetime('now')
|
|
1410
1566
|
`).run(candidate.prompt_hash, origin, score, JSON.stringify(candidate));
|
|
1411
|
-
const dir =
|
|
1567
|
+
const dir = join4(projectRoot, ".massu", "rule-candidates");
|
|
1412
1568
|
mkdirSync5(dir, { recursive: true });
|
|
1413
1569
|
const path = sidecarPath(projectRoot, candidate.prompt_hash);
|
|
1414
|
-
if (!
|
|
1570
|
+
if (!existsSync6(path)) {
|
|
1415
1571
|
writeFileSync3(path, JSON.stringify(candidate, null, 2), "utf-8");
|
|
1416
1572
|
}
|
|
1417
1573
|
}
|
|
@@ -1422,21 +1578,21 @@ var init_rule_candidate_store = __esm({
|
|
|
1422
1578
|
});
|
|
1423
1579
|
|
|
1424
1580
|
// src/hooks/lib/hook-failure-signal.ts
|
|
1425
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as
|
|
1426
|
-
import { join as
|
|
1581
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync7 } from "fs";
|
|
1582
|
+
import { join as join5, dirname as dirname5 } from "path";
|
|
1427
1583
|
function resolveFailureLogPath() {
|
|
1428
1584
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
1429
1585
|
if (explicit) return explicit;
|
|
1430
1586
|
let dir = process.cwd();
|
|
1431
1587
|
for (let i = 0; i < 12; i++) {
|
|
1432
|
-
if (
|
|
1433
|
-
return
|
|
1588
|
+
if (existsSync7(join5(dir, ".massu")) || existsSync7(join5(dir, "massu.config.yaml"))) {
|
|
1589
|
+
return join5(dir, ".massu", "hook-failures.jsonl");
|
|
1434
1590
|
}
|
|
1435
1591
|
const parent = dirname5(dir);
|
|
1436
1592
|
if (parent === dir) break;
|
|
1437
1593
|
dir = parent;
|
|
1438
1594
|
}
|
|
1439
|
-
return
|
|
1595
|
+
return join5(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
1440
1596
|
}
|
|
1441
1597
|
function recordHookFailure(hook, error, context) {
|
|
1442
1598
|
const record = {
|
|
@@ -1493,9 +1649,9 @@ var init_hook_failure_signal = __esm({
|
|
|
1493
1649
|
});
|
|
1494
1650
|
|
|
1495
1651
|
// src/memory-embedder-tokenizer.ts
|
|
1496
|
-
import { readFileSync as
|
|
1652
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1497
1653
|
function loadVocab(vocabPath) {
|
|
1498
|
-
const lines =
|
|
1654
|
+
const lines = readFileSync6(vocabPath, "utf-8").split("\n");
|
|
1499
1655
|
const vocab = /* @__PURE__ */ new Map();
|
|
1500
1656
|
for (let i = 0; i < lines.length; i++) {
|
|
1501
1657
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1615,9 +1771,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1615
1771
|
|
|
1616
1772
|
// src/memory-embedder.ts
|
|
1617
1773
|
import { fileURLToPath } from "url";
|
|
1618
|
-
import { dirname as dirname6, join as
|
|
1619
|
-
import { existsSync as
|
|
1620
|
-
import { createRequire as
|
|
1774
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
1775
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1776
|
+
import { createRequire as createRequire3 } from "module";
|
|
1621
1777
|
function getActiveEmbedModel() {
|
|
1622
1778
|
return _activeModel;
|
|
1623
1779
|
}
|
|
@@ -1641,7 +1797,7 @@ function resolveModelDir() {
|
|
|
1641
1797
|
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
1642
1798
|
let root = null;
|
|
1643
1799
|
for (let i = 0; i < 8; i++) {
|
|
1644
|
-
if (
|
|
1800
|
+
if (existsSync8(join6(dir, "package.json"))) {
|
|
1645
1801
|
root = dir;
|
|
1646
1802
|
break;
|
|
1647
1803
|
}
|
|
@@ -1651,8 +1807,8 @@ function resolveModelDir() {
|
|
|
1651
1807
|
}
|
|
1652
1808
|
if (!root) return null;
|
|
1653
1809
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1654
|
-
const candidate =
|
|
1655
|
-
if (
|
|
1810
|
+
const candidate = join6(root, rel);
|
|
1811
|
+
if (existsSync8(join6(candidate, "model_quantized.onnx"))) return candidate;
|
|
1656
1812
|
}
|
|
1657
1813
|
return null;
|
|
1658
1814
|
} catch {
|
|
@@ -1661,8 +1817,8 @@ function resolveModelDir() {
|
|
|
1661
1817
|
}
|
|
1662
1818
|
function resolveWasmDir() {
|
|
1663
1819
|
try {
|
|
1664
|
-
const
|
|
1665
|
-
const main2 =
|
|
1820
|
+
const req3 = createRequire3(import.meta.url);
|
|
1821
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1666
1822
|
return dirname6(main2);
|
|
1667
1823
|
} catch {
|
|
1668
1824
|
return null;
|
|
@@ -1683,8 +1839,8 @@ async function getSession() {
|
|
|
1683
1839
|
ort.env.wasm.proxy = false;
|
|
1684
1840
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1685
1841
|
_ortTensor = ort.Tensor;
|
|
1686
|
-
const modelPath =
|
|
1687
|
-
const vocabPath =
|
|
1842
|
+
const modelPath = join6(modelDir, "model_quantized.onnx");
|
|
1843
|
+
const vocabPath = join6(modelDir, "vocab.txt");
|
|
1688
1844
|
_vocab = loadVocab(vocabPath);
|
|
1689
1845
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1690
1846
|
executionProviders: ["wasm"],
|
|
@@ -2005,6 +2161,7 @@ __export(memory_db_exports, {
|
|
|
2005
2161
|
migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
|
|
2006
2162
|
migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
|
|
2007
2163
|
migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
|
|
2164
|
+
migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
|
|
2008
2165
|
pruneOldConversationTurns: () => pruneOldConversationTurns,
|
|
2009
2166
|
pruneOldObservations: () => pruneOldObservations,
|
|
2010
2167
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
@@ -2022,7 +2179,7 @@ __export(memory_db_exports, {
|
|
|
2022
2179
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
2023
2180
|
});
|
|
2024
2181
|
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2025
|
-
import { existsSync as
|
|
2182
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
|
|
2026
2183
|
function sanitizeFts5Query(raw) {
|
|
2027
2184
|
const trimmed = raw.trim();
|
|
2028
2185
|
if (!trimmed) return '""';
|
|
@@ -2039,11 +2196,11 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
2039
2196
|
function getMemoryDb() {
|
|
2040
2197
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
2041
2198
|
const dir = dirname7(dbPath);
|
|
2042
|
-
if (!
|
|
2199
|
+
if (!existsSync9(dir)) {
|
|
2043
2200
|
mkdirSync7(dir, { recursive: true });
|
|
2044
2201
|
}
|
|
2045
|
-
const preExisting =
|
|
2046
|
-
const db =
|
|
2202
|
+
const preExisting = existsSync9(dbPath);
|
|
2203
|
+
const db = openDatabase2(dbPath);
|
|
2047
2204
|
db.pragma("journal_mode = WAL");
|
|
2048
2205
|
db.pragma("foreign_keys = ON");
|
|
2049
2206
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -2119,6 +2276,39 @@ function migrateMemoryFilesFor4B(db) {
|
|
|
2119
2276
|
db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
2120
2277
|
}
|
|
2121
2278
|
}
|
|
2279
|
+
function migrateSharedMemoryFor5B(db) {
|
|
2280
|
+
const cols = db.prepare(`PRAGMA table_info(observations)`).all();
|
|
2281
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
|
|
2282
|
+
db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
|
|
2283
|
+
}
|
|
2284
|
+
db.exec(`
|
|
2285
|
+
CREATE TABLE IF NOT EXISTS shared_memory_pending (
|
|
2286
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2287
|
+
record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
|
|
2288
|
+
origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
|
|
2289
|
+
origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
|
|
2290
|
+
envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
|
|
2291
|
+
record_json TEXT NOT NULL, -- the single record's canonical JSON
|
|
2292
|
+
received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
|
|
2293
|
+
accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
|
|
2294
|
+
refused_at_epoch INTEGER, -- set on refuse (B-06)
|
|
2295
|
+
expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
|
|
2296
|
+
);
|
|
2297
|
+
CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
|
|
2298
|
+
CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
|
|
2299
|
+
`);
|
|
2300
|
+
db.exec(`
|
|
2301
|
+
CREATE TABLE IF NOT EXISTS shared_memory_outbound (
|
|
2302
|
+
record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
|
|
2303
|
+
observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
|
|
2304
|
+
origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
|
|
2305
|
+
seq INTEGER NOT NULL, -- the envelope seq this record first went out in
|
|
2306
|
+
exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
|
|
2307
|
+
revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
|
|
2308
|
+
);
|
|
2309
|
+
CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
|
|
2310
|
+
`);
|
|
2311
|
+
}
|
|
2122
2312
|
function migrateAuditLogCheckExtension(db) {
|
|
2123
2313
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
2124
2314
|
if (!row) return;
|
|
@@ -2141,7 +2331,16 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
2141
2331
|
"memory_file_adopted_human",
|
|
2142
2332
|
"memory_file_rendered",
|
|
2143
2333
|
"memory_file_render_refused",
|
|
2144
|
-
"memory_file_tombstoned"
|
|
2334
|
+
"memory_file_tombstoned",
|
|
2335
|
+
// B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
|
|
2336
|
+
// CHECK lacks any of these triggers the table rebuild below.
|
|
2337
|
+
"shared_memory_exported",
|
|
2338
|
+
"shared_memory_export_refused",
|
|
2339
|
+
"shared_memory_imported",
|
|
2340
|
+
"shared_memory_dropped",
|
|
2341
|
+
"shared_memory_accepted",
|
|
2342
|
+
"shared_memory_refused",
|
|
2343
|
+
"shared_memory_revoked"
|
|
2145
2344
|
];
|
|
2146
2345
|
const checkClauseMatch = row.sql.match(/event_type\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*event_type\s+IN\s*\(([\s\S]*?)\)\s*\)/i);
|
|
2147
2346
|
if (checkClauseMatch) {
|
|
@@ -2160,7 +2359,12 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
2160
2359
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2161
2360
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2162
2361
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2163
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2362
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2363
|
+
-- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
|
|
2364
|
+
-- the expected[] list above, or the migration would loop rebuilding forever).
|
|
2365
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2366
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2367
|
+
'shared_memory_revoked'
|
|
2164
2368
|
)),
|
|
2165
2369
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2166
2370
|
model_id TEXT,
|
|
@@ -2237,6 +2441,12 @@ function initMemorySchema(db) {
|
|
|
2237
2441
|
-- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
|
|
2238
2442
|
-- origin='team' and must never reach disk without CR-55's gates.
|
|
2239
2443
|
origin TEXT NOT NULL DEFAULT 'local',
|
|
2444
|
+
-- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
|
|
2445
|
+
-- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
|
|
2446
|
+
-- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
|
|
2447
|
+
-- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
|
|
2448
|
+
-- un-shareable until a human says otherwise (fail-closed).
|
|
2449
|
+
shareable INTEGER NOT NULL DEFAULT 0,
|
|
2240
2450
|
created_at TEXT NOT NULL,
|
|
2241
2451
|
created_at_epoch INTEGER NOT NULL,
|
|
2242
2452
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -2510,7 +2720,13 @@ function initMemorySchema(db) {
|
|
|
2510
2720
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2511
2721
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2512
2722
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2513
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2723
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2724
|
+
-- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
|
|
2725
|
+
-- nobody could ever answer "what has crossed between my repos?" (and an ingest
|
|
2726
|
+
-- catch would swallow the CHECK violation, silently producing NOTHING).
|
|
2727
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2728
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2729
|
+
'shared_memory_revoked'
|
|
2514
2730
|
)),
|
|
2515
2731
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2516
2732
|
model_id TEXT,
|
|
@@ -2879,6 +3095,7 @@ function initMemorySchema(db) {
|
|
|
2879
3095
|
`);
|
|
2880
3096
|
migrateObservationEmbeddingChunks(db);
|
|
2881
3097
|
migrateMemoryFilesFor4B(db);
|
|
3098
|
+
migrateSharedMemoryFor5B(db);
|
|
2882
3099
|
ensureRuleCandidatesTable(db);
|
|
2883
3100
|
migrateRuleDelivery(db);
|
|
2884
3101
|
}
|
|
@@ -3749,7 +3966,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3749
3966
|
var init_memory_db = __esm({
|
|
3750
3967
|
"src/memory-db.ts"() {
|
|
3751
3968
|
"use strict";
|
|
3752
|
-
|
|
3969
|
+
init_db_driver();
|
|
3753
3970
|
init_config();
|
|
3754
3971
|
init_memory_vector();
|
|
3755
3972
|
init_db_backup();
|
|
@@ -3771,10 +3988,10 @@ var init_memory_db = __esm({
|
|
|
3771
3988
|
|
|
3772
3989
|
// src/hooks/user-prompt.ts
|
|
3773
3990
|
init_memory_db();
|
|
3774
|
-
|
|
3991
|
+
init_db_driver();
|
|
3775
3992
|
init_config();
|
|
3776
|
-
import { existsSync as
|
|
3777
|
-
import { join as
|
|
3993
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync4, readFileSync as readFileSync7, readdirSync as readdirSync3, openSync as openSync2, fstatSync, readSync, closeSync as closeSync2 } from "fs";
|
|
3994
|
+
import { join as join7 } from "path";
|
|
3778
3995
|
|
|
3779
3996
|
// src/rule-candidate-detector.ts
|
|
3780
3997
|
var CORRECTION_DISMISSAL_PATTERN = /\b(nevermind|never\s+mind|forget\s+it|ignore\s+(that|it)|actually\s+you('?re|\s+were)\s+right|on\s+second\s+thought|no\s+wait|scratch\s+that|disregard|abandon\s+(that|it))\b/i;
|
|
@@ -4115,8 +4332,8 @@ async function main() {
|
|
|
4115
4332
|
const fileRefs = extractFileReferences(prompt);
|
|
4116
4333
|
if (fileRefs.length > 0) {
|
|
4117
4334
|
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
4118
|
-
if (knowledgeDbPath &&
|
|
4119
|
-
const kdb =
|
|
4335
|
+
if (knowledgeDbPath && existsSync10(knowledgeDbPath)) {
|
|
4336
|
+
const kdb = openDatabase2(knowledgeDbPath, { readonly: true, selfHeal: false });
|
|
4120
4337
|
try {
|
|
4121
4338
|
const placeholders = fileRefs.map(() => "?").join(",");
|
|
4122
4339
|
const matches = kdb.prepare(
|
|
@@ -4168,14 +4385,14 @@ async function main() {
|
|
|
4168
4385
|
category: categorizePrompt(prompt),
|
|
4169
4386
|
blacklist
|
|
4170
4387
|
});
|
|
4171
|
-
const candidateDir =
|
|
4388
|
+
const candidateDir = join7(hookInput.cwd, ".massu", "rule-candidates");
|
|
4172
4389
|
if (scoreResult.emitCandidate) {
|
|
4173
4390
|
const cachedTier = getCachedTierReadOnly(db);
|
|
4174
4391
|
if (entitledForAutoLearning(cachedTier)) {
|
|
4175
4392
|
mkdirSync8(candidateDir, { recursive: true });
|
|
4176
4393
|
const promptHash = hashPrompt(prompt);
|
|
4177
|
-
const candidatePath =
|
|
4178
|
-
if (!
|
|
4394
|
+
const candidatePath = join7(candidateDir, `${promptHash}.json`);
|
|
4395
|
+
if (!existsSync10(candidatePath)) {
|
|
4179
4396
|
upsertCandidate(db, hookInput.cwd, {
|
|
4180
4397
|
prompt,
|
|
4181
4398
|
prompt_hash: promptHash,
|
|
@@ -4199,8 +4416,8 @@ async function main() {
|
|
|
4199
4416
|
}
|
|
4200
4417
|
}
|
|
4201
4418
|
} else {
|
|
4202
|
-
const nudgePath =
|
|
4203
|
-
if (!
|
|
4419
|
+
const nudgePath = join7(candidateDir, ".last-tier-nudge");
|
|
4420
|
+
if (!existsSync10(nudgePath)) {
|
|
4204
4421
|
mkdirSync8(candidateDir, { recursive: true });
|
|
4205
4422
|
process.stderr.write(
|
|
4206
4423
|
`
|
|
@@ -4212,15 +4429,15 @@ async function main() {
|
|
|
4212
4429
|
}
|
|
4213
4430
|
}
|
|
4214
4431
|
}
|
|
4215
|
-
if (
|
|
4432
|
+
if (existsSync10(candidateDir)) {
|
|
4216
4433
|
const candidates = readdirSync3(candidateDir).filter(
|
|
4217
4434
|
(f) => f.endsWith(".json") && !f.startsWith(".")
|
|
4218
4435
|
);
|
|
4219
4436
|
const candidateCount = candidates.length;
|
|
4220
|
-
const surfacedPath =
|
|
4437
|
+
const surfacedPath = join7(candidateDir, ".last-surfaced");
|
|
4221
4438
|
let lastSurfaced = 0;
|
|
4222
|
-
if (
|
|
4223
|
-
const raw =
|
|
4439
|
+
if (existsSync10(surfacedPath)) {
|
|
4440
|
+
const raw = readFileSync7(surfacedPath, "utf-8").trim();
|
|
4224
4441
|
const parsed = parseInt(raw, 10);
|
|
4225
4442
|
if (!Number.isNaN(parsed)) lastSurfaced = parsed;
|
|
4226
4443
|
}
|
|
@@ -4236,10 +4453,10 @@ async function main() {
|
|
|
4236
4453
|
}
|
|
4237
4454
|
} catch (candidateErr) {
|
|
4238
4455
|
try {
|
|
4239
|
-
const dir =
|
|
4240
|
-
if (!
|
|
4241
|
-
const logPath =
|
|
4242
|
-
const pre =
|
|
4456
|
+
const dir = join7(hookInput.cwd, ".massu", "rule-candidates");
|
|
4457
|
+
if (!existsSync10(dir)) mkdirSync8(dir, { recursive: true });
|
|
4458
|
+
const logPath = join7(dir, ".detector-failures.jsonl");
|
|
4459
|
+
const pre = existsSync10(logPath) ? readFileSync7(logPath, "utf-8") : "";
|
|
4243
4460
|
const sep = pre && !pre.endsWith("\n") ? "\n" : "";
|
|
4244
4461
|
writeFileSync4(logPath, pre + sep + JSON.stringify({
|
|
4245
4462
|
session_id,
|
|
@@ -4262,13 +4479,13 @@ async function main() {
|
|
|
4262
4479
|
}
|
|
4263
4480
|
function detectPriorAssistantTurn(transcriptPath) {
|
|
4264
4481
|
try {
|
|
4265
|
-
if (!transcriptPath || !
|
|
4482
|
+
if (!transcriptPath || !existsSync10(transcriptPath)) {
|
|
4266
4483
|
return { hadEditOrWrite: false, files: [] };
|
|
4267
4484
|
}
|
|
4268
4485
|
if (!transcriptPath.includes("/.claude/projects/")) {
|
|
4269
4486
|
return { hadEditOrWrite: false, files: [] };
|
|
4270
4487
|
}
|
|
4271
|
-
const fd =
|
|
4488
|
+
const fd = openSync2(transcriptPath, "r");
|
|
4272
4489
|
let buf;
|
|
4273
4490
|
try {
|
|
4274
4491
|
const stats = fstatSync(fd);
|
|
@@ -4277,7 +4494,7 @@ function detectPriorAssistantTurn(transcriptPath) {
|
|
|
4277
4494
|
buf = Buffer.alloc(readLen);
|
|
4278
4495
|
readSync(fd, buf, 0, readLen, offset);
|
|
4279
4496
|
} finally {
|
|
4280
|
-
|
|
4497
|
+
closeSync2(fd);
|
|
4281
4498
|
}
|
|
4282
4499
|
const lines = buf.toString("utf-8").split("\n").filter(Boolean);
|
|
4283
4500
|
const files = [];
|