mover-os 4.7.7 → 4.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -24
- package/install.js +2868 -251
- package/package.json +15 -3
- package/src/dashboard/build.js +1541 -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 +189 -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 +483 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/approval-registry.js +170 -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 +228 -0
- package/src/dashboard/lib/date-utils.js +43 -0
- package/src/dashboard/lib/distribution-parser.js +137 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +88 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-default-fingerprints.json +53 -0
- package/src/dashboard/lib/engine-health.js +173 -0
- package/src/dashboard/lib/engine-writer.js +1831 -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 +427 -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 +226 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/library-search.js +290 -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 +519 -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/pid-markers.js +80 -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 +286 -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/state-core/backfill.js +298 -0
- package/src/dashboard/lib/state-core/dryrun.js +188 -0
- package/src/dashboard/lib/state-core/event-log.js +615 -0
- package/src/dashboard/lib/state-core/events.js +265 -0
- package/src/dashboard/lib/state-core/projections.js +376 -0
- package/src/dashboard/lib/state-core/start-close.js +162 -0
- package/src/dashboard/lib/state-core/trial.js +96 -0
- package/src/dashboard/lib/strategy-parser.js +248 -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 +371 -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 +2402 -0
- package/src/dashboard/shortcut.js +284 -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-ByVKPvLf.js +34 -0
- package/src/dashboard/ui/dist/assets/index-CCoKjUcC.js +161 -0
- package/src/dashboard/ui/dist/assets/index-CZWNQDt5.css +1 -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,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// memory-text.js — shared text utilities for the Mover memory engine.
|
|
4
|
+
// Zero-dep (Node stdlib only). Two distinct tokenizers on purpose:
|
|
5
|
+
// - tokenize(): SEARCH terms — stopworded, len>=3. Feeds the BM25 index + queries.
|
|
6
|
+
// - words(): LINK matching — raw lowercased word sequence, NO stopword removal,
|
|
7
|
+
// so multi-word note names ("Launch Plan", "The Standing") survive as n-grams.
|
|
8
|
+
// Mixing the two would drop stopword-containing note names and pollute the index with "the".
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
|
|
13
|
+
// Compact English + Mover-chatter stopword set. Kept inline to stay zero-dep.
|
|
14
|
+
const STOPWORDS = new Set((
|
|
15
|
+
"a an and are as at be been being but by for from had has have he her hers him his i if in into is it its " +
|
|
16
|
+
"me my no nor not of off on once only or our ours out over own she so some such than that the their theirs them " +
|
|
17
|
+
"then there these they this those to too under until up very was we were what when where which while who whom why " +
|
|
18
|
+
"will with you your yours about above after again against all am any because before below between both did do does " +
|
|
19
|
+
"doing down during each few more most other same should through too will just dont cant wont youre ive im also get " +
|
|
20
|
+
"got like one two way thing things stuff lot really actually maybe okay yeah yep nope going want need make made " +
|
|
21
|
+
"now new use used using via per etc ok let lets going gonna wanna"
|
|
22
|
+
).split(/\s+/).filter(Boolean));
|
|
23
|
+
|
|
24
|
+
// Generic note-name tokens that are too ambiguous to be a useful wikilink target.
|
|
25
|
+
const VOCAB_STOP = new Set([
|
|
26
|
+
"index", "readme", "schema", "template", "_template", "misc", "notes", "untitled",
|
|
27
|
+
"home", "moc", "map", "tools", "people", "places", "scripts", "inbox", "archive",
|
|
28
|
+
"daily", "weekly", "monthly", "project", "projects", "library", "engine"
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// SEARCH tokens: lowercase, alphanumeric cores (keep internal . - _ for things like
|
|
32
|
+
// "v4.5.1", "engine-writer"), drop stopwords + sub-3-char noise + over-long blobs.
|
|
33
|
+
function tokenize(text) {
|
|
34
|
+
if (!text) return [];
|
|
35
|
+
const out = [];
|
|
36
|
+
const re = /[a-z0-9][a-z0-9._-]{1,40}/gi;
|
|
37
|
+
let m;
|
|
38
|
+
while ((m = re.exec(text)) !== null) {
|
|
39
|
+
let t = m[0].toLowerCase().replace(/^[._-]+|[._-]+$/g, "");
|
|
40
|
+
if (t.length < 3 || t.length > 40) continue;
|
|
41
|
+
if (STOPWORDS.has(t)) continue;
|
|
42
|
+
out.push(t);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// LINK words: lowercase word sequence, light cleanup, NO stopword removal.
|
|
48
|
+
function words(text) {
|
|
49
|
+
if (!text) return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
const re = /[a-z0-9][a-z0-9'._-]*/gi;
|
|
52
|
+
let m;
|
|
53
|
+
while ((m = re.exec(text)) !== null) {
|
|
54
|
+
const w = m[0].toLowerCase().replace(/^['._-]+|['._-]+$/g, "");
|
|
55
|
+
if (w) out.push(w);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build uni/bi/tri-gram membership sets from a (capped) text blob, for O(vocab) link matching.
|
|
61
|
+
function ngramSets(text) {
|
|
62
|
+
const w = words(text);
|
|
63
|
+
const uni = new Set();
|
|
64
|
+
const bi = new Set();
|
|
65
|
+
const tri = new Set();
|
|
66
|
+
for (let i = 0; i < w.length; i++) {
|
|
67
|
+
uni.add(w[i]);
|
|
68
|
+
if (i + 1 < w.length) bi.add(w[i] + " " + w[i + 1]);
|
|
69
|
+
if (i + 2 < w.length) tri.add(w[i] + " " + w[i + 1] + " " + w[i + 2]);
|
|
70
|
+
}
|
|
71
|
+
return { 1: uni, 2: bi, 3: tri };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// term -> frequency map from search tokens.
|
|
75
|
+
function termFreq(tokens) {
|
|
76
|
+
const tf = Object.create(null);
|
|
77
|
+
for (const t of tokens) tf[t] = (tf[t] || 0) + 1;
|
|
78
|
+
return tf;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Recursively collect .md basenames (no extension) under a dir.
|
|
82
|
+
function listMd(dir, acc, depth) {
|
|
83
|
+
if (depth < 0) return acc;
|
|
84
|
+
let ents;
|
|
85
|
+
try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return acc; }
|
|
86
|
+
for (const e of ents) {
|
|
87
|
+
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
88
|
+
const p = path.join(dir, e.name);
|
|
89
|
+
if (e.isDirectory()) listMd(p, acc, depth - 1);
|
|
90
|
+
else if (e.name.endsWith(".md")) acc.push(e.name.replace(/\.md$/, ""));
|
|
91
|
+
}
|
|
92
|
+
return acc;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Vocab = the set of existing vault notes the gardener/recall can link into.
|
|
96
|
+
// Sourced from 01_Projects folder names + every .md basename under 03_Library + 02_Areas/Engine.
|
|
97
|
+
// Returns { entries:[{canonical,lower,nWords}], byN:{1:Map,2:Map,3:Map} }.
|
|
98
|
+
function buildVocab(vault) {
|
|
99
|
+
const raw = new Set();
|
|
100
|
+
try {
|
|
101
|
+
for (const e of fs.readdirSync(path.join(vault, "01_Projects"), { withFileTypes: true })) {
|
|
102
|
+
if (e.isDirectory()) raw.add(e.name);
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
listMd(path.join(vault, "03_Library"), [], 4).forEach((n) => raw.add(n));
|
|
106
|
+
listMd(path.join(vault, "02_Areas", "Engine"), [], 3).forEach((n) => raw.add(n));
|
|
107
|
+
|
|
108
|
+
const entries = [];
|
|
109
|
+
const byN = { 1: new Map(), 2: new Map(), 3: new Map() };
|
|
110
|
+
for (const name of raw) {
|
|
111
|
+
const w = words(name);
|
|
112
|
+
const nWords = w.length;
|
|
113
|
+
if (nWords < 1 || nWords > 3) continue;
|
|
114
|
+
const lower = w.join(" ");
|
|
115
|
+
// Single-word notes must be >=4 chars and not a generic term, or they match everything.
|
|
116
|
+
if (nWords === 1 && (lower.length < 4 || VOCAB_STOP.has(lower))) continue;
|
|
117
|
+
// Skip date-stamped dailies/weeklies AND numbered/ordinal note names ("7 Workflow",
|
|
118
|
+
// "4. Outbound") — a leading digit is never a useful wikilink target, only index noise.
|
|
119
|
+
if (/^daily|^weekly|^\d/.test(lower)) continue;
|
|
120
|
+
if (byN[nWords].has(lower)) continue;
|
|
121
|
+
byN[nWords].set(lower, name);
|
|
122
|
+
entries.push({ canonical: name, lower, nWords });
|
|
123
|
+
}
|
|
124
|
+
return { entries, byN };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Given precomputed ngramSets for a session and a vocab, return matched canonical note names.
|
|
128
|
+
function matchLinks(sets, vocab) {
|
|
129
|
+
const hits = [];
|
|
130
|
+
for (const e of vocab.entries) {
|
|
131
|
+
if (sets[e.nWords] && sets[e.nWords].has(e.lower)) hits.push(e.canonical);
|
|
132
|
+
}
|
|
133
|
+
return hits;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { tokenize, words, ngramSets, termFreq, buildVocab, matchLinks, STOPWORDS, VOCAB_STOP };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { metricsLogPath, healthAnalysisPath, safeRead, safeStat } = require("./paths");
|
|
4
|
+
|
|
5
|
+
// Parse Metrics_Log.md + Health_Analysis_Full.md for vitality dashboard data.
|
|
6
|
+
|
|
7
|
+
function parseMetricsLog(vault) {
|
|
8
|
+
const mPath = metricsLogPath(vault);
|
|
9
|
+
const hPath = healthAnalysisPath(vault);
|
|
10
|
+
const mText = safeRead(mPath);
|
|
11
|
+
const hText = safeRead(hPath);
|
|
12
|
+
const mStat = safeStat(mPath);
|
|
13
|
+
|
|
14
|
+
const out = {
|
|
15
|
+
available: !!(mText || hText),
|
|
16
|
+
metricsLogPath: mPath,
|
|
17
|
+
healthAnalysisPath: hPath,
|
|
18
|
+
lastUpdated: mStat ? mStat.mtime : null,
|
|
19
|
+
latestMetrics: {},
|
|
20
|
+
recentEntries: []
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (mText) {
|
|
24
|
+
// Extract bold-key fields (most recent ones first)
|
|
25
|
+
const fieldRe = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
|
|
26
|
+
let m;
|
|
27
|
+
while ((m = fieldRe.exec(mText)) !== null) {
|
|
28
|
+
const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
|
|
29
|
+
if (!out.latestMetrics[k]) out.latestMetrics[k] = m[2].trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Extract date-prefixed log entries
|
|
33
|
+
const lines = mText.split("\n");
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const dateMatch = line.match(/^[-*]?\s*\[?(\d{4}-\d{2}-\d{2})\]?\s+(.+)$/);
|
|
36
|
+
if (dateMatch) {
|
|
37
|
+
out.recentEntries.push({ date: dateMatch[1], text: dateMatch[2].trim() });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
out.recentEntries = out.recentEntries.slice(-10).reverse();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Health Analysis: extract HRV, sleep, RHR, weight summary.
|
|
44
|
+
//
|
|
45
|
+
// T242 bug hunt fix (C#7): keep the relaxed key-then-anything-then-value
|
|
46
|
+
// greedy regex (it was finding correct values like sleep=6.73h, hrv=8.9ms
|
|
47
|
+
// in prose), but add a sanity-range filter. Previous bug: `Weight gain
|
|
48
|
+
// (+3-4 kg since baseline)` captured "4" as weight. The lazy `[^|]*?`
|
|
49
|
+
// matched " gain (+3-", then `\d+` grabbed "4", then `\s*kg` matched
|
|
50
|
+
// " kg". The 4 was a delta, not the weight. Range 30-300 rejects it.
|
|
51
|
+
// Same fix kills RHR=0.43 (some Fitbit entropy decimal) — out of 30-200.
|
|
52
|
+
if (hText) {
|
|
53
|
+
out.hrvLatest = extractMetricInRange(hText, /HRV[^|]*?(\d+(?:\.\d+)?)\s*(?:ms|rmssd)/i, 5, 200);
|
|
54
|
+
out.sleepLatest = extractMetricInRange(hText, /Sleep[^|]*?(\d+(?:\.\d+)?)\s*(?:h|hours?)/i, 0, 16);
|
|
55
|
+
out.rhrLatest = extractMetricInRange(hText, /(?:RHR|Resting HR)[^|]*?(\d+(?:\.\d+)?)\s*(?:bpm)?/i, 30, 200);
|
|
56
|
+
out.weightLatest = extractMetricInRange(hText, /Weight[^|]*?(\d+(?:\.\d+)?)\s*(?:kg|lb)/i, 30, 300);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractMetricInRange(text, re, min, max) {
|
|
63
|
+
const m = text.match(re);
|
|
64
|
+
if (!m) return null;
|
|
65
|
+
const v = parseFloat(m[1]);
|
|
66
|
+
if (!Number.isFinite(v)) return null;
|
|
67
|
+
if (v < min || v > max) return null;
|
|
68
|
+
return m[1];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractMetricFromText(text, re) {
|
|
72
|
+
const m = text.match(re);
|
|
73
|
+
return m ? m[1] : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { parseMetricsLog };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Moves usage parser — lights the Moves map (~25 Mover moves) by how often
|
|
4
|
+
// each move has ACTUALLY run, read from Claude Code's real append-only
|
|
5
|
+
// command history at ~/.claude/history.jsonl.
|
|
6
|
+
//
|
|
7
|
+
// Per the dashboard's "TELEMETRY NEVER" promise: this reads a local file
|
|
8
|
+
// Claude Code already writes. Nothing is sent anywhere.
|
|
9
|
+
//
|
|
10
|
+
// Source line shape (one JSON object per line, append-only):
|
|
11
|
+
// { "display": "/log ", "pastedContents": {}, "timestamp": 1771088938619,
|
|
12
|
+
// "project": "<vault>/01_Projects/Mover OS Bundle",
|
|
13
|
+
// "sessionId": "50ba...-2562" }
|
|
14
|
+
//
|
|
15
|
+
// - `display` : the raw prompt text the user submitted. Slash commands
|
|
16
|
+
// and `moveros <sub>` invocations start here.
|
|
17
|
+
// - `timestamp` : epoch ms of the invocation.
|
|
18
|
+
// - `project` : the LITERAL cwd string at invocation time. NOT a slug.
|
|
19
|
+
// On a machine that migrated platforms, this field can carry
|
|
20
|
+
// both old and current absolute paths for the same vault.
|
|
21
|
+
//
|
|
22
|
+
// SCOPING (the crux): we count only invocations made inside THIS vault.
|
|
23
|
+
// - The dashboard server's canonical vault identity is
|
|
24
|
+
// `fs.realpathSync(path.resolve(vault))` (server.js:63). We mirror that
|
|
25
|
+
// so a symlinked vault resolves to the same identity here.
|
|
26
|
+
// - We then keep a history row only if its `project`, normalized to a
|
|
27
|
+
// boundary-aligned path-segment array, CONTAINS the vault realpath's
|
|
28
|
+
// distinctive tail segments as a contiguous run. This is a path-BOUNDARY
|
|
29
|
+
// match, NOT a substring `includes` — so a sibling like
|
|
30
|
+
// `Architect_OS_backup/` is correctly excluded (its segment is
|
|
31
|
+
// `Architect_OS_backup`, which never equals `Architect_OS`).
|
|
32
|
+
// - Old Windows-era rows can't be realpath-resolved on this machine, but
|
|
33
|
+
// they ARE this vault's history. The segment-anchor match recovers them
|
|
34
|
+
// (separators normalized `\` → `/`) without loosening the boundary.
|
|
35
|
+
//
|
|
36
|
+
// FALSE-ZERO DISCIPLINE (T261): history.jsonl absent or unreadable returns
|
|
37
|
+
// { available:false, source:'', totalRuns:null, byCommand:{} }
|
|
38
|
+
// so the UI shows "calibrating", never a fabricated all-zero map. We NEVER
|
|
39
|
+
// invent counts. A move the user simply hasn't run is ABSENT from byCommand
|
|
40
|
+
// — the UI treats absent as a REAL zero ("never").
|
|
41
|
+
|
|
42
|
+
const fs = require("fs");
|
|
43
|
+
const path = require("path");
|
|
44
|
+
const os = require("os");
|
|
45
|
+
|
|
46
|
+
function historyPath() {
|
|
47
|
+
return path.join(os.homedir(), ".claude", "history.jsonl");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Normalize any path string (Windows or POSIX) to a clean, trailing-slash-
|
|
51
|
+
// stripped, forward-slash form, then split into non-empty segments.
|
|
52
|
+
function pathSegments(p) {
|
|
53
|
+
return String(p)
|
|
54
|
+
.replace(/\\/g, "/") // Windows separators → POSIX
|
|
55
|
+
.replace(/\/+$/, "") // drop trailing slash(es)
|
|
56
|
+
.split("/")
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build the vault's distinctive tail anchor: the last up-to-3 segments of the
|
|
61
|
+
// vault realpath (e.g. ["Documents","Obsidian","Architect_OS"]). Three
|
|
62
|
+
// segments is distinctive enough to avoid collisions while still matching the
|
|
63
|
+
// cross-platform paths, which share the `Documents/Obsidian/Architect_OS`
|
|
64
|
+
// tail. Falls back to whatever segments exist for shallow vault paths.
|
|
65
|
+
function vaultAnchorSegments(vault) {
|
|
66
|
+
let vaultReal;
|
|
67
|
+
try {
|
|
68
|
+
vaultReal = fs.realpathSync(path.resolve(vault));
|
|
69
|
+
} catch (_) {
|
|
70
|
+
// Vault path doesn't resolve (missing / not yet created). Fall back to the
|
|
71
|
+
// literal resolved string so scoping still has an anchor to match against.
|
|
72
|
+
vaultReal = path.resolve(vault);
|
|
73
|
+
}
|
|
74
|
+
const segs = pathSegments(vaultReal);
|
|
75
|
+
return segs.slice(Math.max(0, segs.length - 3));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// True if `projectRaw` (a literal cwd from history) sits inside the vault.
|
|
79
|
+
// Boundary-aligned: the anchor must appear as a contiguous run of WHOLE
|
|
80
|
+
// segments. `Architect_OS_backup` never matches an `Architect_OS` anchor
|
|
81
|
+
// segment, so sibling dirs are excluded.
|
|
82
|
+
function projectMatchesVault(projectRaw, anchor) {
|
|
83
|
+
if (!projectRaw || anchor.length === 0) return false;
|
|
84
|
+
const segs = pathSegments(projectRaw);
|
|
85
|
+
for (let i = 0; i + anchor.length <= segs.length; i++) {
|
|
86
|
+
let hit = true;
|
|
87
|
+
for (let j = 0; j < anchor.length; j++) {
|
|
88
|
+
if (segs[i + j] !== anchor[j]) { hit = false; break; }
|
|
89
|
+
}
|
|
90
|
+
if (hit) return true; // everything after the anchor is a descendant → in
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Extract a Mover-move identity from a raw `display` string.
|
|
96
|
+
// Returns { cmd, label } or null.
|
|
97
|
+
// - /slash-command → cmd "log", label "/log"
|
|
98
|
+
// - moveros <subcmd> → cmd "pulse", label "moveros pulse"
|
|
99
|
+
// Per the contract: command names are lowercase, may contain hyphens, and the
|
|
100
|
+
// first letter is a-z (so we don't match `/1`, `/-x`, paths, etc.).
|
|
101
|
+
function extractMove(display) {
|
|
102
|
+
const text = String(display || "");
|
|
103
|
+
const slash = text.match(/^\s*(\/[a-z][a-z-]+)/);
|
|
104
|
+
if (slash) {
|
|
105
|
+
const label = slash[1];
|
|
106
|
+
return { cmd: label.slice(1), label };
|
|
107
|
+
}
|
|
108
|
+
const cli = text.match(/^\s*moveros\s+([a-z-]+)/);
|
|
109
|
+
if (cli) {
|
|
110
|
+
return { cmd: cli[1], label: `moveros ${cli[1]}` };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// computeMovesUsage({ vault }) -> {
|
|
116
|
+
// available: boolean, // false only when source is unreadable
|
|
117
|
+
// source: string, // absolute history.jsonl path, or '' when absent
|
|
118
|
+
// totalRuns: number | null, // total in-scope move invocations; null when unavailable
|
|
119
|
+
// byCommand: { [cmd]: { count, lastIso, lastLabel } } // absent cmd === real zero
|
|
120
|
+
// }
|
|
121
|
+
function computeMovesUsage(opts = {}) {
|
|
122
|
+
const { vault } = opts;
|
|
123
|
+
const src = historyPath();
|
|
124
|
+
|
|
125
|
+
// False-zero gate: no readable history → unavailable, never a fake map.
|
|
126
|
+
let raw;
|
|
127
|
+
try {
|
|
128
|
+
raw = fs.readFileSync(src, "utf8");
|
|
129
|
+
} catch (_) {
|
|
130
|
+
return { available: false, source: "", totalRuns: null, byCommand: {} };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const anchor = vaultAnchorSegments(vault);
|
|
134
|
+
const byCommand = {};
|
|
135
|
+
// Track newest timestamp per command separately so the public shape only
|
|
136
|
+
// carries { count, lastIso, lastLabel } (no internal bookkeeping leaks out).
|
|
137
|
+
const lastTsByCmd = Object.create(null);
|
|
138
|
+
let totalRuns = 0;
|
|
139
|
+
|
|
140
|
+
const lines = raw.split("\n");
|
|
141
|
+
for (let i = 0; i < lines.length; i++) {
|
|
142
|
+
const line = lines[i];
|
|
143
|
+
if (!line) continue;
|
|
144
|
+
let row;
|
|
145
|
+
try {
|
|
146
|
+
row = JSON.parse(line);
|
|
147
|
+
} catch (_) {
|
|
148
|
+
// Forward-compat: a malformed line never aborts the scan. Skip it.
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!projectMatchesVault(row.project, anchor)) continue;
|
|
152
|
+
|
|
153
|
+
const move = extractMove(row.display);
|
|
154
|
+
if (!move) continue;
|
|
155
|
+
|
|
156
|
+
const ts = Number(row.timestamp);
|
|
157
|
+
const hasTs = Number.isFinite(ts);
|
|
158
|
+
const iso = hasTs ? new Date(ts).toISOString() : null;
|
|
159
|
+
|
|
160
|
+
totalRuns += 1;
|
|
161
|
+
let entry = byCommand[move.cmd];
|
|
162
|
+
if (!entry) {
|
|
163
|
+
entry = byCommand[move.cmd] = { count: 0, lastIso: null, lastLabel: null };
|
|
164
|
+
lastTsByCmd[move.cmd] = -Infinity;
|
|
165
|
+
}
|
|
166
|
+
entry.count += 1;
|
|
167
|
+
// Keep the most-recent invocation's iso + label. Rows without a usable
|
|
168
|
+
// timestamp never overwrite a dated "last" (they stay as a fallback only
|
|
169
|
+
// if nothing dated was ever seen).
|
|
170
|
+
if (hasTs && ts > lastTsByCmd[move.cmd]) {
|
|
171
|
+
lastTsByCmd[move.cmd] = ts;
|
|
172
|
+
entry.lastIso = iso;
|
|
173
|
+
entry.lastLabel = move.label;
|
|
174
|
+
} else if (entry.lastLabel == null) {
|
|
175
|
+
// First sighting was undated — record at least the label so the UI has
|
|
176
|
+
// a name to show. lastIso stays null (honest: timestamp unknown).
|
|
177
|
+
entry.lastLabel = move.label;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { available: true, source: src, totalRuns, byCommand };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { computeMovesUsage };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* onboarding-forge.js — turn the REAL list of Engine files being written into an
|
|
5
|
+
* ordered, human "forge" sequence for the onboarding synthesis beat (the moment the
|
|
6
|
+
* system composes your operating record from your answers).
|
|
7
|
+
*
|
|
8
|
+
* HONESTY CONTRACT: every beat after the lead-in corresponds 1:1 to a file the writer
|
|
9
|
+
* actually produces — never an invented step, never padding. Unknown-but-real files are
|
|
10
|
+
* appended (still honest) rather than dropped. Deterministic + zero-dep (mirrors the
|
|
11
|
+
* connect-recommender / skill-recommender pattern: pure logic here, rendered by the jsx).
|
|
12
|
+
*
|
|
13
|
+
* @param {Array<string>} files file paths or basenames the finalize step will write
|
|
14
|
+
* @param {{cap?:number}} [opts]
|
|
15
|
+
* @returns {Array<{ key:string, label:string, file:string|null }>} lead beat first
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// canonical compose order — the record is built identity-first, then the bet, then leverage
|
|
19
|
+
const ORDER = [
|
|
20
|
+
"Identity_Prime", "Strategy", "Goals", "Mover_Dossier",
|
|
21
|
+
"Active_Context", "Auto_Learnings", "Metrics_Log", "Voice_DNA",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// each label is a present-tense act of composition, warm + specific (never "Writing file 3/5")
|
|
25
|
+
const LABELS = {
|
|
26
|
+
Identity_Prime: "Shaping who you are",
|
|
27
|
+
Strategy: "Setting your strategy",
|
|
28
|
+
Goals: "Naming the bet",
|
|
29
|
+
Mover_Dossier: "Taking stock of your leverage",
|
|
30
|
+
Active_Context: "Opening your working memory",
|
|
31
|
+
Auto_Learnings: "Preparing to learn your patterns",
|
|
32
|
+
Metrics_Log: "Laying out your scoreboard",
|
|
33
|
+
Voice_DNA: "Tuning to your voice",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function basename(name) {
|
|
37
|
+
return String(name || "").split(/[\\/]/).pop().replace(/\.md$/i, "").trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function forgeBeats(files, opts) {
|
|
41
|
+
const cap = (opts && Number.isFinite(opts.cap)) ? opts.cap : 6;
|
|
42
|
+
const list = Array.isArray(files) ? files : [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const ordered = [];
|
|
45
|
+
|
|
46
|
+
// 1. canonical order first (Identity → Strategy → Goals → Dossier → …)
|
|
47
|
+
for (const key of ORDER) {
|
|
48
|
+
if (!seen.has(key) && list.some((f) => basename(f) === key)) {
|
|
49
|
+
seen.add(key);
|
|
50
|
+
ordered.push(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 2. any real file we don't have a canonical slot for — appended, never dropped (honest)
|
|
54
|
+
for (const f of list) {
|
|
55
|
+
const k = basename(f);
|
|
56
|
+
if (k && !seen.has(k)) { seen.add(k); ordered.push(k); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const beats = ordered.slice(0, cap).map((k) => ({
|
|
60
|
+
key: k,
|
|
61
|
+
label: LABELS[k] || ("Writing " + k.replace(/_/g, " ")),
|
|
62
|
+
file: k + ".md",
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// lead beat — the real first act: parsing the transcript into a profile. file:null because
|
|
66
|
+
// it isn't a file being written, it's the read that precedes the writes.
|
|
67
|
+
return [{ key: "_read", label: "Reading your answers", file: null }, ...beats];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { forgeBeats, LABELS, ORDER };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { dailiesDir } = require("./paths");
|
|
6
|
+
const { localDateKey } = require("./date-utils");
|
|
7
|
+
|
|
8
|
+
// override-outcome-parser.js — F18 / Codex viral #3. Cross-references each
|
|
9
|
+
// override entry against subsequent Daily Notes to detect:
|
|
10
|
+
// - Did the deferred workflow actually run later? (resolution outcome)
|
|
11
|
+
// - Was the chosen alternative followed through? (justification outcome)
|
|
12
|
+
//
|
|
13
|
+
// Returns: array of { override, resolution: 'kept'|'reverted'|'unknown', daysToResolve, evidenceFiles }
|
|
14
|
+
|
|
15
|
+
function compute({ vault, overrides = [], windowDays = 90 } = {}) {
|
|
16
|
+
if (!vault || !Array.isArray(overrides) || overrides.length === 0) {
|
|
17
|
+
return { available: false, outcomes: [] };
|
|
18
|
+
}
|
|
19
|
+
const dDir = dailiesDir(vault);
|
|
20
|
+
if (!fs.existsSync(dDir)) return { available: false, outcomes: [] };
|
|
21
|
+
|
|
22
|
+
const outcomes = overrides.slice(-20).map((o) => {
|
|
23
|
+
const overrideDate = o.at ? new Date(o.at) : null;
|
|
24
|
+
if (!overrideDate || isNaN(overrideDate.getTime())) {
|
|
25
|
+
return { override: o, resolution: 'unknown', daysToResolve: null, evidenceFiles: [], outcome7d: null, outcome30d: null };
|
|
26
|
+
}
|
|
27
|
+
const deferredWorkflow = (o.over || '').toLowerCase();
|
|
28
|
+
const startTs = overrideDate.getTime();
|
|
29
|
+
const evidenceFiles = [];
|
|
30
|
+
let firstSubsequentRun = null;
|
|
31
|
+
|
|
32
|
+
// Walk daily notes within windowDays after the override
|
|
33
|
+
for (let i = 0; i <= windowDays; i++) {
|
|
34
|
+
const d = new Date(startTs + i * 24 * 3600 * 1000);
|
|
35
|
+
const key = localDateKey(d);
|
|
36
|
+
const monthDir = key.slice(0, 7);
|
|
37
|
+
const fpath = path.join(dDir, monthDir, `Daily - ${key}.md`);
|
|
38
|
+
try {
|
|
39
|
+
const text = fs.readFileSync(fpath, "utf8");
|
|
40
|
+
// Look for the deferred workflow keyword in this daily note
|
|
41
|
+
if (deferredWorkflow && text.toLowerCase().includes(deferredWorkflow)) {
|
|
42
|
+
if (!firstSubsequentRun) firstSubsequentRun = { date: key, daysAfter: i };
|
|
43
|
+
evidenceFiles.push({ date: key, file: `Daily - ${key}.md` });
|
|
44
|
+
if (evidenceFiles.length >= 3) break;
|
|
45
|
+
}
|
|
46
|
+
} catch (_) { /* skip */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolution = firstSubsequentRun ? 'kept' : 'unknown';
|
|
50
|
+
return {
|
|
51
|
+
override: o,
|
|
52
|
+
resolution,
|
|
53
|
+
daysToResolve: firstSubsequentRun ? firstSubsequentRun.daysAfter : null,
|
|
54
|
+
evidenceFiles,
|
|
55
|
+
outcome7d: firstSubsequentRun && firstSubsequentRun.daysAfter <= 7 ? 'kept' : 'pending',
|
|
56
|
+
outcome30d: firstSubsequentRun && firstSubsequentRun.daysAfter <= 30 ? 'kept' : 'pending',
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
available: true,
|
|
62
|
+
outcomes,
|
|
63
|
+
kept: outcomes.filter(x => x.resolution === 'kept').length,
|
|
64
|
+
unresolved: outcomes.filter(x => x.resolution === 'unknown').length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { compute };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Normalize override entries from pattern-manifest.json.
|
|
4
|
+
//
|
|
5
|
+
// T242 bug hunt fix (C#5): manifest sometimes carries `at` as the short
|
|
6
|
+
// "2026-04-20T11:11" form (no seconds). Per ECMAScript spec, that string
|
|
7
|
+
// is non-standard — Safari parses it as NaN. Force full ISO-8601 with
|
|
8
|
+
// seconds + UTC marker so Date.parse() is consistent across V8/Safari/
|
|
9
|
+
// Firefox. If the input is already valid, normalize to a canonical form.
|
|
10
|
+
function toFullIso(raw) {
|
|
11
|
+
if (!raw) return "";
|
|
12
|
+
const s = String(raw).trim();
|
|
13
|
+
// Already a full ISO with seconds + zone? Keep.
|
|
14
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/.test(s)) return s;
|
|
15
|
+
// Short form: pad to seconds and treat as UTC. Date constructor accepts
|
|
16
|
+
// this form universally.
|
|
17
|
+
const m = s.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/);
|
|
18
|
+
if (m) {
|
|
19
|
+
const ss = m[4] || "00";
|
|
20
|
+
return `${m[1]}T${m[2]}:${m[3]}:${ss}.000Z`;
|
|
21
|
+
}
|
|
22
|
+
// Date-only? Anchor to midnight UTC.
|
|
23
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return `${s}T00:00:00.000Z`;
|
|
24
|
+
// Anything else — let Date.parse have a try. If valid, re-emit ISO.
|
|
25
|
+
const t = Date.parse(s);
|
|
26
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeOverrides(manifest) {
|
|
30
|
+
const raw = (manifest && (manifest.overrideHistory || manifest.override_history)) || [];
|
|
31
|
+
if (!Array.isArray(raw)) return [];
|
|
32
|
+
return raw.map((o, i) => ({
|
|
33
|
+
id: o.id || `${o.timestamp || o.date || "x"}-${i}`,
|
|
34
|
+
at: toFullIso(o.timestamp || o.date),
|
|
35
|
+
project: o.project || "",
|
|
36
|
+
level: o.level || "",
|
|
37
|
+
what: o.what || o.action || o.task || "",
|
|
38
|
+
over: o.over || o.plan_item || o.conflicted_with || "",
|
|
39
|
+
reason: o.reason || o.justification || o.user_override || "",
|
|
40
|
+
outcome: o.outcome || "",
|
|
41
|
+
})).filter(o => o.at || o.what || o.reason);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function computeOverrideSummary(overrides, now = new Date()) {
|
|
45
|
+
const entries = Array.isArray(overrides) ? overrides : [];
|
|
46
|
+
const nowMs = now.getTime();
|
|
47
|
+
const day = 24 * 3600 * 1000;
|
|
48
|
+
const countWithin = (days) => entries.filter(o => {
|
|
49
|
+
const t = Date.parse(o.at || "");
|
|
50
|
+
return Number.isFinite(t) && nowMs - t <= days * day;
|
|
51
|
+
}).length;
|
|
52
|
+
const byLevel = entries.reduce((acc, o) => {
|
|
53
|
+
const level = String(o.level || "UNKNOWN").toUpperCase() || "UNKNOWN";
|
|
54
|
+
acc[level] = (acc[level] || 0) + 1;
|
|
55
|
+
return acc;
|
|
56
|
+
}, {});
|
|
57
|
+
const last = entries
|
|
58
|
+
.slice()
|
|
59
|
+
.sort((a, b) => (Date.parse(b.at || "") || 0) - (Date.parse(a.at || "") || 0))
|
|
60
|
+
.slice(0, 30);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
total: entries.length,
|
|
64
|
+
last7d: countWithin(7),
|
|
65
|
+
last14d: countWithin(14),
|
|
66
|
+
last30d: countWithin(30),
|
|
67
|
+
last90d: countWithin(90),
|
|
68
|
+
byLevel,
|
|
69
|
+
recent: last,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { normalizeOverrides, computeOverrideSummary };
|