mover-os 4.7.7 → 4.7.8
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/install.js +688 -68
- package/package.json +15 -3
- package/src/dashboard/build.js +1503 -0
- package/src/dashboard/dashboard.js +276 -0
- package/src/dashboard/index.js +319 -0
- package/src/dashboard/lib/activation-log.js +297 -0
- package/src/dashboard/lib/active-context-parser.js +177 -0
- package/src/dashboard/lib/agent-command.js +93 -0
- package/src/dashboard/lib/agent-detect.js +255 -0
- package/src/dashboard/lib/agent-detector.js +92 -0
- package/src/dashboard/lib/agent-session.js +462 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/auto-learnings-parser.js +183 -0
- package/src/dashboard/lib/cli-usage-parser.js +92 -0
- package/src/dashboard/lib/config-parser.js +109 -0
- package/src/dashboard/lib/connect-recommender.js +131 -0
- package/src/dashboard/lib/correlations-parser.js +231 -0
- package/src/dashboard/lib/daily-note-resolver.js +211 -0
- package/src/dashboard/lib/date-utils.js +35 -0
- package/src/dashboard/lib/distribution-parser.js +78 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +83 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-health.js +170 -0
- package/src/dashboard/lib/engine-writer.js +1685 -0
- package/src/dashboard/lib/execution-plan.js +125 -0
- package/src/dashboard/lib/experiments-parser.js +429 -0
- package/src/dashboard/lib/feed-parser.js +294 -0
- package/src/dashboard/lib/forked-future.js +60 -0
- package/src/dashboard/lib/goal-forecast.js +304 -0
- package/src/dashboard/lib/goals-parser.js +67 -0
- package/src/dashboard/lib/health-protocols-parser.js +133 -0
- package/src/dashboard/lib/hook-activity.js +48 -0
- package/src/dashboard/lib/hook-indexer.js +169 -0
- package/src/dashboard/lib/hourly-activity-parser.js +143 -0
- package/src/dashboard/lib/identity-parser.js +85 -0
- package/src/dashboard/lib/ingestion.js +418 -0
- package/src/dashboard/lib/library-indexer-v2.js +212 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/log-activation.sh +61 -0
- package/src/dashboard/lib/memory-curator.js +97 -0
- package/src/dashboard/lib/memory-gardener.js +177 -0
- package/src/dashboard/lib/memory-gepa.js +102 -0
- package/src/dashboard/lib/memory-index.js +470 -0
- package/src/dashboard/lib/memory-rerank.js +72 -0
- package/src/dashboard/lib/memory-text.js +136 -0
- package/src/dashboard/lib/metrics-log-parser.js +76 -0
- package/src/dashboard/lib/moves-usage-parser.js +184 -0
- package/src/dashboard/lib/onboarding-forge.js +70 -0
- package/src/dashboard/lib/override-outcome-parser.js +68 -0
- package/src/dashboard/lib/override-summary.js +73 -0
- package/src/dashboard/lib/paths.js +192 -0
- package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
- package/src/dashboard/lib/phantom-strategy.js +129 -0
- package/src/dashboard/lib/project-scanner.js +121 -0
- package/src/dashboard/lib/promise-wall.js +88 -0
- package/src/dashboard/lib/record-score.js +173 -0
- package/src/dashboard/lib/redaction.js +140 -0
- package/src/dashboard/lib/refusal-parser.js +44 -0
- package/src/dashboard/lib/regenerate-manifest.js +206 -0
- package/src/dashboard/lib/rewind-snapshots.js +689 -0
- package/src/dashboard/lib/roast-wall-parser.js +200 -0
- package/src/dashboard/lib/run-registry.js +226 -0
- package/src/dashboard/lib/safe-write.js +63 -0
- package/src/dashboard/lib/session-log-parser.js +145 -0
- package/src/dashboard/lib/session-time-parser.js +158 -0
- package/src/dashboard/lib/skill-index.js +171 -0
- package/src/dashboard/lib/skill-indexer.js +118 -0
- package/src/dashboard/lib/skill-recommender.js +689 -0
- package/src/dashboard/lib/strategy-parser.js +245 -0
- package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
- package/src/dashboard/lib/streak-parser.js +95 -0
- package/src/dashboard/lib/suggested-now.js +254 -0
- package/src/dashboard/lib/tool-awareness.js +125 -0
- package/src/dashboard/lib/transcript-parser.js +331 -0
- package/src/dashboard/lib/vault-graph-parser.js +205 -0
- package/src/dashboard/lib/view-generator.js +163 -0
- package/src/dashboard/lib/voice-dna-parser.js +57 -0
- package/src/dashboard/lib/walkthrough-script.js +140 -0
- package/src/dashboard/lib/workflow-graph-parser.js +170 -0
- package/src/dashboard/lib/workflow-library-parser.js +146 -0
- package/src/dashboard/server.js +2024 -0
- package/src/dashboard/shortcut.js +0 -0
- package/src/dashboard/static/setup-poc.html +306 -0
- package/src/dashboard/static/walkthrough-poc.html +580 -0
- package/src/dashboard/styles.css +1201 -0
- package/src/dashboard/templates/index.html +278 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hero.png +0 -0
- package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
- package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
- package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
- package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
- package/src/dashboard/ui/dist/icon.svg +4 -0
- package/src/dashboard/ui/dist/index.html +18 -0
- package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
- package/src/dashboard/ui/dist/registerSW.js +1 -0
- package/src/dashboard/ui/dist/sw.js +1 -0
- package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const {
|
|
6
|
+
libraryDir,
|
|
7
|
+
libraryCheatsheetsDir,
|
|
8
|
+
libraryPrinciplesDir,
|
|
9
|
+
librarySopsDir,
|
|
10
|
+
libraryEntitiesDir,
|
|
11
|
+
libraryEntitiesPeopleDir,
|
|
12
|
+
libraryEntitiesOrgsDir,
|
|
13
|
+
libraryEntitiesPlacesDir,
|
|
14
|
+
libraryEntitiesDecisionsDir,
|
|
15
|
+
libraryInputsDir,
|
|
16
|
+
libraryScriptsDir,
|
|
17
|
+
libraryMocsDir,
|
|
18
|
+
safeListDir,
|
|
19
|
+
safeStat,
|
|
20
|
+
exists
|
|
21
|
+
} = require("./paths");
|
|
22
|
+
|
|
23
|
+
// Index 03_Library/* — counts + recent items per subdirectory.
|
|
24
|
+
|
|
25
|
+
function indexLibrary(vault) {
|
|
26
|
+
if (!exists(libraryDir(vault))) return { available: false };
|
|
27
|
+
|
|
28
|
+
const sections = [
|
|
29
|
+
{ name: "Cheatsheets", dir: libraryCheatsheetsDir(vault) },
|
|
30
|
+
{ name: "Principles", dir: libraryPrinciplesDir(vault) },
|
|
31
|
+
{ name: "SOPs", dir: librarySopsDir(vault) },
|
|
32
|
+
{ name: "MOCs", dir: libraryMocsDir(vault) },
|
|
33
|
+
{ name: "Scripts", dir: libraryScriptsDir(vault) }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const out = { available: true, sections: [], recent: [], counts: { total: 0 } };
|
|
37
|
+
|
|
38
|
+
for (const sec of sections) {
|
|
39
|
+
if (!exists(sec.dir)) {
|
|
40
|
+
out.sections.push({ name: sec.name, count: 0, recent: [] });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const entries = walkMarkdown(sec.dir);
|
|
44
|
+
entries.sort((a, b) => b.mtime - a.mtime);
|
|
45
|
+
const recent = entries.slice(0, 3).map(e => ({
|
|
46
|
+
name: e.name.replace(/\.md$/, ""),
|
|
47
|
+
mtime: e.mtime,
|
|
48
|
+
ageDays: Math.floor((Date.now() - e.mtime) / 86400000)
|
|
49
|
+
}));
|
|
50
|
+
out.sections.push({ name: sec.name, count: entries.length, recent });
|
|
51
|
+
out.counts[sec.name.toLowerCase()] = entries.length;
|
|
52
|
+
out.counts.total += entries.length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Entities
|
|
56
|
+
const entitySubs = [
|
|
57
|
+
{ name: "People", dir: libraryEntitiesPeopleDir(vault) },
|
|
58
|
+
{ name: "Orgs", dir: libraryEntitiesOrgsDir(vault) },
|
|
59
|
+
{ name: "Places", dir: libraryEntitiesPlacesDir(vault) },
|
|
60
|
+
{ name: "Decisions", dir: libraryEntitiesDecisionsDir(vault) }
|
|
61
|
+
];
|
|
62
|
+
out.entities = entitySubs.map(s => {
|
|
63
|
+
const entries = exists(s.dir) ? walkMarkdown(s.dir) : [];
|
|
64
|
+
entries.sort((a, b) => b.mtime - a.mtime);
|
|
65
|
+
return {
|
|
66
|
+
name: s.name,
|
|
67
|
+
count: entries.length,
|
|
68
|
+
recent: entries.slice(0, 5).map(e => ({
|
|
69
|
+
name: e.name.replace(/\.md$/, ""),
|
|
70
|
+
mtime: e.mtime,
|
|
71
|
+
ageDays: Math.floor((Date.now() - e.mtime) / 86400000)
|
|
72
|
+
}))
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Inputs (Articles, Books, Videos)
|
|
77
|
+
if (exists(libraryInputsDir(vault))) {
|
|
78
|
+
const inputSubs = safeListDir(libraryInputsDir(vault), { dirsOnly: true });
|
|
79
|
+
out.inputs = inputSubs.map(sub => {
|
|
80
|
+
const entries = walkMarkdown(sub.path);
|
|
81
|
+
return { name: sub.name, count: entries.length };
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
out.inputs = [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkMarkdown(dir, depth = 0) {
|
|
91
|
+
if (depth > 3) return [];
|
|
92
|
+
const out = [];
|
|
93
|
+
const entries = safeListDir(dir);
|
|
94
|
+
for (const e of entries) {
|
|
95
|
+
if (e.isFile && e.name.endsWith(".md") && !e.name.startsWith("_")) {
|
|
96
|
+
const stat = safeStat(e.path);
|
|
97
|
+
if (stat) out.push({ name: e.name, path: e.path, mtime: stat.mtime.getTime() });
|
|
98
|
+
} else if (e.isDir && !e.name.startsWith("_") && !e.name.startsWith(".")) {
|
|
99
|
+
out.push(...walkMarkdown(e.path, depth + 1));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { indexLibrary };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# log-activation.sh — POSTs a V5.0 activation event to a running dashboard.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# log-activation.sh <event_type> [meta_json]
|
|
6
|
+
#
|
|
7
|
+
# Examples:
|
|
8
|
+
# log-activation.sh log_completed
|
|
9
|
+
# log-activation.sh referral_mentioned '{"source":"twitter"}'
|
|
10
|
+
# log-activation.sh onboarding_finished
|
|
11
|
+
#
|
|
12
|
+
# Exits silently (best-effort) if:
|
|
13
|
+
# - No token file at ~/.mover/dashboard/.token (server not running)
|
|
14
|
+
# - curl missing
|
|
15
|
+
# - HTTP failure
|
|
16
|
+
#
|
|
17
|
+
# Used by workflow Bash steps (log.md, capture.md, pivot-strategy.md)
|
|
18
|
+
# and Stop hooks. Phase 41.0.7 of V5 backlog.
|
|
19
|
+
|
|
20
|
+
set -e
|
|
21
|
+
|
|
22
|
+
EVENT_TYPE="${1:-}"
|
|
23
|
+
META="${2:-}"
|
|
24
|
+
|
|
25
|
+
if [ -z "$EVENT_TYPE" ]; then
|
|
26
|
+
exit 0 # silent — no-op when invoked without args
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
TOKEN_FILE="$HOME/.mover/dashboard/.token"
|
|
30
|
+
if [ ! -f "$TOKEN_FILE" ]; then
|
|
31
|
+
exit 0 # silent — server not running
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Extract token + url with minimal JSON parsing
|
|
35
|
+
TOKEN=$(grep -o '"token"[[:space:]]*:[[:space:]]*"[^"]*"' "$TOKEN_FILE" | head -1 | sed 's/.*: *"//;s/"//')
|
|
36
|
+
URL=$(grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*"' "$TOKEN_FILE" | head -1 | sed 's/.*: *"//;s/"//')
|
|
37
|
+
|
|
38
|
+
if [ -z "$TOKEN" ] || [ -z "$URL" ]; then
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Build body
|
|
43
|
+
if [ -n "$META" ]; then
|
|
44
|
+
BODY=$(printf '{"type":"%s","meta":%s}' "$EVENT_TYPE" "$META")
|
|
45
|
+
else
|
|
46
|
+
BODY=$(printf '{"type":"%s"}' "$EVENT_TYPE")
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Best-effort POST. Suppress all output — workflow logs shouldn't show this.
|
|
50
|
+
# Timeout = 2s so a hung server can't stall a workflow.
|
|
51
|
+
if command -v curl >/dev/null 2>&1; then
|
|
52
|
+
curl -s -m 2 \
|
|
53
|
+
-X POST \
|
|
54
|
+
-H "Content-Type: application/json" \
|
|
55
|
+
-H "X-Mover-Token: $TOKEN" \
|
|
56
|
+
-H "Origin: $URL" \
|
|
57
|
+
-d "$BODY" \
|
|
58
|
+
"$URL/api/event" >/dev/null 2>&1 || true
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
exit 0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// memory-curator.js — Mover memory engine, Layer 5 (curator lifecycle + reflection loop).
|
|
4
|
+
//
|
|
5
|
+
// curate(): ages every indexed session active -> stale (30d) -> archived (90d) and decays its
|
|
6
|
+
// trust so recall naturally favours fresh context. It NEVER deletes — old sessions stay
|
|
7
|
+
// searchable, just lower-weighted (the Curator pattern: forget gracefully, lose nothing).
|
|
8
|
+
//
|
|
9
|
+
// reflect(): drafts the DATA half of /analyse-day from the index (recent arcs + trending
|
|
10
|
+
// entities) for the owner to ENGAGE with. Auto-analysis nobody reads is theatre — the
|
|
11
|
+
// judgment half stays human. Writes nothing.
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const { moverDir, safeReadJson } = require("./paths");
|
|
16
|
+
const { parseSessionLogs } = require("./session-log-parser");
|
|
17
|
+
|
|
18
|
+
const MEM_DIR = process.env.MOVER_MEMORY_DIR || path.join(moverDir(), "memory");
|
|
19
|
+
const SEG_DIR = path.join(MEM_DIR, "segments");
|
|
20
|
+
const MANIFEST = path.join(MEM_DIR, "manifest.json");
|
|
21
|
+
|
|
22
|
+
const ACTIVE_DAYS = 30, ARCHIVE_DAYS = 90;
|
|
23
|
+
const TRUST = { active: 1.0, stale: 0.85, archived: 0.6 };
|
|
24
|
+
|
|
25
|
+
function loadManifest() { return safeReadJson(MANIFEST); }
|
|
26
|
+
function loadSeg(n) { return safeReadJson(path.join(SEG_DIR, n + ".json")); }
|
|
27
|
+
function atomicWrite(file, obj) { const t = file + ".tmp"; fs.writeFileSync(t, JSON.stringify(obj)); fs.renameSync(t, file); }
|
|
28
|
+
|
|
29
|
+
function tier(ts) {
|
|
30
|
+
const t = ts ? Date.parse(ts) : NaN;
|
|
31
|
+
if (!Number.isFinite(t)) return "active";
|
|
32
|
+
const days = (Date.now() - t) / 86400000;
|
|
33
|
+
if (days > ARCHIVE_DAYS) return "archived";
|
|
34
|
+
if (days > ACTIVE_DAYS) return "stale";
|
|
35
|
+
return "active";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Age sessions + write trust weights into the manifest (consumed by memory-index query()).
|
|
39
|
+
function curate(opts = {}) {
|
|
40
|
+
const man = loadManifest();
|
|
41
|
+
if (!man) return { available: false, active: 0, stale: 0, archived: 0, adjusted: 0, written: 0 };
|
|
42
|
+
man.trust = man.trust || {};
|
|
43
|
+
const counts = { active: 0, stale: 0, archived: 0 };
|
|
44
|
+
let adjusted = 0;
|
|
45
|
+
for (const seg of (man.segments || []).filter((s) => s.startsWith("raw-"))) {
|
|
46
|
+
const s = loadSeg(seg);
|
|
47
|
+
if (!s) continue;
|
|
48
|
+
for (const id in s.docs) {
|
|
49
|
+
const tr = tier(s.docs[id].ts);
|
|
50
|
+
counts[tr]++;
|
|
51
|
+
const w = TRUST[tr];
|
|
52
|
+
if (w !== 1.0) {
|
|
53
|
+
if (man.trust[id] !== w) { man.trust[id] = w; adjusted++; }
|
|
54
|
+
} else if (man.trust[id] != null && man.trust[id] !== 1.0) {
|
|
55
|
+
delete man.trust[id]; adjusted++; // a session refreshed back into the active window
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
man.curatedAt = new Date().toISOString();
|
|
60
|
+
atomicWrite(MANIFEST, man);
|
|
61
|
+
return { available: true, ...counts, adjusted, total: counts.active + counts.stale + counts.archived, written: adjusted };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Draft a reflection from the index: recent arcs + entities trending across sessions.
|
|
65
|
+
function reflect(opts = {}) {
|
|
66
|
+
const man = loadManifest();
|
|
67
|
+
if (!man) return { available: false, draft: "", trendingEntities: [], recentArcs: [], written: 0 };
|
|
68
|
+
const N = Object.keys(man.indexedSessions || {}).length || 1;
|
|
69
|
+
|
|
70
|
+
const trendingEntities = Object.keys(man.linkFreq || {})
|
|
71
|
+
.map((note) => { const f = man.linkFreq[note]; const idf = Math.log(N / Math.max(1, f)); return { note, sessions: f, score: +(f * idf).toFixed(2) }; })
|
|
72
|
+
.filter((x) => x.sessions / N <= 0.6) // drop the ubiquitous (Engine files etc.)
|
|
73
|
+
.sort((a, b) => b.score - a.score)
|
|
74
|
+
.slice(0, opts.limit || 10);
|
|
75
|
+
|
|
76
|
+
let recentArcs = [];
|
|
77
|
+
try {
|
|
78
|
+
const vault = opts.vault || man.vault;
|
|
79
|
+
if (vault) recentArcs = parseSessionLogs(vault).docs.slice(0, 8).map((d) => ({ date: d.date, title: d.title }));
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
82
|
+
const lines = ["## Reflection (auto-draft — engage with it, don't just read it)", ""];
|
|
83
|
+
if (recentArcs.length) { lines.push("Recent arcs:"); recentArcs.forEach((a) => lines.push(`- ${a.date} — ${a.title}`)); lines.push(""); }
|
|
84
|
+
if (trendingEntities.length) { lines.push("Entities trending across your sessions:"); trendingEntities.forEach((t) => lines.push(`- [[${t.note}]] (${t.sessions} sessions)`)); lines.push(""); }
|
|
85
|
+
lines.push("What pattern is forming? What's the one decision to make next? (your call — this is the /analyse-day judgment half.)");
|
|
86
|
+
|
|
87
|
+
return { available: true, draft: lines.join("\n"), trendingEntities, recentArcs, written: 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { curate, reflect, MEM_DIR };
|
|
91
|
+
|
|
92
|
+
// ── CLI ──
|
|
93
|
+
if (require.main === module) {
|
|
94
|
+
const vault = require("./paths").resolveVault();
|
|
95
|
+
console.log("curate:", JSON.stringify(curate()));
|
|
96
|
+
console.log("\n" + reflect({ vault }).draft);
|
|
97
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// memory-gardener.js — Mover memory engine, Layer 2 (the differentiator / the moat).
|
|
4
|
+
//
|
|
5
|
+
// Memory's job is to GROW the vault graph; recall is the side effect. The gardener reads the
|
|
6
|
+
// index + re-reads recent sessions and PROPOSES two kinds of growth:
|
|
7
|
+
// 1. link — rare (high-IDF) existing notes worth wiring into the graph (the connections
|
|
8
|
+
// you never got around to drawing). IDF buries ubiquitous Engine files.
|
|
9
|
+
// 2. stub — recurring NEW entities (people/projects/concepts) mentioned across sessions
|
|
10
|
+
// that have NO note yet — candidates for a Library/Entities stub.
|
|
11
|
+
//
|
|
12
|
+
// IT NEVER WRITES TO THE VAULT. Every proposal is owner-gated (written:0, gated:true). The
|
|
13
|
+
// compounding growth — drawn only with your approval — is the data-gravity moat: it can only
|
|
14
|
+
// accrue inside your vault, so a competitor can't ship it.
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const { buildVocab, STOPWORDS } = require("./memory-text");
|
|
19
|
+
const { enumerateSessions, parseTranscript } = require("./transcript-parser");
|
|
20
|
+
const { moverDir, safeReadJson } = require("./paths");
|
|
21
|
+
|
|
22
|
+
const MEM_DIR = process.env.MOVER_MEMORY_DIR || path.join(moverDir(), "memory");
|
|
23
|
+
|
|
24
|
+
// Leading words that signal a non-entity Title-Case phrase ("The Standing", "Single Test").
|
|
25
|
+
const STUB_LEAD_STOP = new Set([
|
|
26
|
+
"the", "this", "that", "these", "those", "single", "active", "mover", "phase",
|
|
27
|
+
"session", "daily", "weekly", "strategy", "engine", "library", "project", "your", "our",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Mover/Claude/system boilerplate that recurs in transcripts (CLAUDE.md headers, /log
|
|
31
|
+
// summary sections, compaction scaffolding) but is NEVER a real entity. Exact lowercased
|
|
32
|
+
// match. Without this the stub heuristic drowns in "Final Output" / "Solutions Ledger" noise.
|
|
33
|
+
const STUB_DENY = new Set([
|
|
34
|
+
"final output", "rules loaded", "risk tier", "global rules", "experiment loop",
|
|
35
|
+
"solutions ledger", "primary request", "key technical concepts", "code sections",
|
|
36
|
+
"problem solving", "session log", "active context", "daily note", "execution log",
|
|
37
|
+
"engine health", "pattern manifest", "single test", "session start", "user prompt",
|
|
38
|
+
"tool use", "session id", "pre flight", "key accomplishments", "still pending",
|
|
39
|
+
"strategic redirect", "files updated", "remaining today", "engine writer",
|
|
40
|
+
"claude code", "mover os", "system state", "agent notes", "next milestone",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// Frequent English words. A Title-Case candidate whose words are ALL common is a generic
|
|
44
|
+
// header ("Deep Research", "Current Work", "Pending Tasks"), not an entity. Real entities
|
|
45
|
+
// (people/orgs/products) carry at least one uncommon/proper word ("Jane Doe", "Acme Corp").
|
|
46
|
+
const COMMON_WORDS = new Set((
|
|
47
|
+
"pending tasks current work optional next step deep research market tool selection first " +
|
|
48
|
+
"search broad survey verify library experiment runner parallel agents orchestrator final " +
|
|
49
|
+
"output key technical concepts code sections problem solving primary request summary context " +
|
|
50
|
+
"state system notes report analysis review plan build fix update change result status error " +
|
|
51
|
+
"test check phase mode risk level gap flag item list set type value data file path line block " +
|
|
52
|
+
"group stage round batch pass run call view page card panel board grid chart label title header " +
|
|
53
|
+
"footer menu button field form input query index score count total rate time date day week month " +
|
|
54
|
+
"year today now recent latest real full main core base top end start open close done new old big " +
|
|
55
|
+
"small high low more less all both each every some many few next last first second third final " +
|
|
56
|
+
"good bad best worst fast slow hard easy clear clean quick deep wide long short live dead working " +
|
|
57
|
+
"session daily weekly monthly project task goal idea note step move thing stuff part piece bit"
|
|
58
|
+
).split(/\s+/).filter(Boolean));
|
|
59
|
+
|
|
60
|
+
function loadManifest() { return safeReadJson(path.join(MEM_DIR, "manifest.json")); }
|
|
61
|
+
function loadSeg(name) { return safeReadJson(path.join(MEM_DIR, "segments", name + ".json")); }
|
|
62
|
+
|
|
63
|
+
// LINK proposals — rare existing notes (high IDF) referenced in sessions, worth connecting.
|
|
64
|
+
// Ubiquitous notes (in > maxFreqRatio of sessions) are dropped: they're noise, not signal.
|
|
65
|
+
function proposeLinks(opts = {}) {
|
|
66
|
+
const man = loadManifest();
|
|
67
|
+
if (!man) return { available: false, proposals: [], written: 0, gated: true };
|
|
68
|
+
const N = Object.keys(man.indexedSessions || {}).length || 1;
|
|
69
|
+
const maxFreqRatio = opts.maxFreqRatio != null ? opts.maxFreqRatio : 0.5;
|
|
70
|
+
const minIdf = opts.minIdf != null ? opts.minIdf : 0.5;
|
|
71
|
+
|
|
72
|
+
// note -> set of sessions that mention it (from the raw segments' stored candidateLinks).
|
|
73
|
+
const noteSessions = Object.create(null);
|
|
74
|
+
for (const seg of (man.segments || []).filter((s) => s.startsWith("raw-"))) {
|
|
75
|
+
const s = loadSeg(seg);
|
|
76
|
+
if (!s) continue;
|
|
77
|
+
for (const id in s.docs) {
|
|
78
|
+
const d = s.docs[id];
|
|
79
|
+
const links = (d.meta && d.meta.candidateLinks) || [];
|
|
80
|
+
const sid = (d.meta && d.meta.sessionId) || id;
|
|
81
|
+
for (const note of new Set(links)) {
|
|
82
|
+
(noteSessions[note] || (noteSessions[note] = new Set())).add(sid);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const proposals = [];
|
|
88
|
+
for (const note in noteSessions) {
|
|
89
|
+
const freq = man.linkFreq[note] != null ? man.linkFreq[note] : noteSessions[note].size;
|
|
90
|
+
if (freq / N > maxFreqRatio) continue; // ubiquitous → skip
|
|
91
|
+
const idf = Math.log(N / Math.max(1, freq));
|
|
92
|
+
if (idf < minIdf) continue;
|
|
93
|
+
proposals.push({
|
|
94
|
+
type: "link", note, idf: +idf.toFixed(2), sessions: [...noteSessions[note]].slice(0, 5),
|
|
95
|
+
suggestion: `Link [[${note}]] — referenced in ${freq} session(s) but likely under-connected in the graph.`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
proposals.sort((a, b) => b.idf - a.idf);
|
|
99
|
+
return { available: true, proposals: proposals.slice(0, opts.limit || 25), written: 0, gated: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// STUB proposals — recurring Title-Case entities that have NO existing note. Re-reads recent
|
|
103
|
+
// sessions (original case) and counts candidates across sessions; proposes those seen in
|
|
104
|
+
// >= minSessions distinct sessions. Heuristic on purpose — the owner gates every creation.
|
|
105
|
+
async function proposeStubs(opts = {}) {
|
|
106
|
+
const man = loadManifest();
|
|
107
|
+
const vault = opts.vault || (man && man.vault);
|
|
108
|
+
if (!vault) return { available: false, proposals: [], written: 0, gated: true };
|
|
109
|
+
|
|
110
|
+
const vocab = buildVocab(vault);
|
|
111
|
+
const known = new Set(vocab.entries.map((e) => e.lower));
|
|
112
|
+
const sessions = enumerateSessions(opts.projectsDir).slice(0, opts.scan || 40);
|
|
113
|
+
|
|
114
|
+
const TITLE = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2})\b/g;
|
|
115
|
+
const sessionCount = Object.create(null); // lower -> # sessions
|
|
116
|
+
const display = Object.create(null); // lower -> first display form
|
|
117
|
+
|
|
118
|
+
for (const s of sessions) {
|
|
119
|
+
const r = await parseTranscript(s.file, {});
|
|
120
|
+
if (r.error) continue;
|
|
121
|
+
const seenHere = new Set();
|
|
122
|
+
let m; TITLE.lastIndex = 0;
|
|
123
|
+
while ((m = TITLE.exec(r.sampleText)) !== null) {
|
|
124
|
+
const phrase = m[1].trim();
|
|
125
|
+
const lower = phrase.toLowerCase();
|
|
126
|
+
if (known.has(lower)) continue; // already a note
|
|
127
|
+
if (STUB_DENY.has(lower)) continue; // system boilerplate
|
|
128
|
+
const w = lower.split(/\s+/);
|
|
129
|
+
if (STUB_LEAD_STOP.has(w[0])) continue; // "The Standing", "Single Test"…
|
|
130
|
+
if (w.every((x) => STOPWORDS.has(x))) continue;
|
|
131
|
+
if (w.every((x) => COMMON_WORDS.has(x) || STOPWORDS.has(x))) continue; // all-common header
|
|
132
|
+
if (seenHere.has(lower)) continue;
|
|
133
|
+
seenHere.add(lower);
|
|
134
|
+
sessionCount[lower] = (sessionCount[lower] || 0) + 1;
|
|
135
|
+
if (!display[lower]) display[lower] = phrase;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const minSessions = opts.minSessions || 2;
|
|
140
|
+
const proposals = Object.keys(sessionCount)
|
|
141
|
+
.filter((k) => sessionCount[k] >= minSessions)
|
|
142
|
+
.map((k) => ({
|
|
143
|
+
type: "stub", name: display[k], sessions: sessionCount[k],
|
|
144
|
+
suggestion: `No note for "${display[k]}" (seen in ${sessionCount[k]} sessions) — create a Library/Entities stub?`,
|
|
145
|
+
}))
|
|
146
|
+
.sort((a, b) => b.sessions - a.sessions);
|
|
147
|
+
|
|
148
|
+
return { available: true, proposals: proposals.slice(0, opts.limit || 25), written: 0, gated: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Pretty one-block render for the owner to approve/reject (display only — no writes).
|
|
152
|
+
function renderProposals(links, stubs) {
|
|
153
|
+
const out = [];
|
|
154
|
+
if (links && links.proposals.length) {
|
|
155
|
+
out.push("LINKS worth drawing (rare, high-signal — IDF-ranked):");
|
|
156
|
+
for (const p of links.proposals.slice(0, 12)) out.push(` • [[${p.note}]] (idf ${p.idf}, ${p.sessions.length} sessions)`);
|
|
157
|
+
}
|
|
158
|
+
if (stubs && stubs.proposals.length) {
|
|
159
|
+
out.push("STUBS for entities with no home:");
|
|
160
|
+
for (const p of stubs.proposals.slice(0, 12)) out.push(` • ${p.name} (${p.sessions} sessions)`);
|
|
161
|
+
}
|
|
162
|
+
if (!out.length) out.push("Nothing to garden — the graph is current.");
|
|
163
|
+
return out.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { proposeLinks, proposeStubs, renderProposals, MEM_DIR };
|
|
167
|
+
|
|
168
|
+
// ── CLI ──
|
|
169
|
+
if (require.main === module) {
|
|
170
|
+
(async () => {
|
|
171
|
+
const vault = require("./paths").resolveVault();
|
|
172
|
+
const links = proposeLinks({ vault });
|
|
173
|
+
const stubs = await proposeStubs({ vault });
|
|
174
|
+
if (process.argv.includes("--json")) console.log(JSON.stringify({ links, stubs }, null, 2));
|
|
175
|
+
else console.log(renderProposals(links, stubs));
|
|
176
|
+
})();
|
|
177
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// memory-gepa.js — Mover memory engine, Layer 6 (GEPA: trace-based self-improvement).
|
|
4
|
+
//
|
|
5
|
+
// The flagship Hermes feature, reachable ONLY because the jsonl is a real trace corpus.
|
|
6
|
+
// Skills/workflows fail leaving evidence in the transcript (tool errors, retries). GEPA
|
|
7
|
+
// clusters those failure traces across sessions and PROPOSES targeted fixes — so the system
|
|
8
|
+
// learns WHY it failed, not just that it did. Every proposal is human-PR gated; GEPA never
|
|
9
|
+
// edits a skill, workflow, or rule on its own (written:0, gated:true).
|
|
10
|
+
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const { moverDir, safeReadJson } = require("./paths");
|
|
13
|
+
|
|
14
|
+
const MEM_DIR = process.env.MOVER_MEMORY_DIR || path.join(moverDir(), "memory");
|
|
15
|
+
const SEG_DIR = path.join(MEM_DIR, "segments");
|
|
16
|
+
|
|
17
|
+
function loadManifest() { return safeReadJson(path.join(MEM_DIR, "manifest.json")); }
|
|
18
|
+
function loadSeg(n) { return safeReadJson(path.join(SEG_DIR, n + ".json")); }
|
|
19
|
+
|
|
20
|
+
// Collapse an error string to a stable signature so the same failure clusters across sessions
|
|
21
|
+
// even when the volatile parts (ids, paths, ports, hex) differ.
|
|
22
|
+
function normalizeError(e) {
|
|
23
|
+
return String(e || "").toLowerCase()
|
|
24
|
+
.replace(/0x[0-9a-f]+/g, "0x#")
|
|
25
|
+
.replace(/\b\d+\b/g, "#")
|
|
26
|
+
.replace(/\/[^\s"']+/g, "/path")
|
|
27
|
+
.replace(/\s+/g, " ").trim().slice(0, 90);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Cluster recurring runtime failures from the index and propose fixes (gated). A failure that
|
|
31
|
+
// recurs in >= minSessions distinct sessions is a pattern worth fixing; a one-off is noise.
|
|
32
|
+
function analyzeFailures(opts = {}) {
|
|
33
|
+
const man = loadManifest();
|
|
34
|
+
if (!man) return { available: false, proposals: [], written: 0, gated: true };
|
|
35
|
+
|
|
36
|
+
const sigSessions = Object.create(null); // signature -> Set(sessionId)
|
|
37
|
+
const sigSample = Object.create(null); // signature -> first raw sample
|
|
38
|
+
const sigCmdSessions = Object.create(null); // signature -> { command -> Set(sessionId) } — C1 attribution
|
|
39
|
+
for (const seg of (man.segments || []).filter((s) => s.startsWith("raw-"))) {
|
|
40
|
+
const s = loadSeg(seg);
|
|
41
|
+
if (!s) continue;
|
|
42
|
+
for (const id in s.docs) {
|
|
43
|
+
const d = s.docs[id];
|
|
44
|
+
const errs = (d.meta && d.meta.errors) || [];
|
|
45
|
+
const sid = (d.meta && d.meta.sessionId) || id;
|
|
46
|
+
// ONE workflow per session-doc (the command active at the session's last error). Counted
|
|
47
|
+
// per (signature, session) below so a session with N copies of the same error votes once.
|
|
48
|
+
const cmd = d.meta && d.meta.lastCommand;
|
|
49
|
+
for (const e of errs) {
|
|
50
|
+
const sig = normalizeError(e);
|
|
51
|
+
if (sig.length < 6) continue;
|
|
52
|
+
(sigSessions[sig] || (sigSessions[sig] = new Set())).add(sid);
|
|
53
|
+
if (!sigSample[sig]) sigSample[sig] = String(e).slice(0, 120);
|
|
54
|
+
if (cmd) {
|
|
55
|
+
const m = sigCmdSessions[sig] || (sigCmdSessions[sig] = Object.create(null));
|
|
56
|
+
(m[cmd] || (m[cmd] = new Set())).add(sid); // Set dedupes the same session voting twice
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Rank the workflows a signature failed under by DISTINCT-session count (most-implicated first),
|
|
63
|
+
// so a proposal can name WHICH workflow to fix — not just the bare error string.
|
|
64
|
+
function workflowsFor(sig) {
|
|
65
|
+
const m = sigCmdSessions[sig];
|
|
66
|
+
if (!m) return [];
|
|
67
|
+
return Object.keys(m)
|
|
68
|
+
.map((command) => ({ command, sessions: m[command].size }))
|
|
69
|
+
.sort((a, b) => b.sessions - a.sessions);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const minSessions = opts.minSessions || 2;
|
|
73
|
+
const proposals = Object.keys(sigSessions)
|
|
74
|
+
.filter((k) => sigSessions[k].size >= minSessions)
|
|
75
|
+
.map((k) => {
|
|
76
|
+
const workflows = workflowsFor(k);
|
|
77
|
+
const top = workflows[0];
|
|
78
|
+
return {
|
|
79
|
+
type: "fix", signature: k, sessions: sigSessions[k].size,
|
|
80
|
+
sessionIds: [...sigSessions[k]].slice(0, 5), sample: sigSample[k],
|
|
81
|
+
workflows, // C1→C2: which workflow(s) were running when it broke — names the thing to fix
|
|
82
|
+
suggestion: `Recurring failure across ${sigSessions[k].size} sessions: "${sigSample[k].replace(/\s+/g, " ").slice(0, 70)}". ` +
|
|
83
|
+
(top ? `Most often while running ${top.command} (×${top.sessions}). ` : "") +
|
|
84
|
+
`Read the traces, root-cause it, and propose a guard/fix (human-PR gated — GEPA does not auto-edit).`,
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
.sort((a, b) => b.sessions - a.sessions);
|
|
88
|
+
|
|
89
|
+
return { available: true, proposals: proposals.slice(0, opts.limit || 25), written: 0, gated: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { analyzeFailures, normalizeError, MEM_DIR };
|
|
93
|
+
|
|
94
|
+
// ── CLI ──
|
|
95
|
+
if (require.main === module) {
|
|
96
|
+
const r = analyzeFailures({ minSessions: Number(process.argv[2]) || 3 });
|
|
97
|
+
console.log(`${r.proposals.length} recurring failure patterns (gated, human-PR review):\n`);
|
|
98
|
+
r.proposals.slice(0, 15).forEach((p) => {
|
|
99
|
+
const wf = p.workflows && p.workflows[0] ? ` ${p.workflows[0].command}` : "";
|
|
100
|
+
console.log(` ×${p.sessions} sessions${wf} ${p.sample.replace(/\s+/g, " ").slice(0, 80)}`);
|
|
101
|
+
});
|
|
102
|
+
}
|