@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")
|
|
@@ -1194,26 +1350,26 @@ var init_memory_vector = __esm({
|
|
|
1194
1350
|
});
|
|
1195
1351
|
|
|
1196
1352
|
// src/db-backup.ts
|
|
1197
|
-
import { existsSync as
|
|
1198
|
-
import { resolve as resolve3, join as
|
|
1353
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1354
|
+
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
1199
1355
|
import { homedir as homedir3 } from "os";
|
|
1200
1356
|
function dbBackupsRoot(home = homedir3()) {
|
|
1201
1357
|
return resolve3(home, ".massu", "db-backups");
|
|
1202
1358
|
}
|
|
1203
1359
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
1204
1360
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
1205
|
-
return
|
|
1361
|
+
return join3(dbBackupsRoot(home), slug);
|
|
1206
1362
|
}
|
|
1207
1363
|
function backupStamp(nowMs) {
|
|
1208
1364
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
1209
1365
|
}
|
|
1210
1366
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
1211
1367
|
const dir = projectBackupDir(projectRoot, home);
|
|
1212
|
-
if (!
|
|
1368
|
+
if (!existsSync5(dir)) return [];
|
|
1213
1369
|
const out = [];
|
|
1214
1370
|
for (const f of readdirSync(dir)) {
|
|
1215
1371
|
if (!f.endsWith(".db")) continue;
|
|
1216
|
-
const p =
|
|
1372
|
+
const p = join3(dir, f);
|
|
1217
1373
|
const st = statSync(p);
|
|
1218
1374
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
1219
1375
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -1226,13 +1382,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
1226
1382
|
if (backups.length === 0) return false;
|
|
1227
1383
|
const newest = backups[0];
|
|
1228
1384
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
1229
|
-
if (
|
|
1385
|
+
if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
1230
1386
|
return true;
|
|
1231
1387
|
}
|
|
1232
1388
|
function integrityOk(dbPath) {
|
|
1233
1389
|
let db = null;
|
|
1234
1390
|
try {
|
|
1235
|
-
db =
|
|
1391
|
+
db = openDatabase2(dbPath, { readonly: true });
|
|
1236
1392
|
const rows = db.pragma("integrity_check");
|
|
1237
1393
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
1238
1394
|
} catch {
|
|
@@ -1245,16 +1401,16 @@ function integrityOk(dbPath) {
|
|
|
1245
1401
|
}
|
|
1246
1402
|
}
|
|
1247
1403
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
1248
|
-
if (!
|
|
1404
|
+
if (!existsSync5(dbPath)) {
|
|
1249
1405
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
1250
1406
|
}
|
|
1251
1407
|
const dir = projectBackupDir(projectRoot, home);
|
|
1252
1408
|
mkdirSync4(dir, { recursive: true });
|
|
1253
1409
|
const name = basename(dbPath, ".db");
|
|
1254
|
-
const dest =
|
|
1410
|
+
const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
1255
1411
|
let src = null;
|
|
1256
1412
|
try {
|
|
1257
|
-
src =
|
|
1413
|
+
src = openDatabase2(dbPath, { readonly: true });
|
|
1258
1414
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
1259
1415
|
} catch (err) {
|
|
1260
1416
|
throw new DbBackupError(
|
|
@@ -1293,7 +1449,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
1293
1449
|
}
|
|
1294
1450
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
1295
1451
|
try {
|
|
1296
|
-
if (!
|
|
1452
|
+
if (!existsSync5(dbPath)) return null;
|
|
1297
1453
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
1298
1454
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
1299
1455
|
} catch (err) {
|
|
@@ -1305,7 +1461,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
1305
1461
|
var init_db_backup = __esm({
|
|
1306
1462
|
"src/db-backup.ts"() {
|
|
1307
1463
|
"use strict";
|
|
1308
|
-
|
|
1464
|
+
init_db_driver();
|
|
1309
1465
|
DEFAULT_RETENTION = 5;
|
|
1310
1466
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1311
1467
|
DbBackupError = class extends Error {
|
|
@@ -1565,21 +1721,21 @@ var init_rule_candidate_store = __esm({
|
|
|
1565
1721
|
});
|
|
1566
1722
|
|
|
1567
1723
|
// src/hooks/lib/hook-failure-signal.ts
|
|
1568
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as
|
|
1569
|
-
import { join as
|
|
1724
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
|
|
1725
|
+
import { join as join4, dirname as dirname5 } from "path";
|
|
1570
1726
|
function resolveFailureLogPath() {
|
|
1571
1727
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
1572
1728
|
if (explicit) return explicit;
|
|
1573
1729
|
let dir = process.cwd();
|
|
1574
1730
|
for (let i = 0; i < 12; i++) {
|
|
1575
|
-
if (
|
|
1576
|
-
return
|
|
1731
|
+
if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
|
|
1732
|
+
return join4(dir, ".massu", "hook-failures.jsonl");
|
|
1577
1733
|
}
|
|
1578
1734
|
const parent = dirname5(dir);
|
|
1579
1735
|
if (parent === dir) break;
|
|
1580
1736
|
dir = parent;
|
|
1581
1737
|
}
|
|
1582
|
-
return
|
|
1738
|
+
return join4(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
1583
1739
|
}
|
|
1584
1740
|
function recordHookFailure(hook, error, context) {
|
|
1585
1741
|
const record = {
|
|
@@ -1636,9 +1792,9 @@ var init_hook_failure_signal = __esm({
|
|
|
1636
1792
|
});
|
|
1637
1793
|
|
|
1638
1794
|
// src/memory-embedder-tokenizer.ts
|
|
1639
|
-
import { readFileSync as
|
|
1795
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1640
1796
|
function loadVocab(vocabPath) {
|
|
1641
|
-
const lines =
|
|
1797
|
+
const lines = readFileSync5(vocabPath, "utf-8").split("\n");
|
|
1642
1798
|
const vocab = /* @__PURE__ */ new Map();
|
|
1643
1799
|
for (let i = 0; i < lines.length; i++) {
|
|
1644
1800
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1758,9 +1914,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1758
1914
|
|
|
1759
1915
|
// src/memory-embedder.ts
|
|
1760
1916
|
import { fileURLToPath } from "url";
|
|
1761
|
-
import { dirname as dirname6, join as
|
|
1762
|
-
import { existsSync as
|
|
1763
|
-
import { createRequire as
|
|
1917
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
1918
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1919
|
+
import { createRequire as createRequire3 } from "module";
|
|
1764
1920
|
function getActiveEmbedModel() {
|
|
1765
1921
|
return _activeModel;
|
|
1766
1922
|
}
|
|
@@ -1784,7 +1940,7 @@ function resolveModelDir() {
|
|
|
1784
1940
|
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
1785
1941
|
let root = null;
|
|
1786
1942
|
for (let i = 0; i < 8; i++) {
|
|
1787
|
-
if (
|
|
1943
|
+
if (existsSync7(join5(dir, "package.json"))) {
|
|
1788
1944
|
root = dir;
|
|
1789
1945
|
break;
|
|
1790
1946
|
}
|
|
@@ -1794,8 +1950,8 @@ function resolveModelDir() {
|
|
|
1794
1950
|
}
|
|
1795
1951
|
if (!root) return null;
|
|
1796
1952
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1797
|
-
const candidate =
|
|
1798
|
-
if (
|
|
1953
|
+
const candidate = join5(root, rel);
|
|
1954
|
+
if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
|
|
1799
1955
|
}
|
|
1800
1956
|
return null;
|
|
1801
1957
|
} catch {
|
|
@@ -1804,8 +1960,8 @@ function resolveModelDir() {
|
|
|
1804
1960
|
}
|
|
1805
1961
|
function resolveWasmDir() {
|
|
1806
1962
|
try {
|
|
1807
|
-
const
|
|
1808
|
-
const main2 =
|
|
1963
|
+
const req3 = createRequire3(import.meta.url);
|
|
1964
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1809
1965
|
return dirname6(main2);
|
|
1810
1966
|
} catch {
|
|
1811
1967
|
return null;
|
|
@@ -1826,8 +1982,8 @@ async function getSession() {
|
|
|
1826
1982
|
ort.env.wasm.proxy = false;
|
|
1827
1983
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1828
1984
|
_ortTensor = ort.Tensor;
|
|
1829
|
-
const modelPath =
|
|
1830
|
-
const vocabPath =
|
|
1985
|
+
const modelPath = join5(modelDir, "model_quantized.onnx");
|
|
1986
|
+
const vocabPath = join5(modelDir, "vocab.txt");
|
|
1831
1987
|
_vocab = loadVocab(vocabPath);
|
|
1832
1988
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1833
1989
|
executionProviders: ["wasm"],
|
|
@@ -2163,6 +2319,7 @@ __export(memory_db_exports, {
|
|
|
2163
2319
|
migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
|
|
2164
2320
|
migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
|
|
2165
2321
|
migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
|
|
2322
|
+
migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
|
|
2166
2323
|
pruneOldConversationTurns: () => pruneOldConversationTurns,
|
|
2167
2324
|
pruneOldObservations: () => pruneOldObservations,
|
|
2168
2325
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
@@ -2180,7 +2337,7 @@ __export(memory_db_exports, {
|
|
|
2180
2337
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
2181
2338
|
});
|
|
2182
2339
|
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2183
|
-
import { existsSync as
|
|
2340
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
|
|
2184
2341
|
function sanitizeFts5Query(raw) {
|
|
2185
2342
|
const trimmed = raw.trim();
|
|
2186
2343
|
if (!trimmed) return '""';
|
|
@@ -2197,11 +2354,11 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
2197
2354
|
function getMemoryDb() {
|
|
2198
2355
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
2199
2356
|
const dir = dirname7(dbPath);
|
|
2200
|
-
if (!
|
|
2357
|
+
if (!existsSync8(dir)) {
|
|
2201
2358
|
mkdirSync6(dir, { recursive: true });
|
|
2202
2359
|
}
|
|
2203
|
-
const preExisting =
|
|
2204
|
-
const db =
|
|
2360
|
+
const preExisting = existsSync8(dbPath);
|
|
2361
|
+
const db = openDatabase2(dbPath);
|
|
2205
2362
|
db.pragma("journal_mode = WAL");
|
|
2206
2363
|
db.pragma("foreign_keys = ON");
|
|
2207
2364
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -2277,6 +2434,39 @@ function migrateMemoryFilesFor4B(db) {
|
|
|
2277
2434
|
db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
2278
2435
|
}
|
|
2279
2436
|
}
|
|
2437
|
+
function migrateSharedMemoryFor5B(db) {
|
|
2438
|
+
const cols = db.prepare(`PRAGMA table_info(observations)`).all();
|
|
2439
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
|
|
2440
|
+
db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
|
|
2441
|
+
}
|
|
2442
|
+
db.exec(`
|
|
2443
|
+
CREATE TABLE IF NOT EXISTS shared_memory_pending (
|
|
2444
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2445
|
+
record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
|
|
2446
|
+
origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
|
|
2447
|
+
origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
|
|
2448
|
+
envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
|
|
2449
|
+
record_json TEXT NOT NULL, -- the single record's canonical JSON
|
|
2450
|
+
received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
|
|
2451
|
+
accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
|
|
2452
|
+
refused_at_epoch INTEGER, -- set on refuse (B-06)
|
|
2453
|
+
expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
|
|
2454
|
+
);
|
|
2455
|
+
CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
|
|
2456
|
+
CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
|
|
2457
|
+
`);
|
|
2458
|
+
db.exec(`
|
|
2459
|
+
CREATE TABLE IF NOT EXISTS shared_memory_outbound (
|
|
2460
|
+
record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
|
|
2461
|
+
observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
|
|
2462
|
+
origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
|
|
2463
|
+
seq INTEGER NOT NULL, -- the envelope seq this record first went out in
|
|
2464
|
+
exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
|
|
2465
|
+
revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
|
|
2466
|
+
);
|
|
2467
|
+
CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
|
|
2468
|
+
`);
|
|
2469
|
+
}
|
|
2280
2470
|
function migrateAuditLogCheckExtension(db) {
|
|
2281
2471
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
2282
2472
|
if (!row) return;
|
|
@@ -2299,7 +2489,16 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
2299
2489
|
"memory_file_adopted_human",
|
|
2300
2490
|
"memory_file_rendered",
|
|
2301
2491
|
"memory_file_render_refused",
|
|
2302
|
-
"memory_file_tombstoned"
|
|
2492
|
+
"memory_file_tombstoned",
|
|
2493
|
+
// B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
|
|
2494
|
+
// CHECK lacks any of these triggers the table rebuild below.
|
|
2495
|
+
"shared_memory_exported",
|
|
2496
|
+
"shared_memory_export_refused",
|
|
2497
|
+
"shared_memory_imported",
|
|
2498
|
+
"shared_memory_dropped",
|
|
2499
|
+
"shared_memory_accepted",
|
|
2500
|
+
"shared_memory_refused",
|
|
2501
|
+
"shared_memory_revoked"
|
|
2303
2502
|
];
|
|
2304
2503
|
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);
|
|
2305
2504
|
if (checkClauseMatch) {
|
|
@@ -2318,7 +2517,12 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
2318
2517
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2319
2518
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2320
2519
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2321
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2520
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2521
|
+
-- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
|
|
2522
|
+
-- the expected[] list above, or the migration would loop rebuilding forever).
|
|
2523
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2524
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2525
|
+
'shared_memory_revoked'
|
|
2322
2526
|
)),
|
|
2323
2527
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2324
2528
|
model_id TEXT,
|
|
@@ -2395,6 +2599,12 @@ function initMemorySchema(db) {
|
|
|
2395
2599
|
-- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
|
|
2396
2600
|
-- origin='team' and must never reach disk without CR-55's gates.
|
|
2397
2601
|
origin TEXT NOT NULL DEFAULT 'local',
|
|
2602
|
+
-- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
|
|
2603
|
+
-- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
|
|
2604
|
+
-- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
|
|
2605
|
+
-- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
|
|
2606
|
+
-- un-shareable until a human says otherwise (fail-closed).
|
|
2607
|
+
shareable INTEGER NOT NULL DEFAULT 0,
|
|
2398
2608
|
created_at TEXT NOT NULL,
|
|
2399
2609
|
created_at_epoch INTEGER NOT NULL,
|
|
2400
2610
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -2668,7 +2878,13 @@ function initMemorySchema(db) {
|
|
|
2668
2878
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2669
2879
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2670
2880
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2671
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2881
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2882
|
+
-- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
|
|
2883
|
+
-- nobody could ever answer "what has crossed between my repos?" (and an ingest
|
|
2884
|
+
-- catch would swallow the CHECK violation, silently producing NOTHING).
|
|
2885
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2886
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2887
|
+
'shared_memory_revoked'
|
|
2672
2888
|
)),
|
|
2673
2889
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2674
2890
|
model_id TEXT,
|
|
@@ -3037,6 +3253,7 @@ function initMemorySchema(db) {
|
|
|
3037
3253
|
`);
|
|
3038
3254
|
migrateObservationEmbeddingChunks(db);
|
|
3039
3255
|
migrateMemoryFilesFor4B(db);
|
|
3256
|
+
migrateSharedMemoryFor5B(db);
|
|
3040
3257
|
ensureRuleCandidatesTable(db);
|
|
3041
3258
|
migrateRuleDelivery(db);
|
|
3042
3259
|
}
|
|
@@ -3907,7 +4124,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3907
4124
|
var init_memory_db = __esm({
|
|
3908
4125
|
"src/memory-db.ts"() {
|
|
3909
4126
|
"use strict";
|
|
3910
|
-
|
|
4127
|
+
init_db_driver();
|
|
3911
4128
|
init_config();
|
|
3912
4129
|
init_memory_vector();
|
|
3913
4130
|
init_db_backup();
|
|
@@ -3932,7 +4149,7 @@ init_memory_db();
|
|
|
3932
4149
|
init_rule_delivery();
|
|
3933
4150
|
|
|
3934
4151
|
// src/session-archiver.ts
|
|
3935
|
-
import { existsSync as
|
|
4152
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7, renameSync } from "fs";
|
|
3936
4153
|
import { resolve as resolve5, dirname as dirname8 } from "path";
|
|
3937
4154
|
|
|
3938
4155
|
// src/session-state-generator.ts
|
|
@@ -4085,12 +4302,12 @@ function archiveAndRegenerate(db, sessionId) {
|
|
|
4085
4302
|
const archiveDir = resolved.sessionArchivePath;
|
|
4086
4303
|
let archived = false;
|
|
4087
4304
|
let archivePath;
|
|
4088
|
-
if (
|
|
4089
|
-
const existingContent =
|
|
4305
|
+
if (existsSync9(currentMdPath)) {
|
|
4306
|
+
const existingContent = readFileSync6(currentMdPath, "utf-8");
|
|
4090
4307
|
if (existingContent.trim().length > 10) {
|
|
4091
4308
|
const { date, slug } = extractArchiveInfo(existingContent);
|
|
4092
4309
|
archivePath = resolve5(archiveDir, `${date}-${slug}.md`);
|
|
4093
|
-
if (!
|
|
4310
|
+
if (!existsSync9(archiveDir)) {
|
|
4094
4311
|
mkdirSync7(archiveDir, { recursive: true });
|
|
4095
4312
|
}
|
|
4096
4313
|
try {
|
|
@@ -4104,7 +4321,7 @@ function archiveAndRegenerate(db, sessionId) {
|
|
|
4104
4321
|
}
|
|
4105
4322
|
const newContent = generateCurrentMd(db, sessionId);
|
|
4106
4323
|
const dir = dirname8(currentMdPath);
|
|
4107
|
-
if (!
|
|
4324
|
+
if (!existsSync9(dir)) {
|
|
4108
4325
|
mkdirSync7(dir, { recursive: true });
|
|
4109
4326
|
}
|
|
4110
4327
|
writeFileSync3(currentMdPath, newContent, "utf-8");
|
|
@@ -4214,11 +4431,6 @@ init_rule_delivery();
|
|
|
4214
4431
|
|
|
4215
4432
|
// src/observation-extractor.ts
|
|
4216
4433
|
init_memory_db();
|
|
4217
|
-
|
|
4218
|
-
// src/adr-generator.ts
|
|
4219
|
-
init_config();
|
|
4220
|
-
|
|
4221
|
-
// src/observation-extractor.ts
|
|
4222
4434
|
init_config();
|
|
4223
4435
|
var PRIVATE_PATTERNS = [
|
|
4224
4436
|
/\/Users\/\w+/,
|
|
@@ -4425,13 +4637,80 @@ async function drainSyncQueue(db) {
|
|
|
4425
4637
|
}
|
|
4426
4638
|
}
|
|
4427
4639
|
function sleep(ms) {
|
|
4428
|
-
return new Promise((
|
|
4640
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
// src/team-rule-sync.ts
|
|
4644
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9 } from "fs";
|
|
4645
|
+
import { homedir as homedir6 } from "os";
|
|
4646
|
+
import { join as join6, dirname as dirname10 } from "path";
|
|
4647
|
+
|
|
4648
|
+
// src/security/promotion-apply-mac.ts
|
|
4649
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4650
|
+
import { homedir as homedir5 } from "os";
|
|
4651
|
+
|
|
4652
|
+
// src/memory-authorship.ts
|
|
4653
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
4654
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync10, mkdirSync as mkdirSync8, chmodSync as chmodSync3, statSync as statSync2 } from "fs";
|
|
4655
|
+
import { homedir as homedir4 } from "os";
|
|
4656
|
+
import { resolve as resolve6 } from "path";
|
|
4657
|
+
function renderKeyPath(home = homedir4()) {
|
|
4658
|
+
return resolve6(home, ".massu", "render-key");
|
|
4659
|
+
}
|
|
4660
|
+
function readRenderKey(home = homedir4()) {
|
|
4661
|
+
const p = renderKeyPath(home);
|
|
4662
|
+
try {
|
|
4663
|
+
if (!existsSync10(p)) return void 0;
|
|
4664
|
+
const raw = readFileSync7(p);
|
|
4665
|
+
if (raw.length !== 32) return void 0;
|
|
4666
|
+
return raw;
|
|
4667
|
+
} catch {
|
|
4668
|
+
return void 0;
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
function ensureRenderKey(home = homedir4()) {
|
|
4672
|
+
const existing = readRenderKey(home);
|
|
4673
|
+
if (existing) return existing;
|
|
4674
|
+
const p = renderKeyPath(home);
|
|
4675
|
+
try {
|
|
4676
|
+
const dir = resolve6(home, ".massu");
|
|
4677
|
+
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
4678
|
+
try {
|
|
4679
|
+
chmodSync3(dir, 448);
|
|
4680
|
+
} catch {
|
|
4681
|
+
}
|
|
4682
|
+
const key = randomBytes(32);
|
|
4683
|
+
writeFileSync4(p, key, { mode: 384 });
|
|
4684
|
+
try {
|
|
4685
|
+
chmodSync3(p, 384);
|
|
4686
|
+
} catch {
|
|
4687
|
+
}
|
|
4688
|
+
return key;
|
|
4689
|
+
} catch {
|
|
4690
|
+
return void 0;
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
// src/security/promotion-apply-mac.ts
|
|
4695
|
+
var DOMAIN = "massu.promotion-apply.v1";
|
|
4696
|
+
function canonical(f) {
|
|
4697
|
+
return [DOMAIN, f.origin, f.org_id, f.prompt_hash, f.destination, f.draft_text].join("\n");
|
|
4698
|
+
}
|
|
4699
|
+
function deriveApplyKey(renderKey) {
|
|
4700
|
+
return createHmac2("sha256", renderKey).update(DOMAIN, "utf8").digest();
|
|
4701
|
+
}
|
|
4702
|
+
function computePromotionApplyMac(fields, home = homedir5()) {
|
|
4703
|
+
try {
|
|
4704
|
+
const key = ensureRenderKey(home);
|
|
4705
|
+
if (!key) return null;
|
|
4706
|
+
return createHmac2("sha256", deriveApplyKey(key)).update(canonical(fields), "utf8").digest("hex");
|
|
4707
|
+
} catch {
|
|
4708
|
+
return null;
|
|
4709
|
+
}
|
|
4429
4710
|
}
|
|
4430
4711
|
|
|
4431
4712
|
// src/team-rule-sync.ts
|
|
4432
4713
|
init_config();
|
|
4433
|
-
import { existsSync as existsSync9, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync8 } from "fs";
|
|
4434
|
-
import { join as join5, dirname as dirname9 } from "path";
|
|
4435
4714
|
|
|
4436
4715
|
// src/license.ts
|
|
4437
4716
|
init_config();
|
|
@@ -4482,13 +4761,13 @@ function verifyEd25519SignedEnvelope(key, payload) {
|
|
|
4482
4761
|
if (typeof k !== "string") continue;
|
|
4483
4762
|
canonicalObj[k] = payload[k];
|
|
4484
4763
|
}
|
|
4485
|
-
const
|
|
4764
|
+
const canonical2 = JSON.stringify(canonicalObj, [...payloadKeys].sort());
|
|
4486
4765
|
try {
|
|
4487
4766
|
const der = Buffer.concat([SPKI_ED25519_PREFIX, Buffer.from(key.pubkeyBytes)]);
|
|
4488
4767
|
const pubkey = createPublicKey({ key: der, format: "der", type: "spki" });
|
|
4489
4768
|
const ok = cryptoVerify(
|
|
4490
4769
|
null,
|
|
4491
|
-
Buffer.from(
|
|
4770
|
+
Buffer.from(canonical2, "utf-8"),
|
|
4492
4771
|
pubkey,
|
|
4493
4772
|
Buffer.from(sig, "base64")
|
|
4494
4773
|
);
|
|
@@ -4646,6 +4925,10 @@ var TEAM_SHARED_PROMOTION_MIN_TIER = "team";
|
|
|
4646
4925
|
function entitledForTeamSharedPromotion(tier) {
|
|
4647
4926
|
return tierLevel(tier) >= tierLevel(TEAM_SHARED_PROMOTION_MIN_TIER);
|
|
4648
4927
|
}
|
|
4928
|
+
var CROSS_REPO_SURFACING_MIN_TIER = "free";
|
|
4929
|
+
function entitledForCrossRepoSurfacing(tier) {
|
|
4930
|
+
return tierLevel(tier) >= tierLevel(CROSS_REPO_SURFACING_MIN_TIER);
|
|
4931
|
+
}
|
|
4649
4932
|
|
|
4650
4933
|
// src/security/promotion-pubkey.generated.ts
|
|
4651
4934
|
var PROMOTION_PUBKEY_ED25519 = new Uint8Array([107, 161, 33, 17, 189, 44, 193, 128, 252, 155, 188, 236, 100, 163, 23, 146, 219, 155, 216, 139, 134, 72, 211, 182, 151, 122, 209, 151, 135, 65, 167, 26]);
|
|
@@ -4667,6 +4950,136 @@ function verifyPromotionEnvelope(payload) {
|
|
|
4667
4950
|
);
|
|
4668
4951
|
}
|
|
4669
4952
|
|
|
4953
|
+
// src/lib/safe-write.ts
|
|
4954
|
+
import {
|
|
4955
|
+
writeFileSync as writeFileSync5,
|
|
4956
|
+
renameSync as renameSync2,
|
|
4957
|
+
openSync as openSync2,
|
|
4958
|
+
fsyncSync as fsyncSync2,
|
|
4959
|
+
closeSync as closeSync2,
|
|
4960
|
+
existsSync as existsSync11,
|
|
4961
|
+
realpathSync,
|
|
4962
|
+
unlinkSync as unlinkSync2,
|
|
4963
|
+
accessSync as accessSync2,
|
|
4964
|
+
statSync as statSync3,
|
|
4965
|
+
chmodSync as chmodSync4,
|
|
4966
|
+
constants
|
|
4967
|
+
} from "fs";
|
|
4968
|
+
import { dirname as dirname9, resolve as resolve7, relative, isAbsolute, basename as basename3, sep } from "path";
|
|
4969
|
+
var PathEscapeError = class extends Error {
|
|
4970
|
+
constructor(message) {
|
|
4971
|
+
super(message);
|
|
4972
|
+
this.name = "PathEscapeError";
|
|
4973
|
+
}
|
|
4974
|
+
};
|
|
4975
|
+
var RESERVED_DEVICE_NAMES = /* @__PURE__ */ new Set([
|
|
4976
|
+
"con",
|
|
4977
|
+
"prn",
|
|
4978
|
+
"aux",
|
|
4979
|
+
"nul",
|
|
4980
|
+
"com1",
|
|
4981
|
+
"com2",
|
|
4982
|
+
"com3",
|
|
4983
|
+
"com4",
|
|
4984
|
+
"com5",
|
|
4985
|
+
"com6",
|
|
4986
|
+
"com7",
|
|
4987
|
+
"com8",
|
|
4988
|
+
"com9",
|
|
4989
|
+
"lpt1",
|
|
4990
|
+
"lpt2",
|
|
4991
|
+
"lpt3",
|
|
4992
|
+
"lpt4",
|
|
4993
|
+
"lpt5",
|
|
4994
|
+
"lpt6",
|
|
4995
|
+
"lpt7",
|
|
4996
|
+
"lpt8",
|
|
4997
|
+
"lpt9"
|
|
4998
|
+
]);
|
|
4999
|
+
function assertContainedIn(rootDir, candidate, opts = {}) {
|
|
5000
|
+
if (candidate.includes("\0")) {
|
|
5001
|
+
throw new PathEscapeError("path contains a NUL byte");
|
|
5002
|
+
}
|
|
5003
|
+
const root = resolve7(rootDir);
|
|
5004
|
+
const abs = resolve7(root, candidate);
|
|
5005
|
+
if (!opts.allowNested) {
|
|
5006
|
+
const rel = relative(root, abs);
|
|
5007
|
+
if (rel.includes(sep)) {
|
|
5008
|
+
throw new PathEscapeError(`path must be a plain basename inside ${root}: ${candidate}`);
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
const base = basename3(abs);
|
|
5012
|
+
const stem = base.replace(/\.[^.]*$/, "").toLowerCase();
|
|
5013
|
+
if (RESERVED_DEVICE_NAMES.has(stem)) {
|
|
5014
|
+
throw new PathEscapeError(`reserved device name: ${base}`);
|
|
5015
|
+
}
|
|
5016
|
+
if (!isContainedIn(root, abs)) {
|
|
5017
|
+
throw new PathEscapeError(`path escapes ${root}: ${candidate}`);
|
|
5018
|
+
}
|
|
5019
|
+
return abs;
|
|
5020
|
+
}
|
|
5021
|
+
function isContainedIn(rootDir, candidate, opts = {}) {
|
|
5022
|
+
if (candidate.includes("\0")) return false;
|
|
5023
|
+
const root = resolve7(rootDir);
|
|
5024
|
+
const abs = resolve7(root, candidate);
|
|
5025
|
+
const rel = relative(root, abs);
|
|
5026
|
+
if (rel === "" && !opts.allowRoot) return false;
|
|
5027
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return false;
|
|
5028
|
+
let probe = abs;
|
|
5029
|
+
while (!existsSync11(probe) && probe.length > 1) {
|
|
5030
|
+
const parent = dirname9(probe);
|
|
5031
|
+
if (parent === probe) break;
|
|
5032
|
+
probe = parent;
|
|
5033
|
+
}
|
|
5034
|
+
let realRoot;
|
|
5035
|
+
let realProbe;
|
|
5036
|
+
try {
|
|
5037
|
+
realRoot = realpathSync(root);
|
|
5038
|
+
realProbe = realpathSync(probe);
|
|
5039
|
+
} catch {
|
|
5040
|
+
return true;
|
|
5041
|
+
}
|
|
5042
|
+
const realRel = relative(realRoot, realProbe);
|
|
5043
|
+
if (realRel === "") return true;
|
|
5044
|
+
return !realRel.startsWith("..") && !isAbsolute(realRel);
|
|
5045
|
+
}
|
|
5046
|
+
function atomicWriteFileSync(destPath, contents) {
|
|
5047
|
+
const dir = dirname9(destPath);
|
|
5048
|
+
const tmp = resolve7(dir, `.${basename3(destPath)}.massu-tmp-${process.pid}`);
|
|
5049
|
+
let destMode;
|
|
5050
|
+
if (existsSync11(destPath)) {
|
|
5051
|
+
accessSync2(destPath, constants.W_OK);
|
|
5052
|
+
destMode = statSync3(destPath).mode & 511;
|
|
5053
|
+
}
|
|
5054
|
+
let fd;
|
|
5055
|
+
try {
|
|
5056
|
+
writeFileSync5(tmp, contents, "utf-8");
|
|
5057
|
+
if (destMode !== void 0) chmodSync4(tmp, destMode);
|
|
5058
|
+
fd = openSync2(tmp, "r+");
|
|
5059
|
+
fsyncSync2(fd);
|
|
5060
|
+
closeSync2(fd);
|
|
5061
|
+
fd = void 0;
|
|
5062
|
+
renameSync2(tmp, destPath);
|
|
5063
|
+
} catch (err) {
|
|
5064
|
+
if (fd !== void 0) {
|
|
5065
|
+
try {
|
|
5066
|
+
closeSync2(fd);
|
|
5067
|
+
} catch {
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
try {
|
|
5071
|
+
if (existsSync11(tmp)) unlinkSync2(tmp);
|
|
5072
|
+
} catch {
|
|
5073
|
+
}
|
|
5074
|
+
throw err;
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
var SLUG_ALLOWED = /^[a-z0-9_]+$/;
|
|
5078
|
+
function deriveSlug(input) {
|
|
5079
|
+
const cleaned = input.toLowerCase().replace(/[^a-z0-9\s]+/g, " ").replace(/\s+/g, "_").replace(/^_+|_+$/g, "");
|
|
5080
|
+
return cleaned.slice(0, 60) || "rule_candidate";
|
|
5081
|
+
}
|
|
5082
|
+
|
|
4670
5083
|
// src/memory-index-region.ts
|
|
4671
5084
|
init_fileLock();
|
|
4672
5085
|
|
|
@@ -4680,6 +5093,12 @@ init_config();
|
|
|
4680
5093
|
// src/rule-candidate-applier.ts
|
|
4681
5094
|
init_memory_db();
|
|
4682
5095
|
|
|
5096
|
+
// src/memory-origin.ts
|
|
5097
|
+
var LOCAL_ORIGIN = "local";
|
|
5098
|
+
function isLocalOrigin(o) {
|
|
5099
|
+
return o === LOCAL_ORIGIN;
|
|
5100
|
+
}
|
|
5101
|
+
|
|
4683
5102
|
// src/rule-candidate-funnel.ts
|
|
4684
5103
|
init_memory_db();
|
|
4685
5104
|
|
|
@@ -4805,11 +5224,11 @@ async function pullTeamPromotions(db, opts = {}) {
|
|
|
4805
5224
|
result.revoked_handled += 1;
|
|
4806
5225
|
continue;
|
|
4807
5226
|
}
|
|
4808
|
-
if (
|
|
5227
|
+
if (existsSync12(candidatePath) || alreadyApplied(db, p.prompt_hash)) {
|
|
4809
5228
|
result.skipped += 1;
|
|
4810
5229
|
continue;
|
|
4811
5230
|
}
|
|
4812
|
-
materializeCandidate(db, projectRoot, candidatePath, p, signedOrgId);
|
|
5231
|
+
materializeCandidate(db, projectRoot, candidatePath, p, signedOrgId, opts.home ?? homedir6());
|
|
4813
5232
|
result.materialized += 1;
|
|
4814
5233
|
}
|
|
4815
5234
|
const serverCursor = typeof envelope.cursor === "number" ? envelope.cursor : 0;
|
|
@@ -4841,7 +5260,7 @@ function isValidWirePromotion(p) {
|
|
|
4841
5260
|
return typeof r.prompt_hash === "string" && PROMPT_HASH_RE.test(r.prompt_hash) && typeof r.destination === "string" && typeof r.draft_text === "string" && typeof r.promoted_by === "string" && typeof r.promoted_at === "string";
|
|
4842
5261
|
}
|
|
4843
5262
|
function sidecarPath(projectRoot, promptHash) {
|
|
4844
|
-
return
|
|
5263
|
+
return join6(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
4845
5264
|
}
|
|
4846
5265
|
function alreadyApplied(db, promptHash) {
|
|
4847
5266
|
try {
|
|
@@ -4855,9 +5274,9 @@ function alreadyApplied(db, promptHash) {
|
|
|
4855
5274
|
}
|
|
4856
5275
|
}
|
|
4857
5276
|
function handleRevocation(db, projectRoot, candidatePath, promptHash) {
|
|
4858
|
-
if (
|
|
5277
|
+
if (existsSync12(candidatePath)) {
|
|
4859
5278
|
try {
|
|
4860
|
-
|
|
5279
|
+
unlinkSync3(candidatePath);
|
|
4861
5280
|
} catch {
|
|
4862
5281
|
}
|
|
4863
5282
|
return;
|
|
@@ -4869,8 +5288,12 @@ function handleRevocation(db, projectRoot, candidatePath, promptHash) {
|
|
|
4869
5288
|
);
|
|
4870
5289
|
}
|
|
4871
5290
|
}
|
|
4872
|
-
function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
|
|
5291
|
+
function materializeCandidate(db, projectRoot, candidatePath, p, orgId, home) {
|
|
4873
5292
|
const promptText = p.draft_text.replace(/\n+/g, " ").slice(0, 200) || `team rule ${p.prompt_hash}`;
|
|
5293
|
+
const applyMac = computePromotionApplyMac(
|
|
5294
|
+
{ origin: "team", org_id: orgId, prompt_hash: p.prompt_hash, destination: p.destination, draft_text: p.draft_text },
|
|
5295
|
+
home
|
|
5296
|
+
);
|
|
4874
5297
|
const sidecar = {
|
|
4875
5298
|
// Standard RuleCandidatePayload fields (so `/massu-rule approve` → readCandidate
|
|
4876
5299
|
// → validateCandidatePayload passes), synthesized from the promotion.
|
|
@@ -4893,6 +5316,7 @@ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
|
|
|
4893
5316
|
promoted_by: p.promoted_by,
|
|
4894
5317
|
promoted_at: p.promoted_at,
|
|
4895
5318
|
signature_verified: true,
|
|
5319
|
+
...applyMac ? { apply_mac: applyMac } : {},
|
|
4896
5320
|
...p.hardened === true ? { hardened: true } : {}
|
|
4897
5321
|
},
|
|
4898
5322
|
// Extra fields the `/massu-rule approve` flow reads to drive the apply (the
|
|
@@ -4903,9 +5327,9 @@ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
|
|
|
4903
5327
|
draft_text: p.draft_text,
|
|
4904
5328
|
...p.review_attestation !== void 0 ? { publisher_review_attestation: p.review_attestation } : {}
|
|
4905
5329
|
};
|
|
4906
|
-
const dir =
|
|
4907
|
-
if (!
|
|
4908
|
-
|
|
5330
|
+
const dir = dirname10(candidatePath);
|
|
5331
|
+
if (!existsSync12(dir)) mkdirSync9(dir, { recursive: true });
|
|
5332
|
+
writeFileSync6(candidatePath, JSON.stringify(sidecar, null, 2), "utf-8");
|
|
4909
5333
|
try {
|
|
4910
5334
|
shareObservation(db, p.promoted_by, getProjectName(), "rule_promotion", promptText, {
|
|
4911
5335
|
filePath: void 0,
|
|
@@ -4956,124 +5380,948 @@ function failSync(db, reason) {
|
|
|
4956
5380
|
return { ...ZERO, sync_error: reason };
|
|
4957
5381
|
}
|
|
4958
5382
|
|
|
4959
|
-
// src/
|
|
5383
|
+
// src/shared-memory-sync.ts
|
|
4960
5384
|
init_config();
|
|
5385
|
+
import { homedir as homedir10 } from "os";
|
|
4961
5386
|
|
|
4962
|
-
// src/
|
|
5387
|
+
// src/memory-repo-identity.ts
|
|
4963
5388
|
init_config();
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
}
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
return
|
|
5389
|
+
init_memory_db();
|
|
5390
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5391
|
+
var REPO_ID_META_KEY = "repo_id";
|
|
5392
|
+
var SHARED_PIN_META_PREFIX = "shared_pin:";
|
|
5393
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5394
|
+
function getRepoId(db) {
|
|
5395
|
+
const v = getMemoryMeta(db, REPO_ID_META_KEY);
|
|
5396
|
+
return v && UUID_RE.test(v) ? v : null;
|
|
5397
|
+
}
|
|
5398
|
+
function mintRepoId(db) {
|
|
5399
|
+
const existing = getRepoId(db);
|
|
5400
|
+
if (existing) return existing;
|
|
5401
|
+
const id = randomUUID2();
|
|
5402
|
+
setMemoryMeta(db, REPO_ID_META_KEY, id);
|
|
5403
|
+
return id;
|
|
5404
|
+
}
|
|
5405
|
+
function deriveRepoLabel(projectName) {
|
|
5406
|
+
if (!projectName || !projectName.trim()) return "repo";
|
|
5407
|
+
const slug = deriveSlug(projectName);
|
|
5408
|
+
return slug && SLUG_ALLOWED.test(slug) ? slug : "repo";
|
|
5409
|
+
}
|
|
5410
|
+
function getRepoLabel() {
|
|
5411
|
+
return deriveRepoLabel(getConfig().project.name);
|
|
5412
|
+
}
|
|
5413
|
+
function getSharedPin(db, originRepoId) {
|
|
5414
|
+
return getMemoryMeta(db, SHARED_PIN_META_PREFIX + originRepoId);
|
|
5415
|
+
}
|
|
5416
|
+
function tofuPinSharedFingerprint(db, originRepoId, fingerprint) {
|
|
5417
|
+
const existing = getSharedPin(db, originRepoId);
|
|
5418
|
+
if (existing) return existing;
|
|
5419
|
+
setMemoryMeta(db, SHARED_PIN_META_PREFIX + originRepoId, fingerprint);
|
|
5420
|
+
return fingerprint;
|
|
4979
5421
|
}
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
5422
|
+
|
|
5423
|
+
// src/memory-repos-registry.ts
|
|
5424
|
+
import {
|
|
5425
|
+
chmodSync as chmodSync5,
|
|
5426
|
+
existsSync as existsSync13,
|
|
5427
|
+
mkdirSync as mkdirSync10,
|
|
5428
|
+
readFileSync as readFileSync9,
|
|
5429
|
+
writeFileSync as writeFileSync7
|
|
5430
|
+
} from "fs";
|
|
5431
|
+
import { homedir as homedir7 } from "os";
|
|
5432
|
+
import { dirname as dirname11, join as join7 } from "path";
|
|
5433
|
+
var REPOS_REGISTRY_VERSION = 1;
|
|
5434
|
+
function reposRegistryPath(home = homedir7()) {
|
|
5435
|
+
return join7(home, ".massu", "repos.json");
|
|
5436
|
+
}
|
|
5437
|
+
function emptyRegistry() {
|
|
5438
|
+
return { version: REPOS_REGISTRY_VERSION, repos: [] };
|
|
5439
|
+
}
|
|
5440
|
+
function readReposRegistry(home = homedir7()) {
|
|
5441
|
+
const p = reposRegistryPath(home);
|
|
5442
|
+
if (!existsSync13(p)) return emptyRegistry();
|
|
5443
|
+
try {
|
|
5444
|
+
const parsed = JSON.parse(readFileSync9(p, "utf-8"));
|
|
5445
|
+
if (!parsed || typeof parsed !== "object") return emptyRegistry();
|
|
5446
|
+
const r = parsed;
|
|
5447
|
+
if (!Array.isArray(r.repos)) return emptyRegistry();
|
|
5448
|
+
const repos = r.repos.filter(
|
|
5449
|
+
(e) => !!e && typeof e === "object" && typeof e.repo_id === "string" && typeof e.label === "string"
|
|
5450
|
+
);
|
|
5451
|
+
return { version: typeof r.version === "number" ? r.version : REPOS_REGISTRY_VERSION, repos };
|
|
5452
|
+
} catch {
|
|
5453
|
+
return emptyRegistry();
|
|
4999
5454
|
}
|
|
5455
|
+
}
|
|
5456
|
+
function findRepoByLabel(home, label) {
|
|
5457
|
+
return readReposRegistry(home).repos.find((e) => e.label === label) ?? null;
|
|
5458
|
+
}
|
|
5459
|
+
function upsertRepoRegistration(entry, home = homedir7()) {
|
|
5460
|
+
const registry = readReposRegistry(home);
|
|
5461
|
+
const next = registry.repos.filter((e) => e.repo_id !== entry.repo_id);
|
|
5462
|
+
next.push(entry);
|
|
5463
|
+
const out = { version: REPOS_REGISTRY_VERSION, repos: next };
|
|
5464
|
+
const p = reposRegistryPath(home);
|
|
5465
|
+
mkdirSync10(dirname11(p), { recursive: true, mode: 448 });
|
|
5466
|
+
writeFileSync7(p, JSON.stringify(out, null, 2), { mode: 384 });
|
|
5467
|
+
chmodSync5(p, 384);
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
// src/security/local-share-verifier.ts
|
|
5471
|
+
import { homedir as homedir9 } from "os";
|
|
5472
|
+
|
|
5473
|
+
// src/security/local-share-signer.ts
|
|
5474
|
+
import {
|
|
5475
|
+
chmodSync as chmodSync6,
|
|
5476
|
+
existsSync as existsSync14,
|
|
5477
|
+
mkdirSync as mkdirSync11,
|
|
5478
|
+
readFileSync as readFileSync10,
|
|
5479
|
+
writeFileSync as writeFileSync8
|
|
5480
|
+
} from "fs";
|
|
5481
|
+
import { homedir as homedir8 } from "os";
|
|
5482
|
+
import { join as join8 } from "path";
|
|
5483
|
+
import {
|
|
5484
|
+
createHash as createHash3,
|
|
5485
|
+
createPrivateKey,
|
|
5486
|
+
createPublicKey as createPublicKey2,
|
|
5487
|
+
generateKeyPairSync,
|
|
5488
|
+
sign as cryptoSign
|
|
5489
|
+
} from "crypto";
|
|
5490
|
+
|
|
5491
|
+
// src/shared-memory-envelope.ts
|
|
5492
|
+
import { createHash as createHash2 } from "crypto";
|
|
5493
|
+
var SHARED_MEMORY_KIND = "massu.shared-memory.v1";
|
|
5494
|
+
var SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS = [
|
|
5495
|
+
"issued_at",
|
|
5496
|
+
"kind",
|
|
5497
|
+
"origin_repo_id",
|
|
5498
|
+
"origin_repo_label",
|
|
5499
|
+
"records_json",
|
|
5500
|
+
"revokes_json",
|
|
5501
|
+
"seq"
|
|
5502
|
+
];
|
|
5503
|
+
function canonicalizeSharedMemoryEnvelope(body) {
|
|
5504
|
+
const keys = SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS;
|
|
5505
|
+
const canonicalObj = {};
|
|
5506
|
+
for (const k of keys) {
|
|
5507
|
+
canonicalObj[k] = body[k];
|
|
5508
|
+
}
|
|
5509
|
+
return JSON.stringify(canonicalObj, [...keys].sort());
|
|
5510
|
+
}
|
|
5511
|
+
function hashSharedMemoryRecord(record) {
|
|
5512
|
+
const canonical2 = JSON.stringify({
|
|
5513
|
+
created_at_epoch: record.created_at_epoch,
|
|
5514
|
+
detail: record.detail,
|
|
5515
|
+
importance: record.importance,
|
|
5516
|
+
superseded_by_hash: record.superseded_by_hash,
|
|
5517
|
+
title: record.title,
|
|
5518
|
+
type: record.type
|
|
5519
|
+
});
|
|
5520
|
+
return createHash2("sha256").update(canonical2, "utf-8").digest("hex");
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
// src/security/local-share-signer.ts
|
|
5524
|
+
function localShareKeyDir(home = homedir8()) {
|
|
5525
|
+
return join8(home, ".massu", "keys");
|
|
5526
|
+
}
|
|
5527
|
+
function localSharePrivKeyPath(home = homedir8()) {
|
|
5528
|
+
return join8(localShareKeyDir(home), "local-share.key");
|
|
5529
|
+
}
|
|
5530
|
+
function localSharePubKeyPath(home = homedir8()) {
|
|
5531
|
+
return join8(localShareKeyDir(home), "local-share.pub");
|
|
5532
|
+
}
|
|
5533
|
+
var SPKI_ED25519_PREFIX_LEN = 12;
|
|
5534
|
+
function ensureLocalShareKeypair(home = homedir8()) {
|
|
5535
|
+
const privPath = localSharePrivKeyPath(home);
|
|
5536
|
+
if (existsSync14(privPath)) return;
|
|
5537
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
5538
|
+
const privPem = privateKey.export({ format: "pem", type: "pkcs8" });
|
|
5539
|
+
const pubPem = publicKey.export({ format: "pem", type: "spki" });
|
|
5540
|
+
mkdirSync11(localShareKeyDir(home), { recursive: true, mode: 448 });
|
|
5541
|
+
writeFileSync8(privPath, privPem, { mode: 384 });
|
|
5542
|
+
chmodSync6(privPath, 384);
|
|
5543
|
+
writeFileSync8(localSharePubKeyPath(home), pubPem, { mode: 420 });
|
|
5544
|
+
}
|
|
5545
|
+
function readLocalSharePublicKeyRaw(home = homedir8()) {
|
|
5546
|
+
const pubPem = readFileSync10(localSharePubKeyPath(home), "utf-8");
|
|
5547
|
+
const der = createPublicKey2(pubPem).export({ format: "der", type: "spki" });
|
|
5548
|
+
return Uint8Array.from(der.subarray(SPKI_ED25519_PREFIX_LEN));
|
|
5549
|
+
}
|
|
5550
|
+
function localSharePubkeyFingerprint(home = homedir8()) {
|
|
5551
|
+
return createHash3("sha256").update(Buffer.from(readLocalSharePublicKeyRaw(home))).digest("hex");
|
|
5552
|
+
}
|
|
5553
|
+
function signSharedMemoryEnvelope(body, home = homedir8()) {
|
|
5554
|
+
ensureLocalShareKeypair(home);
|
|
5555
|
+
const privPem = readFileSync10(localSharePrivKeyPath(home), "utf-8");
|
|
5556
|
+
const privKey = createPrivateKey(privPem);
|
|
5557
|
+
const canonical2 = canonicalizeSharedMemoryEnvelope(body);
|
|
5558
|
+
const signature = cryptoSign(null, Buffer.from(canonical2, "utf-8"), privKey).toString("base64");
|
|
5000
5559
|
return {
|
|
5001
|
-
|
|
5002
|
-
|
|
5560
|
+
...body,
|
|
5561
|
+
_signature: signature,
|
|
5562
|
+
_signature_alg: "ed25519",
|
|
5563
|
+
_signature_payload_keys: SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS,
|
|
5564
|
+
_signature_pubkey_fingerprint: localSharePubkeyFingerprint(home)
|
|
5003
5565
|
};
|
|
5004
5566
|
}
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
`).all();
|
|
5028
|
-
let backfilled = 0;
|
|
5029
|
-
for (const session of sessions) {
|
|
5030
|
-
const { score, breakdown } = calculateQualityScore(db, session.session_id);
|
|
5031
|
-
storeQualityScore(db, session.session_id, score, breakdown);
|
|
5032
|
-
backfilled++;
|
|
5567
|
+
|
|
5568
|
+
// src/security/local-share-verifier.ts
|
|
5569
|
+
function verifyLocalShareEnvelope(envelope, pinnedFingerprint, home = homedir9()) {
|
|
5570
|
+
let key;
|
|
5571
|
+
try {
|
|
5572
|
+
const pubkeyBytes = readLocalSharePublicKeyRaw(home);
|
|
5573
|
+
key = {
|
|
5574
|
+
pubkeyBytes,
|
|
5575
|
+
// The fingerprint of the key we are ABOUT to verify with (the on-disk key).
|
|
5576
|
+
fingerprintHex: localSharePubkeyFingerprint(home),
|
|
5577
|
+
// The trusted set comes from the PIN — a different artifact than the key.
|
|
5578
|
+
// If the on-disk key was swapped, fingerprintHex ∉ this set → the core's
|
|
5579
|
+
// self-check fails → hard drop. This Set is deliberately NOT built from
|
|
5580
|
+
// `fingerprintHex`/`pubkeyBytes` (that would be vacuous — defect D4).
|
|
5581
|
+
knownFingerprints: /* @__PURE__ */ new Set([pinnedFingerprint]),
|
|
5582
|
+
keyLabel: "local-share"
|
|
5583
|
+
};
|
|
5584
|
+
} catch (err) {
|
|
5585
|
+
return {
|
|
5586
|
+
kind: "error",
|
|
5587
|
+
reason: `local-share pubkey unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
5588
|
+
};
|
|
5033
5589
|
}
|
|
5034
|
-
return
|
|
5590
|
+
return verifyEd25519SignedEnvelope(key, envelope);
|
|
5035
5591
|
}
|
|
5036
5592
|
|
|
5037
|
-
// src/
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
}
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
}
|
|
5049
|
-
function getCurrency() {
|
|
5050
|
-
return getConfig().analytics?.cost?.currency ?? "USD";
|
|
5593
|
+
// src/shared-memory-sync.ts
|
|
5594
|
+
init_memory_db();
|
|
5595
|
+
|
|
5596
|
+
// src/shared-memory-audit.ts
|
|
5597
|
+
function latestSessionId(db) {
|
|
5598
|
+
try {
|
|
5599
|
+
const row = db.prepare(`SELECT session_id FROM sessions ORDER BY started_at_epoch DESC LIMIT 1`).get();
|
|
5600
|
+
return row?.session_id ?? null;
|
|
5601
|
+
} catch {
|
|
5602
|
+
return null;
|
|
5603
|
+
}
|
|
5051
5604
|
}
|
|
5052
|
-
function
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
const usage = msg.usage;
|
|
5062
|
-
inputTokens += usage.input_tokens ?? 0;
|
|
5063
|
-
outputTokens += usage.output_tokens ?? 0;
|
|
5064
|
-
cacheReadTokens += usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? 0;
|
|
5065
|
-
cacheWriteTokens += usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
|
|
5066
|
-
}
|
|
5067
|
-
if (entry.type === "assistant" && msg?.model) {
|
|
5068
|
-
model = msg.model;
|
|
5069
|
-
}
|
|
5605
|
+
function writeSharedAudit(db, eventType, evidence, metadata, actor = "hook") {
|
|
5606
|
+
const sessionId = latestSessionId(db);
|
|
5607
|
+
if (!sessionId) return;
|
|
5608
|
+
try {
|
|
5609
|
+
db.prepare(
|
|
5610
|
+
`INSERT INTO audit_log (session_id, event_type, actor, evidence, metadata)
|
|
5611
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
5612
|
+
).run(sessionId, eventType, actor, evidence, JSON.stringify(metadata));
|
|
5613
|
+
} catch {
|
|
5070
5614
|
}
|
|
5071
|
-
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, model };
|
|
5072
5615
|
}
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5616
|
+
|
|
5617
|
+
// src/shared-memory-sync.ts
|
|
5618
|
+
var REPO_ID_RE = /^[0-9a-f-]{36}$/;
|
|
5619
|
+
var SHARED_CURSOR_PREFIX = "shared_cursor:";
|
|
5620
|
+
var ZERO_IMPORT = { imported: 0, dropped: 0, revoked: 0, skipped: 0 };
|
|
5621
|
+
async function importSharedMemories(db, transport, opts = {}) {
|
|
5622
|
+
const home = opts.home ?? homedir10();
|
|
5623
|
+
const nowEpoch = opts.nowEpoch ?? Math.floor(Date.now() / 1e3);
|
|
5624
|
+
const config = getConfig();
|
|
5625
|
+
const tier = opts.tier ?? getCachedTierReadOnly(db);
|
|
5626
|
+
if (!entitledForCrossRepoSurfacing(tier)) return { ...ZERO_IMPORT };
|
|
5627
|
+
const subscribe = opts.subscribe ?? config.memory?.share?.subscribe ?? [];
|
|
5628
|
+
if (!Array.isArray(subscribe) || subscribe.length === 0) return { ...ZERO_IMPORT };
|
|
5629
|
+
const ownRepoId = getRepoId(db);
|
|
5630
|
+
const result = { ...ZERO_IMPORT };
|
|
5631
|
+
for (const subscribedLabel of subscribe) {
|
|
5632
|
+
const entry = findRepoByLabel(home, subscribedLabel);
|
|
5633
|
+
if (!entry || !REPO_ID_RE.test(entry.repo_id)) continue;
|
|
5634
|
+
const originRepoId = entry.repo_id;
|
|
5635
|
+
if (ownRepoId && originRepoId === ownRepoId) continue;
|
|
5636
|
+
const pinned = getSharedPin(db, originRepoId) ?? tofuPinSharedFingerprint(db, originRepoId, entry.pubkey_fingerprint);
|
|
5637
|
+
const cursorKey = SHARED_CURSOR_PREFIX + originRepoId;
|
|
5638
|
+
const since = parseCursor2(getMemoryMeta(db, cursorKey));
|
|
5639
|
+
let envelopes;
|
|
5640
|
+
try {
|
|
5641
|
+
envelopes = await transport.fetchSince(originRepoId, since);
|
|
5642
|
+
} catch {
|
|
5643
|
+
continue;
|
|
5644
|
+
}
|
|
5645
|
+
let maxSeq = since;
|
|
5646
|
+
const advancePast = (env) => {
|
|
5647
|
+
if (typeof env.seq === "number" && env.seq > maxSeq) maxSeq = env.seq;
|
|
5648
|
+
};
|
|
5649
|
+
for (const env of envelopes) {
|
|
5650
|
+
const verdict = verifyLocalShareEnvelope(env, pinned, home);
|
|
5651
|
+
if (verdict.kind !== "valid") {
|
|
5652
|
+
result.dropped += 1;
|
|
5653
|
+
drop(db, "bad_signature", { kind: verdict.kind, origin: originRepoId });
|
|
5654
|
+
if (verdict.kind === "bad_signature") advancePast(env);
|
|
5655
|
+
continue;
|
|
5656
|
+
}
|
|
5657
|
+
if (!hasSignedLoadBearingKeys(env)) {
|
|
5658
|
+
result.dropped += 1;
|
|
5659
|
+
drop(db, "unsigned_field", { origin: originRepoId });
|
|
5660
|
+
advancePast(env);
|
|
5661
|
+
continue;
|
|
5662
|
+
}
|
|
5663
|
+
if (env.origin_repo_id !== originRepoId) {
|
|
5664
|
+
result.dropped += 1;
|
|
5665
|
+
drop(db, "origin_mismatch", { expected: originRepoId, got: env.origin_repo_id });
|
|
5666
|
+
advancePast(env);
|
|
5667
|
+
continue;
|
|
5668
|
+
}
|
|
5669
|
+
if (ownRepoId && env.origin_repo_id === ownRepoId) {
|
|
5670
|
+
result.dropped += 1;
|
|
5671
|
+
advancePast(env);
|
|
5672
|
+
continue;
|
|
5673
|
+
}
|
|
5674
|
+
advancePast(env);
|
|
5675
|
+
for (const revokedHash of parseStringArray(env.revokes_json)) {
|
|
5676
|
+
if (applyRevocation(db, revokedHash, nowEpoch)) result.revoked += 1;
|
|
5677
|
+
}
|
|
5678
|
+
const originLabel = deriveRepoLabel(entry.label);
|
|
5679
|
+
for (const rec of parseRecords(env.records_json)) {
|
|
5680
|
+
if (!isWellFormedRecord(rec)) {
|
|
5681
|
+
result.dropped += 1;
|
|
5682
|
+
drop(db, "malformed_record", { origin: originRepoId });
|
|
5683
|
+
continue;
|
|
5684
|
+
}
|
|
5685
|
+
const computed = hashSharedMemoryRecord(stripHash(rec));
|
|
5686
|
+
if (computed !== rec.record_hash) {
|
|
5687
|
+
result.dropped += 1;
|
|
5688
|
+
drop(db, "record_hash_mismatch", { origin: originRepoId });
|
|
5689
|
+
continue;
|
|
5690
|
+
}
|
|
5691
|
+
if (pendingExists(db, rec.record_hash)) {
|
|
5692
|
+
result.skipped += 1;
|
|
5693
|
+
continue;
|
|
5694
|
+
}
|
|
5695
|
+
insertPending(db, rec, env, originRepoId, originLabel, nowEpoch);
|
|
5696
|
+
result.imported += 1;
|
|
5697
|
+
writeSharedAudit(db, "shared_memory_imported", `pending from ${originLabel}`, {
|
|
5698
|
+
record_hash: rec.record_hash,
|
|
5699
|
+
origin_repo_id: originRepoId
|
|
5700
|
+
});
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
if (maxSeq > since) setMemoryMeta(db, cursorKey, String(maxSeq));
|
|
5704
|
+
}
|
|
5705
|
+
return result;
|
|
5706
|
+
}
|
|
5707
|
+
var SIGNED_LOAD_BEARING_KEYS = ["origin_repo_id", "records_json", "revokes_json"];
|
|
5708
|
+
function hasSignedLoadBearingKeys(env) {
|
|
5709
|
+
const signed = Array.isArray(env._signature_payload_keys) ? env._signature_payload_keys : [];
|
|
5710
|
+
return SIGNED_LOAD_BEARING_KEYS.every((k) => signed.includes(k));
|
|
5711
|
+
}
|
|
5712
|
+
function parseCursor2(raw) {
|
|
5713
|
+
if (raw === null) return 0;
|
|
5714
|
+
const n = Number(raw);
|
|
5715
|
+
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
5716
|
+
}
|
|
5717
|
+
function parseStringArray(json) {
|
|
5718
|
+
try {
|
|
5719
|
+
const v = JSON.parse(json);
|
|
5720
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
5721
|
+
} catch {
|
|
5722
|
+
return [];
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
function parseRecords(json) {
|
|
5726
|
+
try {
|
|
5727
|
+
const v = JSON.parse(json);
|
|
5728
|
+
return Array.isArray(v) ? v : [];
|
|
5729
|
+
} catch {
|
|
5730
|
+
return [];
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
function isWellFormedRecord(r) {
|
|
5734
|
+
if (!r || typeof r !== "object") return false;
|
|
5735
|
+
const o = r;
|
|
5736
|
+
return typeof o.record_hash === "string" && typeof o.type === "string" && typeof o.title === "string" && typeof o.detail === "string" && typeof o.importance === "number" && typeof o.created_at_epoch === "number" && (o.superseded_by_hash === null || typeof o.superseded_by_hash === "string");
|
|
5737
|
+
}
|
|
5738
|
+
function stripHash(r) {
|
|
5739
|
+
return {
|
|
5740
|
+
type: r.type,
|
|
5741
|
+
title: r.title,
|
|
5742
|
+
detail: r.detail,
|
|
5743
|
+
importance: r.importance,
|
|
5744
|
+
created_at_epoch: r.created_at_epoch,
|
|
5745
|
+
superseded_by_hash: r.superseded_by_hash
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5748
|
+
function pendingExists(db, recordHash) {
|
|
5749
|
+
return !!db.prepare(`SELECT 1 FROM shared_memory_pending WHERE record_hash = ? LIMIT 1`).get(recordHash);
|
|
5750
|
+
}
|
|
5751
|
+
function loadPending(db, recordHash) {
|
|
5752
|
+
return db.prepare(
|
|
5753
|
+
`SELECT id, record_hash, origin_repo_id, origin_repo_label, envelope_raw, record_json,
|
|
5754
|
+
accepted_at_epoch, refused_at_epoch, expired_at_epoch
|
|
5755
|
+
FROM shared_memory_pending WHERE record_hash = ? LIMIT 1`
|
|
5756
|
+
).get(recordHash) ?? null;
|
|
5757
|
+
}
|
|
5758
|
+
function insertPending(db, rec, env, originRepoId, originLabel, nowEpoch) {
|
|
5759
|
+
db.prepare(
|
|
5760
|
+
`INSERT OR IGNORE INTO shared_memory_pending
|
|
5761
|
+
(record_hash, origin_repo_id, origin_repo_label, envelope_raw, record_json, received_at_epoch)
|
|
5762
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
5763
|
+
).run(rec.record_hash, originRepoId, originLabel, JSON.stringify(env), JSON.stringify(rec), nowEpoch);
|
|
5764
|
+
}
|
|
5765
|
+
function applyRevocation(db, recordHash, nowEpoch) {
|
|
5766
|
+
const row = loadPending(db, recordHash);
|
|
5767
|
+
if (!row) return false;
|
|
5768
|
+
let changed = false;
|
|
5769
|
+
if (row.accepted_at_epoch !== null) {
|
|
5770
|
+
const iso = new Date(nowEpoch * 1e3).toISOString();
|
|
5771
|
+
const res = db.prepare(
|
|
5772
|
+
`UPDATE observations
|
|
5773
|
+
SET valid_to = ?, expired_at = ?, valid_to_epoch = ?, expired_at_epoch = ?
|
|
5774
|
+
WHERE origin = ?
|
|
5775
|
+
AND expired_at IS NULL
|
|
5776
|
+
AND json_extract(evidence, '$.record_hash') = ?`
|
|
5777
|
+
).run(iso, iso, nowEpoch, nowEpoch, `repo:${row.origin_repo_id}`, recordHash);
|
|
5778
|
+
changed = res.changes > 0;
|
|
5779
|
+
}
|
|
5780
|
+
const p = db.prepare(`UPDATE shared_memory_pending SET expired_at_epoch = ? WHERE record_hash = ? AND expired_at_epoch IS NULL`).run(nowEpoch, recordHash);
|
|
5781
|
+
changed = changed || p.changes > 0;
|
|
5782
|
+
if (changed) writeSharedAudit(db, "shared_memory_revoked", `revoked ${recordHash}`, { record_hash: recordHash });
|
|
5783
|
+
return changed;
|
|
5784
|
+
}
|
|
5785
|
+
function drop(db, reason, meta) {
|
|
5786
|
+
recordTelemetry(db, "shared_memory_dropped", { reason, ...meta });
|
|
5787
|
+
writeSharedAudit(db, "shared_memory_dropped", `dropped: ${reason}`, { reason, ...meta });
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
// src/shared-memory-export.ts
|
|
5791
|
+
init_config();
|
|
5792
|
+
import { homedir as homedir11 } from "os";
|
|
5793
|
+
import { join as join9, dirname as dirname12 } from "path";
|
|
5794
|
+
import { mkdirSync as mkdirSync12, chmodSync as chmodSync7, writeFileSync as writeFileSync9, renameSync as renameSync3, openSync as openSync3, fsyncSync as fsyncSync3, closeSync as closeSync3 } from "fs";
|
|
5795
|
+
|
|
5796
|
+
// src/consolidation-config.ts
|
|
5797
|
+
init_config();
|
|
5798
|
+
var DEFAULT_CONSOLIDATION_CONFIG = {
|
|
5799
|
+
enabled: true,
|
|
5800
|
+
sessionSweepEnabled: true,
|
|
5801
|
+
// llmEndpoint / llmModel deliberately UNSET: the shipped default is
|
|
5802
|
+
// zero-LLM, zero-network, works offline on any machine.
|
|
5803
|
+
summarizeAfterDays: 5,
|
|
5804
|
+
// inside the 7-day conversation_turns prune window
|
|
5805
|
+
retentionDays: 90,
|
|
5806
|
+
importanceFloor: 2,
|
|
5807
|
+
protectedTypes: ["decision", "cr_violation", "incident_near_miss"],
|
|
5808
|
+
usageWarmupDays: 30,
|
|
5809
|
+
usageDecay: 0.9,
|
|
5810
|
+
reweightIntervalDays: 1,
|
|
5811
|
+
promoteMinOccurrences: 3,
|
|
5812
|
+
budgetMs: 3e3,
|
|
5813
|
+
suggestUpgrades: true,
|
|
5814
|
+
suggestIntervalDays: 30
|
|
5815
|
+
};
|
|
5816
|
+
function resolveConsolidationConfig() {
|
|
5817
|
+
try {
|
|
5818
|
+
const c = getConfig().memory?.consolidation;
|
|
5819
|
+
if (!c) return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5820
|
+
return { ...DEFAULT_CONSOLIDATION_CONFIG, ...c };
|
|
5821
|
+
} catch {
|
|
5822
|
+
return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
// src/memory-llm.ts
|
|
5827
|
+
var LLM_API_KEY_ENV = "MASSU_MEMORY_LLM_API_KEY";
|
|
5828
|
+
var DEFAULT_BUDGET_MS = 2e4;
|
|
5829
|
+
var DEFAULT_MAX_CHARS = 900;
|
|
5830
|
+
var NOISE_PATTERNS = [
|
|
5831
|
+
/<command-(name|message|args)>/i,
|
|
5832
|
+
/<local-command-[^>]*>/i,
|
|
5833
|
+
/^\s*<[a-z-]+>\s*$/i,
|
|
5834
|
+
/^\s*(ok|okay|thanks|thank you|yes|no|yep|nope|sure|continue|proceed|go ahead)\s*[.!]?\s*$/i,
|
|
5835
|
+
/^\s*\/[a-z-]+\s*$/i,
|
|
5836
|
+
// a bare slash-command invocation
|
|
5837
|
+
/system-reminder/i
|
|
5838
|
+
];
|
|
5839
|
+
function isSummarizableSignal(text) {
|
|
5840
|
+
const t = text.trim();
|
|
5841
|
+
if (t.length < 25) return false;
|
|
5842
|
+
return !NOISE_PATTERNS.some((re) => re.test(t));
|
|
5843
|
+
}
|
|
5844
|
+
var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
|
|
5845
|
+
var LABELLED_CREDENTIAL = new RegExp(
|
|
5846
|
+
`\\b[A-Za-z0-9_-]*(?:${CREDENTIAL_LABELS.join("|")})["'\\s:=]+[A-Za-z0-9_\\-/+]{16,}`,
|
|
5847
|
+
"gi"
|
|
5848
|
+
);
|
|
5849
|
+
var SECRET_PATTERNS = [
|
|
5850
|
+
[/\bms_live_[A-Za-z0-9_-]{6,}/g, "ms_live_[REDACTED]", "MASSU_LIVE_KEY"],
|
|
5851
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]", "OPENAI_STYLE_KEY"],
|
|
5852
|
+
[/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{16,}/g, "[REDACTED_TOKEN]", "GITHUB_TOKEN"],
|
|
5853
|
+
[/\bsbp_[A-Za-z0-9]{16,}/g, "sbp_[REDACTED]", "SUPABASE_TOKEN"],
|
|
5854
|
+
[/\bAKIA[0-9A-Z]{12,}/g, "[REDACTED_AWS_KEY]", "AWS_ACCESS_KEY"],
|
|
5855
|
+
[
|
|
5856
|
+
/\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g,
|
|
5857
|
+
"[REDACTED_JWT]",
|
|
5858
|
+
"JWT"
|
|
5859
|
+
],
|
|
5860
|
+
[LABELLED_CREDENTIAL, "[REDACTED_CREDENTIAL]", "LABELLED_CREDENTIAL"]
|
|
5861
|
+
];
|
|
5862
|
+
function redactSecrets(text) {
|
|
5863
|
+
let out = text;
|
|
5864
|
+
for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
|
|
5865
|
+
return out;
|
|
5866
|
+
}
|
|
5867
|
+
function containsSecret(text) {
|
|
5868
|
+
for (const [re, , name] of SECRET_PATTERNS) {
|
|
5869
|
+
re.lastIndex = 0;
|
|
5870
|
+
const hit = re.test(text);
|
|
5871
|
+
re.lastIndex = 0;
|
|
5872
|
+
if (hit) return { matched: true, patternName: name };
|
|
5873
|
+
}
|
|
5874
|
+
return { matched: false };
|
|
5875
|
+
}
|
|
5876
|
+
function extractiveSummary(sources, maxChars = DEFAULT_MAX_CHARS) {
|
|
5877
|
+
const cleaned = sources.map((s, i) => ({ ...s, i, text: s.text.replace(/\s+/g, " ").trim() })).filter((s) => isSummarizableSignal(s.text));
|
|
5878
|
+
if (cleaned.length === 0) return "";
|
|
5879
|
+
const ranked = [...cleaned].sort((a, b) => b.weight - a.weight || a.i - b.i);
|
|
5880
|
+
const picked = [];
|
|
5881
|
+
let used = 0;
|
|
5882
|
+
for (const s of ranked) {
|
|
5883
|
+
const cost = s.text.length + 1;
|
|
5884
|
+
if (used + cost > maxChars) continue;
|
|
5885
|
+
picked.push(s);
|
|
5886
|
+
used += cost;
|
|
5887
|
+
}
|
|
5888
|
+
if (picked.length === 0) {
|
|
5889
|
+
return ranked[0].text.slice(0, maxChars);
|
|
5890
|
+
}
|
|
5891
|
+
picked.sort((a, b) => a.i - b.i);
|
|
5892
|
+
return picked.map((s) => s.text).join(" ");
|
|
5893
|
+
}
|
|
5894
|
+
async function summarizeViaEndpoint(endpoint, model, material, budgetMs, maxChars) {
|
|
5895
|
+
try {
|
|
5896
|
+
const controller = new AbortController();
|
|
5897
|
+
const timer = setTimeout(() => controller.abort(), budgetMs);
|
|
5898
|
+
const apiKey = process.env[LLM_API_KEY_ENV];
|
|
5899
|
+
const headers = { "content-type": "application/json" };
|
|
5900
|
+
if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;
|
|
5901
|
+
let resp;
|
|
5902
|
+
try {
|
|
5903
|
+
resp = await fetch(`${endpoint.replace(/\/+$/, "")}/v1/chat/completions`, {
|
|
5904
|
+
method: "POST",
|
|
5905
|
+
headers,
|
|
5906
|
+
body: JSON.stringify({
|
|
5907
|
+
model,
|
|
5908
|
+
// a NAME/ALIAS the user configured — never a physical model id
|
|
5909
|
+
messages: [
|
|
5910
|
+
{
|
|
5911
|
+
role: "system",
|
|
5912
|
+
content: "You distill a software engineering session into ONE durable lesson a developer will read months later. Be concrete and factual. State what broke, what was tried and rejected, and what actually worked. Invent nothing that is not in the input."
|
|
5913
|
+
},
|
|
5914
|
+
{ role: "user", content: material }
|
|
5915
|
+
],
|
|
5916
|
+
max_tokens: 400,
|
|
5917
|
+
temperature: 0.2
|
|
5918
|
+
}),
|
|
5919
|
+
signal: controller.signal
|
|
5920
|
+
});
|
|
5921
|
+
} finally {
|
|
5922
|
+
clearTimeout(timer);
|
|
5923
|
+
}
|
|
5924
|
+
if (!resp.ok) return null;
|
|
5925
|
+
const data = await resp.json();
|
|
5926
|
+
const text = data.choices?.[0]?.message?.content;
|
|
5927
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
5928
|
+
return text.trim().slice(0, maxChars);
|
|
5929
|
+
} catch {
|
|
5930
|
+
return null;
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
async function summarizeText(sources, opts = {}) {
|
|
5934
|
+
const cfg = opts.config ?? resolveConsolidationConfig();
|
|
5935
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
5936
|
+
const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
5937
|
+
const extractive = extractiveSummary(sources, maxChars);
|
|
5938
|
+
if (!cfg.llmEndpoint || !cfg.llmModel || !extractive) {
|
|
5939
|
+
return { text: extractive, tier: "extractive" };
|
|
5940
|
+
}
|
|
5941
|
+
const material = redactSecrets(
|
|
5942
|
+
sources.map((s) => s.text.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n")
|
|
5943
|
+
);
|
|
5944
|
+
const viaModel = await summarizeViaEndpoint(
|
|
5945
|
+
cfg.llmEndpoint,
|
|
5946
|
+
cfg.llmModel,
|
|
5947
|
+
material,
|
|
5948
|
+
budgetMs,
|
|
5949
|
+
maxChars
|
|
5950
|
+
);
|
|
5951
|
+
return viaModel ? { text: viaModel, tier: "model" } : { text: extractive, tier: "extractive" };
|
|
5952
|
+
}
|
|
5953
|
+
|
|
5954
|
+
// src/shared-memory-export.ts
|
|
5955
|
+
init_memory_db();
|
|
5956
|
+
var DEFAULT_EXPORT_CAPS = {
|
|
5957
|
+
maxRecords: 200,
|
|
5958
|
+
maxBytesPerRecord: 16384,
|
|
5959
|
+
maxTotalBytes: 512e3
|
|
5960
|
+
};
|
|
5961
|
+
var EXPORT_SEQ_KEY = "shared_export_seq";
|
|
5962
|
+
var BACKUP_DONE_KEY = "shared_backup_at";
|
|
5963
|
+
function zero(enabled) {
|
|
5964
|
+
return { enabled, published: false, exported: 0, refused: 0, refusals: [], seq: null };
|
|
5965
|
+
}
|
|
5966
|
+
function ensureSharingBackup(db, repoId, home, nowEpoch, backupDirOverride) {
|
|
5967
|
+
if (getMemoryMeta(db, BACKUP_DONE_KEY)) return;
|
|
5968
|
+
const backupDir = backupDirOverride ?? join9(home, ".massu", "shared", "backups", repoId);
|
|
5969
|
+
const dest = join9(backupDir, `memory-${nowEpoch}.db`);
|
|
5970
|
+
const snapshot = db.serialize();
|
|
5971
|
+
mkdirSync12(dirname12(dest), { recursive: true, mode: 448 });
|
|
5972
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
5973
|
+
writeFileSync9(tmp, snapshot, { mode: 384 });
|
|
5974
|
+
const fd = openSync3(tmp, "r+");
|
|
5975
|
+
fsyncSync3(fd);
|
|
5976
|
+
closeSync3(fd);
|
|
5977
|
+
renameSync3(tmp, dest);
|
|
5978
|
+
chmodSync7(dest, 384);
|
|
5979
|
+
setMemoryMeta(db, BACKUP_DONE_KEY, String(nowEpoch));
|
|
5980
|
+
}
|
|
5981
|
+
async function exportSharedMemories(db, transport, opts = {}) {
|
|
5982
|
+
const home = opts.home ?? homedir11();
|
|
5983
|
+
const nowEpoch = opts.nowEpoch ?? Math.floor(Date.now() / 1e3);
|
|
5984
|
+
const caps = opts.caps ?? DEFAULT_EXPORT_CAPS;
|
|
5985
|
+
const config = getConfig();
|
|
5986
|
+
const shareEnabled = opts.shareEnabled ?? config.memory?.share?.enabled ?? false;
|
|
5987
|
+
if (!shareEnabled) return zero(false);
|
|
5988
|
+
const tier = opts.tier ?? getCachedTierReadOnly(db);
|
|
5989
|
+
if (!entitledForCrossRepoSurfacing(tier)) return zero(true);
|
|
5990
|
+
const repoId = getRepoId(db) ?? mintRepoId(db);
|
|
5991
|
+
const label = getRepoLabel();
|
|
5992
|
+
ensureLocalShareKeypair(home);
|
|
5993
|
+
const fingerprint = localSharePubkeyFingerprint(home);
|
|
5994
|
+
upsertRepoRegistration(
|
|
5995
|
+
{
|
|
5996
|
+
repo_id: repoId,
|
|
5997
|
+
label,
|
|
5998
|
+
pubkey_fingerprint: fingerprint,
|
|
5999
|
+
last_seen_path: getProjectRoot(),
|
|
6000
|
+
share_enabled: true
|
|
6001
|
+
},
|
|
6002
|
+
home
|
|
6003
|
+
);
|
|
6004
|
+
try {
|
|
6005
|
+
ensureSharingBackup(db, repoId, home, nowEpoch, opts.backupDir);
|
|
6006
|
+
} catch (err) {
|
|
6007
|
+
recordTelemetry(db, "shared_memory_export_refused", {
|
|
6008
|
+
reason: "backup_failed",
|
|
6009
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
6010
|
+
});
|
|
6011
|
+
auditExportRefused(db, null, "backup_failed");
|
|
6012
|
+
return { ...zero(true), refused: 1, refusals: [{ observation_id: null, reason: "backup_failed" }] };
|
|
6013
|
+
}
|
|
6014
|
+
const rows = db.prepare(
|
|
6015
|
+
`SELECT id, type, title, detail, importance, created_at_epoch, origin
|
|
6016
|
+
FROM observations
|
|
6017
|
+
WHERE shareable = 1
|
|
6018
|
+
ORDER BY id
|
|
6019
|
+
LIMIT ?`
|
|
6020
|
+
).all(caps.maxRecords + 1);
|
|
6021
|
+
if (rows.length > caps.maxRecords) {
|
|
6022
|
+
recordTelemetry(db, "shared_memory_export_refused", { reason: "too_many_records", count: rows.length });
|
|
6023
|
+
auditExportRefused(db, null, "too_many_records");
|
|
6024
|
+
return { ...zero(true), refused: 1, refusals: [{ observation_id: null, reason: "too_many_records" }] };
|
|
6025
|
+
}
|
|
6026
|
+
const wasExported = db.prepare(`SELECT 1 FROM shared_memory_outbound WHERE record_hash = ? LIMIT 1`);
|
|
6027
|
+
const refusals = [];
|
|
6028
|
+
const records = [];
|
|
6029
|
+
const outboundInserts = [];
|
|
6030
|
+
for (const r of rows) {
|
|
6031
|
+
if (!isLocalOrigin(r.origin)) {
|
|
6032
|
+
refusals.push({ observation_id: r.id, reason: "non_local_origin" });
|
|
6033
|
+
auditExportRefused(db, r.id, "non_local_origin");
|
|
6034
|
+
continue;
|
|
6035
|
+
}
|
|
6036
|
+
const title = r.title ?? "";
|
|
6037
|
+
const detail = r.detail ?? "";
|
|
6038
|
+
const sTitle = containsSecret(title);
|
|
6039
|
+
const sDetail = containsSecret(detail);
|
|
6040
|
+
if (sTitle.matched || sDetail.matched) {
|
|
6041
|
+
const reason = `secret:${sTitle.patternName ?? sDetail.patternName}`;
|
|
6042
|
+
refusals.push({ observation_id: r.id, reason });
|
|
6043
|
+
auditExportRefused(db, r.id, reason);
|
|
6044
|
+
continue;
|
|
6045
|
+
}
|
|
6046
|
+
if (home && (title.includes(home) || detail.includes(home))) {
|
|
6047
|
+
refusals.push({ observation_id: r.id, reason: "home_path" });
|
|
6048
|
+
auditExportRefused(db, r.id, "home_path");
|
|
6049
|
+
continue;
|
|
6050
|
+
}
|
|
6051
|
+
const base = {
|
|
6052
|
+
type: r.type,
|
|
6053
|
+
title,
|
|
6054
|
+
detail,
|
|
6055
|
+
importance: r.importance,
|
|
6056
|
+
created_at_epoch: r.created_at_epoch,
|
|
6057
|
+
superseded_by_hash: null
|
|
6058
|
+
};
|
|
6059
|
+
const record_hash = hashSharedMemoryRecord(base);
|
|
6060
|
+
if (wasExported.get(record_hash)) continue;
|
|
6061
|
+
const recordBytes = Buffer.byteLength(JSON.stringify({ ...base, record_hash }), "utf-8");
|
|
6062
|
+
if (recordBytes > caps.maxBytesPerRecord) {
|
|
6063
|
+
refusals.push({ observation_id: r.id, reason: "record_too_large" });
|
|
6064
|
+
auditExportRefused(db, r.id, "record_too_large");
|
|
6065
|
+
continue;
|
|
6066
|
+
}
|
|
6067
|
+
records.push({ ...base, record_hash });
|
|
6068
|
+
outboundInserts.push({ hash: record_hash, id: r.id });
|
|
6069
|
+
}
|
|
6070
|
+
const revokeRows = db.prepare(
|
|
6071
|
+
`SELECT o.record_hash AS h
|
|
6072
|
+
FROM shared_memory_outbound o
|
|
6073
|
+
JOIN observations obs ON obs.id = o.observation_id
|
|
6074
|
+
WHERE o.revoked_at_epoch IS NULL
|
|
6075
|
+
AND obs.expired_at IS NOT NULL
|
|
6076
|
+
LIMIT ?`
|
|
6077
|
+
).all(caps.maxRecords);
|
|
6078
|
+
const revokedHashes = revokeRows.map((r) => r.h);
|
|
6079
|
+
if (records.length === 0 && revokedHashes.length === 0) {
|
|
6080
|
+
return { ...zero(true), refused: refusals.length, refusals };
|
|
6081
|
+
}
|
|
6082
|
+
const recordsJson = JSON.stringify(records);
|
|
6083
|
+
if (Buffer.byteLength(recordsJson, "utf-8") > caps.maxTotalBytes) {
|
|
6084
|
+
recordTelemetry(db, "shared_memory_export_refused", { reason: "envelope_too_large" });
|
|
6085
|
+
auditExportRefused(db, null, "envelope_too_large");
|
|
6086
|
+
return { ...zero(true), refused: refusals.length + 1, refusals: [...refusals, { observation_id: null, reason: "envelope_too_large" }] };
|
|
6087
|
+
}
|
|
6088
|
+
const seq = nextSeq(db);
|
|
6089
|
+
const body = {
|
|
6090
|
+
kind: SHARED_MEMORY_KIND,
|
|
6091
|
+
origin_repo_id: repoId,
|
|
6092
|
+
origin_repo_label: label,
|
|
6093
|
+
seq,
|
|
6094
|
+
issued_at: new Date(nowEpoch * 1e3).toISOString(),
|
|
6095
|
+
records_json: recordsJson,
|
|
6096
|
+
revokes_json: JSON.stringify(revokedHashes)
|
|
6097
|
+
};
|
|
6098
|
+
const envelope = signSharedMemoryEnvelope(body, home);
|
|
6099
|
+
await transport.publish(envelope);
|
|
6100
|
+
const insertOutbound = db.prepare(
|
|
6101
|
+
`INSERT OR IGNORE INTO shared_memory_outbound (record_hash, observation_id, origin_repo_id, seq, exported_at_epoch)
|
|
6102
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
6103
|
+
);
|
|
6104
|
+
const markRevoked = db.prepare(
|
|
6105
|
+
`UPDATE shared_memory_outbound SET revoked_at_epoch = ? WHERE record_hash = ? AND revoked_at_epoch IS NULL`
|
|
6106
|
+
);
|
|
6107
|
+
const commit = db.transaction(() => {
|
|
6108
|
+
for (const o of outboundInserts) insertOutbound.run(o.hash, o.id, repoId, seq, nowEpoch);
|
|
6109
|
+
for (const h of revokedHashes) markRevoked.run(nowEpoch, h);
|
|
6110
|
+
setMemoryMeta(db, EXPORT_SEQ_KEY, String(seq));
|
|
6111
|
+
});
|
|
6112
|
+
commit();
|
|
6113
|
+
recordTelemetry(db, "shared_memory_exported", { count: records.length, revoked: revokedHashes.length, seq });
|
|
6114
|
+
auditExported(db, records.length, seq);
|
|
6115
|
+
return { enabled: true, published: true, exported: records.length, refused: refusals.length, refusals, seq };
|
|
6116
|
+
}
|
|
6117
|
+
function nextSeq(db) {
|
|
6118
|
+
const raw = getMemoryMeta(db, EXPORT_SEQ_KEY);
|
|
6119
|
+
const last = raw !== null && Number.isInteger(Number(raw)) ? Number(raw) : 0;
|
|
6120
|
+
return last + 1;
|
|
6121
|
+
}
|
|
6122
|
+
function auditExported(db, count, seq) {
|
|
6123
|
+
writeSharedAudit(db, "shared_memory_exported", `exported ${count} record(s) at seq ${seq}`, { count, seq });
|
|
6124
|
+
}
|
|
6125
|
+
function auditExportRefused(db, obsId, reason) {
|
|
6126
|
+
writeSharedAudit(db, "shared_memory_export_refused", `export refused: ${reason}`, { observation_id: obsId, reason });
|
|
6127
|
+
}
|
|
6128
|
+
|
|
6129
|
+
// src/shared-memory-transport.ts
|
|
6130
|
+
import {
|
|
6131
|
+
chmodSync as chmodSync8,
|
|
6132
|
+
existsSync as existsSync15,
|
|
6133
|
+
mkdirSync as mkdirSync13,
|
|
6134
|
+
readdirSync as readdirSync2,
|
|
6135
|
+
readFileSync as readFileSync11
|
|
6136
|
+
} from "fs";
|
|
6137
|
+
import { homedir as homedir12 } from "os";
|
|
6138
|
+
import { join as join10 } from "path";
|
|
6139
|
+
import { createHash as createHash4 } from "crypto";
|
|
6140
|
+
var REPO_ID_PATH_RE = /^[0-9a-f-]{36}$/;
|
|
6141
|
+
var CONTENT_HASH_RE = /^[0-9a-f]{64}$/;
|
|
6142
|
+
function sharedRootDir(home = homedir12()) {
|
|
6143
|
+
return join10(home, ".massu", "shared");
|
|
6144
|
+
}
|
|
6145
|
+
function outboxDirFor(originRepoId, home = homedir12()) {
|
|
6146
|
+
if (!REPO_ID_PATH_RE.test(originRepoId)) {
|
|
6147
|
+
throw new Error(`refusing to use an unvalidated repo_id as a path component: ${originRepoId}`);
|
|
6148
|
+
}
|
|
6149
|
+
const rel = join10("outbox", originRepoId);
|
|
6150
|
+
return assertContainedIn(sharedRootDir(home), rel, { allowNested: true });
|
|
6151
|
+
}
|
|
6152
|
+
function envelopeContentHash(env) {
|
|
6153
|
+
return createHash4("sha256").update(JSON.stringify(env), "utf-8").digest("hex");
|
|
6154
|
+
}
|
|
6155
|
+
var LocalFsTransport = class {
|
|
6156
|
+
constructor(home = homedir12()) {
|
|
6157
|
+
this.home = home;
|
|
6158
|
+
}
|
|
6159
|
+
home;
|
|
6160
|
+
async publish(env) {
|
|
6161
|
+
const dir = outboxDirFor(env.origin_repo_id, this.home);
|
|
6162
|
+
mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
6163
|
+
const hash = envelopeContentHash(env);
|
|
6164
|
+
if (!CONTENT_HASH_RE.test(hash)) {
|
|
6165
|
+
throw new Error(`refusing to write an envelope with a malformed content hash: ${hash}`);
|
|
6166
|
+
}
|
|
6167
|
+
const dest = assertContainedIn(dir, `${seqPrefix(env.seq)}-${hash}.json`, { allowNested: false });
|
|
6168
|
+
atomicWriteFileSync(dest, JSON.stringify(env, null, 2));
|
|
6169
|
+
chmodSync8(dest, 384);
|
|
6170
|
+
}
|
|
6171
|
+
async fetchSince(originRepoId, sinceSeq) {
|
|
6172
|
+
if (!REPO_ID_PATH_RE.test(originRepoId)) return [];
|
|
6173
|
+
let dir;
|
|
6174
|
+
try {
|
|
6175
|
+
dir = outboxDirFor(originRepoId, this.home);
|
|
6176
|
+
} catch {
|
|
6177
|
+
return [];
|
|
6178
|
+
}
|
|
6179
|
+
if (!existsSync15(dir)) return [];
|
|
6180
|
+
const out = [];
|
|
6181
|
+
for (const name of readdirSync2(dir)) {
|
|
6182
|
+
if (!name.endsWith(".json")) continue;
|
|
6183
|
+
const seqFromName = parseSeqPrefix(name);
|
|
6184
|
+
if (seqFromName !== null && seqFromName <= sinceSeq) continue;
|
|
6185
|
+
let env;
|
|
6186
|
+
try {
|
|
6187
|
+
const p = assertContainedIn(dir, name, { allowNested: false });
|
|
6188
|
+
env = JSON.parse(readFileSync11(p, "utf-8"));
|
|
6189
|
+
} catch {
|
|
6190
|
+
continue;
|
|
6191
|
+
}
|
|
6192
|
+
if (typeof env?.seq === "number" && env.seq > sinceSeq) out.push(env);
|
|
6193
|
+
}
|
|
6194
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
6195
|
+
return out;
|
|
6196
|
+
}
|
|
6197
|
+
};
|
|
6198
|
+
function seqPrefix(seq) {
|
|
6199
|
+
const s = Number.isInteger(seq) && seq >= 0 ? seq : 0;
|
|
6200
|
+
return String(s).padStart(16, "0");
|
|
6201
|
+
}
|
|
6202
|
+
function parseSeqPrefix(name) {
|
|
6203
|
+
const m = name.match(/^(\d{1,16})-/);
|
|
6204
|
+
return m ? Number(m[1]) : null;
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
// src/hooks/session-end.ts
|
|
6208
|
+
init_config();
|
|
6209
|
+
|
|
6210
|
+
// src/analytics.ts
|
|
6211
|
+
init_config();
|
|
6212
|
+
var DEFAULT_WEIGHTS = {
|
|
6213
|
+
bug_found: -5,
|
|
6214
|
+
vr_failure: -10,
|
|
6215
|
+
incident: -20,
|
|
6216
|
+
cr_violation: -3,
|
|
6217
|
+
vr_pass: 2,
|
|
6218
|
+
clean_commit: 5,
|
|
6219
|
+
successful_verification: 3
|
|
6220
|
+
};
|
|
6221
|
+
var DEFAULT_CATEGORIES = ["security", "architecture", "coupling", "tests", "rule_compliance"];
|
|
6222
|
+
function getWeights() {
|
|
6223
|
+
return getConfig().analytics?.quality?.weights ?? DEFAULT_WEIGHTS;
|
|
6224
|
+
}
|
|
6225
|
+
function getCategories() {
|
|
6226
|
+
return getConfig().analytics?.quality?.categories ?? DEFAULT_CATEGORIES;
|
|
6227
|
+
}
|
|
6228
|
+
function calculateQualityScore(db, sessionId) {
|
|
6229
|
+
const weights = getWeights();
|
|
6230
|
+
const categories = getCategories();
|
|
6231
|
+
const observations = db.prepare(
|
|
6232
|
+
"SELECT type, detail FROM observations WHERE session_id = ? LIMIT 10000"
|
|
6233
|
+
).all(sessionId);
|
|
6234
|
+
let score = 50;
|
|
6235
|
+
const breakdown = Object.fromEntries(
|
|
6236
|
+
categories.map((c) => [c, 0])
|
|
6237
|
+
);
|
|
6238
|
+
for (const obs of observations) {
|
|
6239
|
+
const weight = weights[obs.type] ?? 0;
|
|
6240
|
+
score += weight;
|
|
6241
|
+
const desc = (obs.detail ?? "").toLowerCase();
|
|
6242
|
+
for (const category of categories) {
|
|
6243
|
+
if (desc.includes(category)) {
|
|
6244
|
+
breakdown[category] += weight;
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
return {
|
|
6249
|
+
score: Math.max(0, Math.min(100, score)),
|
|
6250
|
+
breakdown
|
|
6251
|
+
};
|
|
6252
|
+
}
|
|
6253
|
+
function storeQualityScore(db, sessionId, score, breakdown) {
|
|
6254
|
+
db.prepare(`
|
|
6255
|
+
INSERT INTO session_quality_scores
|
|
6256
|
+
(session_id, score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score)
|
|
6257
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
6258
|
+
`).run(
|
|
6259
|
+
sessionId,
|
|
6260
|
+
score,
|
|
6261
|
+
breakdown.security ?? 0,
|
|
6262
|
+
breakdown.architecture ?? 0,
|
|
6263
|
+
breakdown.coupling ?? 0,
|
|
6264
|
+
breakdown.tests ?? 0,
|
|
6265
|
+
breakdown.rule_compliance ?? 0
|
|
6266
|
+
);
|
|
6267
|
+
}
|
|
6268
|
+
function backfillQualityScores(db) {
|
|
6269
|
+
const sessions = db.prepare(`
|
|
6270
|
+
SELECT DISTINCT s.session_id
|
|
6271
|
+
FROM sessions s
|
|
6272
|
+
LEFT JOIN session_quality_scores q ON s.session_id = q.session_id
|
|
6273
|
+
WHERE q.session_id IS NULL
|
|
6274
|
+
LIMIT 100000
|
|
6275
|
+
`).all();
|
|
6276
|
+
let backfilled = 0;
|
|
6277
|
+
for (const session of sessions) {
|
|
6278
|
+
const { score, breakdown } = calculateQualityScore(db, session.session_id);
|
|
6279
|
+
storeQualityScore(db, session.session_id, score, breakdown);
|
|
6280
|
+
backfilled++;
|
|
6281
|
+
}
|
|
6282
|
+
return backfilled;
|
|
6283
|
+
}
|
|
6284
|
+
|
|
6285
|
+
// src/cost-tracker.ts
|
|
6286
|
+
init_config();
|
|
6287
|
+
var DEFAULT_MODEL_PRICING = {
|
|
6288
|
+
"claude-opus-4-6": { input_per_million: 15, output_per_million: 75, cache_read_per_million: 1.5, cache_write_per_million: 18.75 },
|
|
6289
|
+
"claude-sonnet-4-6": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
|
|
6290
|
+
"claude-sonnet-4-5": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
|
|
6291
|
+
"claude-haiku-4-5-20251001": { input_per_million: 0.8, output_per_million: 4, cache_read_per_million: 0.08, cache_write_per_million: 1 },
|
|
6292
|
+
"default": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 }
|
|
6293
|
+
};
|
|
6294
|
+
function getModelPricing() {
|
|
6295
|
+
return getConfig().analytics?.cost?.models ?? DEFAULT_MODEL_PRICING;
|
|
6296
|
+
}
|
|
6297
|
+
function getCurrency() {
|
|
6298
|
+
return getConfig().analytics?.cost?.currency ?? "USD";
|
|
6299
|
+
}
|
|
6300
|
+
function extractTokenUsage(entries) {
|
|
6301
|
+
let inputTokens = 0;
|
|
6302
|
+
let outputTokens = 0;
|
|
6303
|
+
let cacheReadTokens = 0;
|
|
6304
|
+
let cacheWriteTokens = 0;
|
|
6305
|
+
let model = "unknown";
|
|
6306
|
+
for (const entry of entries) {
|
|
6307
|
+
const msg = entry.message;
|
|
6308
|
+
if (entry.type === "assistant" && msg?.usage) {
|
|
6309
|
+
const usage = msg.usage;
|
|
6310
|
+
inputTokens += usage.input_tokens ?? 0;
|
|
6311
|
+
outputTokens += usage.output_tokens ?? 0;
|
|
6312
|
+
cacheReadTokens += usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? 0;
|
|
6313
|
+
cacheWriteTokens += usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
|
|
6314
|
+
}
|
|
6315
|
+
if (entry.type === "assistant" && msg?.model) {
|
|
6316
|
+
model = msg.model;
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
6319
|
+
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, model };
|
|
6320
|
+
}
|
|
6321
|
+
function calculateCost(usage) {
|
|
6322
|
+
const pricing = getModelPricing();
|
|
6323
|
+
const modelPricing = pricing[usage.model] ?? pricing["default"] ?? pricing["claude-sonnet-4-5"] ?? { input_per_million: 3, output_per_million: 15 };
|
|
6324
|
+
const inputCost = usage.inputTokens / 1e6 * modelPricing.input_per_million;
|
|
5077
6325
|
const outputCost = usage.outputTokens / 1e6 * modelPricing.output_per_million;
|
|
5078
6326
|
const cacheReadCost = usage.cacheReadTokens / 1e6 * (modelPricing.cache_read_per_million ?? 0);
|
|
5079
6327
|
const cacheWriteCost = usage.cacheWriteTokens / 1e6 * (modelPricing.cache_write_per_million ?? 0);
|
|
@@ -5107,7 +6355,7 @@ function storeSessionCost(db, sessionId, usage, cost) {
|
|
|
5107
6355
|
|
|
5108
6356
|
// src/prompt-analyzer.ts
|
|
5109
6357
|
init_config();
|
|
5110
|
-
import { createHash as
|
|
6358
|
+
import { createHash as createHash5 } from "crypto";
|
|
5111
6359
|
|
|
5112
6360
|
// src/security-utils.ts
|
|
5113
6361
|
function escapeRegex(str) {
|
|
@@ -5131,7 +6379,7 @@ function categorizePrompt(promptText) {
|
|
|
5131
6379
|
}
|
|
5132
6380
|
function hashPrompt(promptText) {
|
|
5133
6381
|
const normalized = promptText.toLowerCase().replace(/\s+/g, " ").trim();
|
|
5134
|
-
return
|
|
6382
|
+
return createHash5("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
5135
6383
|
}
|
|
5136
6384
|
function detectOutcome(followUpPrompts, assistantResponses) {
|
|
5137
6385
|
let correctionsNeeded = 0;
|
|
@@ -5744,160 +6992,9 @@ ${d.decision}`.trim(),
|
|
|
5744
6992
|
|
|
5745
6993
|
// src/memory-consolidate.ts
|
|
5746
6994
|
init_memory_db();
|
|
5747
|
-
import { createHash as
|
|
5748
|
-
import { existsSync as
|
|
5749
|
-
import { join as
|
|
5750
|
-
|
|
5751
|
-
// src/consolidation-config.ts
|
|
5752
|
-
init_config();
|
|
5753
|
-
var DEFAULT_CONSOLIDATION_CONFIG = {
|
|
5754
|
-
enabled: true,
|
|
5755
|
-
sessionSweepEnabled: true,
|
|
5756
|
-
// llmEndpoint / llmModel deliberately UNSET: the shipped default is
|
|
5757
|
-
// zero-LLM, zero-network, works offline on any machine.
|
|
5758
|
-
summarizeAfterDays: 5,
|
|
5759
|
-
// inside the 7-day conversation_turns prune window
|
|
5760
|
-
retentionDays: 90,
|
|
5761
|
-
importanceFloor: 2,
|
|
5762
|
-
protectedTypes: ["decision", "cr_violation", "incident_near_miss"],
|
|
5763
|
-
usageWarmupDays: 30,
|
|
5764
|
-
usageDecay: 0.9,
|
|
5765
|
-
reweightIntervalDays: 1,
|
|
5766
|
-
promoteMinOccurrences: 3,
|
|
5767
|
-
budgetMs: 3e3,
|
|
5768
|
-
suggestUpgrades: true,
|
|
5769
|
-
suggestIntervalDays: 30
|
|
5770
|
-
};
|
|
5771
|
-
function resolveConsolidationConfig() {
|
|
5772
|
-
try {
|
|
5773
|
-
const c = getConfig().memory?.consolidation;
|
|
5774
|
-
if (!c) return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5775
|
-
return { ...DEFAULT_CONSOLIDATION_CONFIG, ...c };
|
|
5776
|
-
} catch {
|
|
5777
|
-
return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5778
|
-
}
|
|
5779
|
-
}
|
|
5780
|
-
|
|
5781
|
-
// src/memory-llm.ts
|
|
5782
|
-
var LLM_API_KEY_ENV = "MASSU_MEMORY_LLM_API_KEY";
|
|
5783
|
-
var DEFAULT_BUDGET_MS = 2e4;
|
|
5784
|
-
var DEFAULT_MAX_CHARS = 900;
|
|
5785
|
-
var NOISE_PATTERNS = [
|
|
5786
|
-
/<command-(name|message|args)>/i,
|
|
5787
|
-
/<local-command-[^>]*>/i,
|
|
5788
|
-
/^\s*<[a-z-]+>\s*$/i,
|
|
5789
|
-
/^\s*(ok|okay|thanks|thank you|yes|no|yep|nope|sure|continue|proceed|go ahead)\s*[.!]?\s*$/i,
|
|
5790
|
-
/^\s*\/[a-z-]+\s*$/i,
|
|
5791
|
-
// a bare slash-command invocation
|
|
5792
|
-
/system-reminder/i
|
|
5793
|
-
];
|
|
5794
|
-
function isSummarizableSignal(text) {
|
|
5795
|
-
const t = text.trim();
|
|
5796
|
-
if (t.length < 25) return false;
|
|
5797
|
-
return !NOISE_PATTERNS.some((re) => re.test(t));
|
|
5798
|
-
}
|
|
5799
|
-
var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
|
|
5800
|
-
var LABELLED_CREDENTIAL = new RegExp(
|
|
5801
|
-
`\\b[A-Za-z0-9_-]*(?:${CREDENTIAL_LABELS.join("|")})["'\\s:=]+[A-Za-z0-9_\\-/+]{16,}`,
|
|
5802
|
-
"gi"
|
|
5803
|
-
);
|
|
5804
|
-
var SECRET_PATTERNS = [
|
|
5805
|
-
[/\bms_live_[A-Za-z0-9_-]{6,}/g, "ms_live_[REDACTED]", "MASSU_LIVE_KEY"],
|
|
5806
|
-
[/\bsk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]", "OPENAI_STYLE_KEY"],
|
|
5807
|
-
[/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{16,}/g, "[REDACTED_TOKEN]", "GITHUB_TOKEN"],
|
|
5808
|
-
[/\bsbp_[A-Za-z0-9]{16,}/g, "sbp_[REDACTED]", "SUPABASE_TOKEN"],
|
|
5809
|
-
[/\bAKIA[0-9A-Z]{12,}/g, "[REDACTED_AWS_KEY]", "AWS_ACCESS_KEY"],
|
|
5810
|
-
[
|
|
5811
|
-
/\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g,
|
|
5812
|
-
"[REDACTED_JWT]",
|
|
5813
|
-
"JWT"
|
|
5814
|
-
],
|
|
5815
|
-
[LABELLED_CREDENTIAL, "[REDACTED_CREDENTIAL]", "LABELLED_CREDENTIAL"]
|
|
5816
|
-
];
|
|
5817
|
-
function redactSecrets(text) {
|
|
5818
|
-
let out = text;
|
|
5819
|
-
for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
|
|
5820
|
-
return out;
|
|
5821
|
-
}
|
|
5822
|
-
function extractiveSummary(sources, maxChars = DEFAULT_MAX_CHARS) {
|
|
5823
|
-
const cleaned = sources.map((s, i) => ({ ...s, i, text: s.text.replace(/\s+/g, " ").trim() })).filter((s) => isSummarizableSignal(s.text));
|
|
5824
|
-
if (cleaned.length === 0) return "";
|
|
5825
|
-
const ranked = [...cleaned].sort((a, b) => b.weight - a.weight || a.i - b.i);
|
|
5826
|
-
const picked = [];
|
|
5827
|
-
let used = 0;
|
|
5828
|
-
for (const s of ranked) {
|
|
5829
|
-
const cost = s.text.length + 1;
|
|
5830
|
-
if (used + cost > maxChars) continue;
|
|
5831
|
-
picked.push(s);
|
|
5832
|
-
used += cost;
|
|
5833
|
-
}
|
|
5834
|
-
if (picked.length === 0) {
|
|
5835
|
-
return ranked[0].text.slice(0, maxChars);
|
|
5836
|
-
}
|
|
5837
|
-
picked.sort((a, b) => a.i - b.i);
|
|
5838
|
-
return picked.map((s) => s.text).join(" ");
|
|
5839
|
-
}
|
|
5840
|
-
async function summarizeViaEndpoint(endpoint, model, material, budgetMs, maxChars) {
|
|
5841
|
-
try {
|
|
5842
|
-
const controller = new AbortController();
|
|
5843
|
-
const timer = setTimeout(() => controller.abort(), budgetMs);
|
|
5844
|
-
const apiKey = process.env[LLM_API_KEY_ENV];
|
|
5845
|
-
const headers = { "content-type": "application/json" };
|
|
5846
|
-
if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;
|
|
5847
|
-
let resp;
|
|
5848
|
-
try {
|
|
5849
|
-
resp = await fetch(`${endpoint.replace(/\/+$/, "")}/v1/chat/completions`, {
|
|
5850
|
-
method: "POST",
|
|
5851
|
-
headers,
|
|
5852
|
-
body: JSON.stringify({
|
|
5853
|
-
model,
|
|
5854
|
-
// a NAME/ALIAS the user configured — never a physical model id
|
|
5855
|
-
messages: [
|
|
5856
|
-
{
|
|
5857
|
-
role: "system",
|
|
5858
|
-
content: "You distill a software engineering session into ONE durable lesson a developer will read months later. Be concrete and factual. State what broke, what was tried and rejected, and what actually worked. Invent nothing that is not in the input."
|
|
5859
|
-
},
|
|
5860
|
-
{ role: "user", content: material }
|
|
5861
|
-
],
|
|
5862
|
-
max_tokens: 400,
|
|
5863
|
-
temperature: 0.2
|
|
5864
|
-
}),
|
|
5865
|
-
signal: controller.signal
|
|
5866
|
-
});
|
|
5867
|
-
} finally {
|
|
5868
|
-
clearTimeout(timer);
|
|
5869
|
-
}
|
|
5870
|
-
if (!resp.ok) return null;
|
|
5871
|
-
const data = await resp.json();
|
|
5872
|
-
const text = data.choices?.[0]?.message?.content;
|
|
5873
|
-
if (typeof text !== "string" || !text.trim()) return null;
|
|
5874
|
-
return text.trim().slice(0, maxChars);
|
|
5875
|
-
} catch {
|
|
5876
|
-
return null;
|
|
5877
|
-
}
|
|
5878
|
-
}
|
|
5879
|
-
async function summarizeText(sources, opts = {}) {
|
|
5880
|
-
const cfg = opts.config ?? resolveConsolidationConfig();
|
|
5881
|
-
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
5882
|
-
const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
5883
|
-
const extractive = extractiveSummary(sources, maxChars);
|
|
5884
|
-
if (!cfg.llmEndpoint || !cfg.llmModel || !extractive) {
|
|
5885
|
-
return { text: extractive, tier: "extractive" };
|
|
5886
|
-
}
|
|
5887
|
-
const material = redactSecrets(
|
|
5888
|
-
sources.map((s) => s.text.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n")
|
|
5889
|
-
);
|
|
5890
|
-
const viaModel = await summarizeViaEndpoint(
|
|
5891
|
-
cfg.llmEndpoint,
|
|
5892
|
-
cfg.llmModel,
|
|
5893
|
-
material,
|
|
5894
|
-
budgetMs,
|
|
5895
|
-
maxChars
|
|
5896
|
-
);
|
|
5897
|
-
return viaModel ? { text: viaModel, tier: "model" } : { text: extractive, tier: "extractive" };
|
|
5898
|
-
}
|
|
5899
|
-
|
|
5900
|
-
// src/memory-consolidate.ts
|
|
6995
|
+
import { createHash as createHash6 } from "crypto";
|
|
6996
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
6997
|
+
import { join as join11 } from "path";
|
|
5901
6998
|
init_memory_embedder();
|
|
5902
6999
|
init_memory_vector();
|
|
5903
7000
|
var LEASE_KEY = "consolidate_lease";
|
|
@@ -6064,7 +7161,7 @@ ${r.detail ?? ""}`.trim());
|
|
|
6064
7161
|
}
|
|
6065
7162
|
clusters.push(cluster);
|
|
6066
7163
|
}
|
|
6067
|
-
const candidateDir =
|
|
7164
|
+
const candidateDir = join11(projectRoot, ".massu", "rule-candidates");
|
|
6068
7165
|
let promoted = 0;
|
|
6069
7166
|
for (const cluster of clusters) {
|
|
6070
7167
|
const occurrences = cluster.reduce((n, c) => n + (c.recurrence_count || 1), 0);
|
|
@@ -6076,15 +7173,15 @@ ${r.detail ?? ""}`.trim());
|
|
|
6076
7173
|
const promptText = `${representative.title}${representative.detail ? `
|
|
6077
7174
|
${representative.detail}` : ""}`;
|
|
6078
7175
|
const clusterKey = cluster.map((c) => c.title.toLowerCase().replace(/\s+/g, " ").trim()).sort().join("|");
|
|
6079
|
-
const promptHash =
|
|
6080
|
-
const candidatePath =
|
|
6081
|
-
if (
|
|
7176
|
+
const promptHash = createHash6("sha256").update(clusterKey).digest("hex").slice(0, 16);
|
|
7177
|
+
const candidatePath = join11(candidateDir, `${promptHash}.json`);
|
|
7178
|
+
if (existsSync16(candidatePath)) continue;
|
|
6082
7179
|
if (dryRun) {
|
|
6083
7180
|
promoted++;
|
|
6084
7181
|
continue;
|
|
6085
7182
|
}
|
|
6086
|
-
|
|
6087
|
-
|
|
7183
|
+
mkdirSync14(candidateDir, { recursive: true });
|
|
7184
|
+
writeFileSync10(
|
|
6088
7185
|
candidatePath,
|
|
6089
7186
|
JSON.stringify(
|
|
6090
7187
|
{
|
|
@@ -6149,7 +7246,7 @@ function stageReweight(db, cfg, now) {
|
|
|
6149
7246
|
MAX_ROWS_PER_STAGE
|
|
6150
7247
|
);
|
|
6151
7248
|
const bump = db.prepare(`UPDATE observations SET importance = importance + 1 WHERE id = ?`);
|
|
6152
|
-
const
|
|
7249
|
+
const drop2 = db.prepare(`UPDATE observations SET importance = importance - 1 WHERE id = ?`);
|
|
6153
7250
|
const mark = db.prepare(
|
|
6154
7251
|
`INSERT INTO memory_usage (source, record_id, hit_count, hits_windowed, last_reweight_epoch)
|
|
6155
7252
|
VALUES ('observation', ?, 0, 0, ?)
|
|
@@ -6162,7 +7259,7 @@ function stageReweight(db, cfg, now) {
|
|
|
6162
7259
|
changed++;
|
|
6163
7260
|
}
|
|
6164
7261
|
for (const r of demote) {
|
|
6165
|
-
|
|
7262
|
+
drop2.run(r.id);
|
|
6166
7263
|
mark.run(r.id, now);
|
|
6167
7264
|
changed++;
|
|
6168
7265
|
}
|
|
@@ -6321,6 +7418,12 @@ async function main() {
|
|
|
6321
7418
|
await pullTeamPromotions(db);
|
|
6322
7419
|
} catch (_pullErr) {
|
|
6323
7420
|
}
|
|
7421
|
+
try {
|
|
7422
|
+
const shareTransport = new LocalFsTransport();
|
|
7423
|
+
await exportSharedMemories(db, shareTransport);
|
|
7424
|
+
await importSharedMemories(db, shareTransport);
|
|
7425
|
+
} catch (_shareErr) {
|
|
7426
|
+
}
|
|
6324
7427
|
} catch (syncErr) {
|
|
6325
7428
|
const msg = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
6326
7429
|
if (openLease) nackLearning(db, openLease, msg);
|
|
@@ -6583,14 +7686,14 @@ function extractFilesFromToolCall(toolName, input) {
|
|
|
6583
7686
|
return [];
|
|
6584
7687
|
}
|
|
6585
7688
|
function readStdin() {
|
|
6586
|
-
return new Promise((
|
|
7689
|
+
return new Promise((resolve8) => {
|
|
6587
7690
|
let data = "";
|
|
6588
7691
|
process.stdin.setEncoding("utf-8");
|
|
6589
7692
|
process.stdin.on("data", (chunk) => {
|
|
6590
7693
|
data += chunk;
|
|
6591
7694
|
});
|
|
6592
|
-
process.stdin.on("end", () =>
|
|
6593
|
-
setTimeout(() =>
|
|
7695
|
+
process.stdin.on("end", () => resolve8(data));
|
|
7696
|
+
setTimeout(() => resolve8(data), 5e3);
|
|
6594
7697
|
});
|
|
6595
7698
|
}
|
|
6596
7699
|
main();
|