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.
Files changed (119) hide show
  1. package/README.md +34 -24
  2. package/install.js +2868 -251
  3. package/package.json +15 -3
  4. package/src/dashboard/build.js +1541 -0
  5. package/src/dashboard/dashboard.js +276 -0
  6. package/src/dashboard/index.js +319 -0
  7. package/src/dashboard/lib/activation-log.js +297 -0
  8. package/src/dashboard/lib/active-context-parser.js +189 -0
  9. package/src/dashboard/lib/agent-command.js +93 -0
  10. package/src/dashboard/lib/agent-detect.js +255 -0
  11. package/src/dashboard/lib/agent-detector.js +92 -0
  12. package/src/dashboard/lib/agent-session.js +483 -0
  13. package/src/dashboard/lib/anti-identity-detector.js +116 -0
  14. package/src/dashboard/lib/approval-registry.js +170 -0
  15. package/src/dashboard/lib/auto-learnings-parser.js +183 -0
  16. package/src/dashboard/lib/cli-usage-parser.js +92 -0
  17. package/src/dashboard/lib/config-parser.js +109 -0
  18. package/src/dashboard/lib/connect-recommender.js +131 -0
  19. package/src/dashboard/lib/correlations-parser.js +231 -0
  20. package/src/dashboard/lib/daily-note-resolver.js +228 -0
  21. package/src/dashboard/lib/date-utils.js +43 -0
  22. package/src/dashboard/lib/distribution-parser.js +137 -0
  23. package/src/dashboard/lib/dossier-parser.js +64 -0
  24. package/src/dashboard/lib/drift-history.js +88 -0
  25. package/src/dashboard/lib/drift-score.js +119 -0
  26. package/src/dashboard/lib/engine-default-fingerprints.json +53 -0
  27. package/src/dashboard/lib/engine-health.js +173 -0
  28. package/src/dashboard/lib/engine-writer.js +1831 -0
  29. package/src/dashboard/lib/execution-plan.js +125 -0
  30. package/src/dashboard/lib/experiments-parser.js +429 -0
  31. package/src/dashboard/lib/feed-parser.js +294 -0
  32. package/src/dashboard/lib/forked-future.js +60 -0
  33. package/src/dashboard/lib/goal-forecast.js +427 -0
  34. package/src/dashboard/lib/goals-parser.js +67 -0
  35. package/src/dashboard/lib/health-protocols-parser.js +133 -0
  36. package/src/dashboard/lib/hook-activity.js +48 -0
  37. package/src/dashboard/lib/hook-indexer.js +169 -0
  38. package/src/dashboard/lib/hourly-activity-parser.js +143 -0
  39. package/src/dashboard/lib/identity-parser.js +85 -0
  40. package/src/dashboard/lib/ingestion.js +418 -0
  41. package/src/dashboard/lib/library-indexer-v2.js +226 -0
  42. package/src/dashboard/lib/library-indexer.js +105 -0
  43. package/src/dashboard/lib/library-search.js +290 -0
  44. package/src/dashboard/lib/log-activation.sh +61 -0
  45. package/src/dashboard/lib/memory-curator.js +97 -0
  46. package/src/dashboard/lib/memory-gardener.js +177 -0
  47. package/src/dashboard/lib/memory-gepa.js +102 -0
  48. package/src/dashboard/lib/memory-index.js +519 -0
  49. package/src/dashboard/lib/memory-rerank.js +72 -0
  50. package/src/dashboard/lib/memory-text.js +136 -0
  51. package/src/dashboard/lib/metrics-log-parser.js +76 -0
  52. package/src/dashboard/lib/moves-usage-parser.js +184 -0
  53. package/src/dashboard/lib/onboarding-forge.js +70 -0
  54. package/src/dashboard/lib/override-outcome-parser.js +68 -0
  55. package/src/dashboard/lib/override-summary.js +73 -0
  56. package/src/dashboard/lib/paths.js +192 -0
  57. package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
  58. package/src/dashboard/lib/phantom-strategy.js +129 -0
  59. package/src/dashboard/lib/pid-markers.js +80 -0
  60. package/src/dashboard/lib/project-scanner.js +121 -0
  61. package/src/dashboard/lib/promise-wall.js +88 -0
  62. package/src/dashboard/lib/record-score.js +173 -0
  63. package/src/dashboard/lib/redaction.js +140 -0
  64. package/src/dashboard/lib/refusal-parser.js +44 -0
  65. package/src/dashboard/lib/regenerate-manifest.js +206 -0
  66. package/src/dashboard/lib/rewind-snapshots.js +689 -0
  67. package/src/dashboard/lib/roast-wall-parser.js +200 -0
  68. package/src/dashboard/lib/run-registry.js +286 -0
  69. package/src/dashboard/lib/safe-write.js +63 -0
  70. package/src/dashboard/lib/session-log-parser.js +145 -0
  71. package/src/dashboard/lib/session-time-parser.js +158 -0
  72. package/src/dashboard/lib/skill-index.js +171 -0
  73. package/src/dashboard/lib/skill-indexer.js +118 -0
  74. package/src/dashboard/lib/skill-recommender.js +689 -0
  75. package/src/dashboard/lib/state-core/backfill.js +298 -0
  76. package/src/dashboard/lib/state-core/dryrun.js +188 -0
  77. package/src/dashboard/lib/state-core/event-log.js +615 -0
  78. package/src/dashboard/lib/state-core/events.js +265 -0
  79. package/src/dashboard/lib/state-core/projections.js +376 -0
  80. package/src/dashboard/lib/state-core/start-close.js +162 -0
  81. package/src/dashboard/lib/state-core/trial.js +96 -0
  82. package/src/dashboard/lib/strategy-parser.js +248 -0
  83. package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
  84. package/src/dashboard/lib/streak-parser.js +95 -0
  85. package/src/dashboard/lib/suggested-now.js +254 -0
  86. package/src/dashboard/lib/tool-awareness.js +125 -0
  87. package/src/dashboard/lib/transcript-parser.js +371 -0
  88. package/src/dashboard/lib/vault-graph-parser.js +205 -0
  89. package/src/dashboard/lib/view-generator.js +163 -0
  90. package/src/dashboard/lib/voice-dna-parser.js +57 -0
  91. package/src/dashboard/lib/walkthrough-script.js +140 -0
  92. package/src/dashboard/lib/workflow-graph-parser.js +170 -0
  93. package/src/dashboard/lib/workflow-library-parser.js +146 -0
  94. package/src/dashboard/server.js +2402 -0
  95. package/src/dashboard/shortcut.js +284 -0
  96. package/src/dashboard/static/setup-poc.html +306 -0
  97. package/src/dashboard/static/walkthrough-poc.html +580 -0
  98. package/src/dashboard/styles.css +1201 -0
  99. package/src/dashboard/templates/index.html +278 -0
  100. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
  101. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
  102. package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
  103. package/src/dashboard/ui/dist/assets/hero.png +0 -0
  104. package/src/dashboard/ui/dist/assets/index-ByVKPvLf.js +34 -0
  105. package/src/dashboard/ui/dist/assets/index-CCoKjUcC.js +161 -0
  106. package/src/dashboard/ui/dist/assets/index-CZWNQDt5.css +1 -0
  107. package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  108. package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  109. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  110. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  111. package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  112. package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
  113. package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
  114. package/src/dashboard/ui/dist/icon.svg +4 -0
  115. package/src/dashboard/ui/dist/index.html +18 -0
  116. package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
  117. package/src/dashboard/ui/dist/registerSW.js +1 -0
  118. package/src/dashboard/ui/dist/sw.js +1 -0
  119. package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+
3
+ const { moverDossierPath, safeRead, safeStat } = require("./paths");
4
+
5
+ // Parse Mover_Dossier.md — asset inventory (skills, capital, audience, network).
6
+
7
+ function parseDossier(vault) {
8
+ const filePath = moverDossierPath(vault);
9
+ const text = safeRead(filePath);
10
+ const stat = safeStat(filePath);
11
+ if (!text) return { available: false };
12
+
13
+ const out = {
14
+ available: true,
15
+ filePath,
16
+ lastUpdated: stat ? stat.mtime : null,
17
+ fields: {},
18
+ sections: {}
19
+ };
20
+
21
+ // Bold-key fields
22
+ const re = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
23
+ let m;
24
+ while ((m = re.exec(text)) !== null) {
25
+ const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
26
+ out.fields[k] = m[2].trim();
27
+ }
28
+
29
+ // Section split
30
+ const lines = text.split("\n");
31
+ let cur = null, buf = [];
32
+ for (const line of lines) {
33
+ const h = line.match(/^##+\s+(.+?)\s*$/);
34
+ if (h) {
35
+ if (cur) out.sections[cur.toLowerCase()] = buf.join("\n");
36
+ cur = h[1];
37
+ buf = [];
38
+ } else if (cur) buf.push(line);
39
+ }
40
+ if (cur) out.sections[cur.toLowerCase()] = buf.join("\n");
41
+
42
+ // Common keys
43
+ out.skills = extractList(out.sections, "skills");
44
+ out.capital = extractList(out.sections, "capital");
45
+ out.audience = extractList(out.sections, "audience");
46
+ out.network = extractList(out.sections, "network");
47
+ out.assets = extractList(out.sections, "assets") || extractList(out.sections, "asset inventory");
48
+
49
+ return out;
50
+ }
51
+
52
+ function extractList(sections, needle) {
53
+ for (const [title, body] of Object.entries(sections)) {
54
+ if (title.includes(needle)) {
55
+ return body.split("\n")
56
+ .map(l => l.match(/^\s*[-*\d.]+\s+(.+)$/))
57
+ .filter(Boolean).map(m => m[1].trim())
58
+ .slice(0, 8);
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ module.exports = { parseDossier };
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+
3
+ // Drift history persistence. drift-score.js is a pure compute function;
4
+ // to render a real time-series sparkline the dashboard needs the daily
5
+ // values stored somewhere. We persist one snapshot per calendar day at
6
+ // ~/.mover/drift-history.json. Same-day requests overwrite the entry,
7
+ // so the latest computed score wins; new days append. Capped at 90 days.
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const os = require("os");
12
+ const { localDateKey } = require("./date-utils");
13
+
14
+ const HISTORY_PATH = path.join(os.homedir(), ".mover", "drift-history.json");
15
+ const MAX_DAYS = 90;
16
+
17
+ // LOCAL calendar day, not UTC. The bare toISOString().slice(0,10) bucketed
18
+ // by UTC while every other parser (streak, distribution, session-time) keys
19
+ // by local time via date-utils, so near midnight in any non-UTC zone the
20
+ // drift sparkline's day disagreed with the rest of the dashboard by one day.
21
+ function todayKey() {
22
+ return localDateKey();
23
+ }
24
+
25
+ function ensureDir(filePath) {
26
+ const dir = path.dirname(filePath);
27
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
28
+ }
29
+
30
+ function load() {
31
+ try {
32
+ if (!fs.existsSync(HISTORY_PATH)) return { version: 1, history: [] };
33
+ const raw = fs.readFileSync(HISTORY_PATH, "utf8");
34
+ const data = JSON.parse(raw);
35
+ if (!data || !Array.isArray(data.history)) return { version: 1, history: [] };
36
+ return data;
37
+ } catch {
38
+ return { version: 1, history: [] };
39
+ }
40
+ }
41
+
42
+ function save(data) {
43
+ try {
44
+ ensureDir(HISTORY_PATH);
45
+ // Atomic write (find-bugs #8): write to a tmp file then rename in.
46
+ // POSIX rename is atomic per inode; modern NTFS is effectively atomic
47
+ // on same-filesystem rename. A crash mid-write previously truncated
48
+ // the history file — JSON.parse threw on next load and dropped all
49
+ // drift history silently to {history: []}.
50
+ const tmp = `${HISTORY_PATH}.tmp-${process.pid}-${Date.now()}`;
51
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
52
+ fs.renameSync(tmp, HISTORY_PATH);
53
+ } catch (e) {
54
+ // Persistence failure is non-fatal — the dashboard still renders
55
+ // with current snapshot.
56
+ }
57
+ }
58
+
59
+ // Record today's value. Overwrites the same-day entry if present so the
60
+ // last computed score wins; appends a new entry for a new day. Trims to
61
+ // MAX_DAYS to keep the file small.
62
+ function recordSnapshot({ value, components }) {
63
+ if (!Number.isFinite(value)) return load();
64
+ const data = load();
65
+ const day = todayKey();
66
+ const idx = data.history.findIndex(e => e.date === day);
67
+ const entry = { date: day, value, components: components || {} };
68
+ if (idx >= 0) {
69
+ data.history[idx] = entry;
70
+ } else {
71
+ data.history.push(entry);
72
+ data.history.sort((a, b) => a.date.localeCompare(b.date));
73
+ if (data.history.length > MAX_DAYS) {
74
+ data.history = data.history.slice(-MAX_DAYS);
75
+ }
76
+ }
77
+ save(data);
78
+ return data;
79
+ }
80
+
81
+ // Return the trailing N days as [{date, value}] sorted ASC. Used by the
82
+ // /api/state.json driftScore.history field.
83
+ function getHistory(days = 30) {
84
+ const data = load();
85
+ return data.history.slice(-days).map(e => ({ date: e.date, value: e.value }));
86
+ }
87
+
88
+ module.exports = { recordSnapshot, getHistory, HISTORY_PATH };
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+
3
+ // drift-score.js — composite score 0-100 representing distance from stated strategy.
4
+ // Reads from pattern-manifest + active-context + engine-health + daily-note-resolver.
5
+ // Pure function: takes already-parsed inputs, returns DriftScore shape.
6
+ //
7
+ // Score components (each 0-25, sum = 0-100):
8
+ // 1. Zombie load — manifest.zombie_count (clamped 0-5, weighted x5)
9
+ // 2. Override burden — count of override_history entries last 7 days (x6, clamp 25)
10
+ // 3. Distribution gap — days since last `/log` distribution-tagged entry (x2.5, clamp 25)
11
+ // 4. Stale review age — analyse_day_overdue (x1.5, clamp 25)
12
+ //
13
+ // Band thresholds: 0-29 low, 30-49 med, 50-74 high, 75-100 crit.
14
+ // Delta computed against the value 24h ago if available.
15
+
16
+ function compute(inputs) {
17
+ const {
18
+ manifest = null,
19
+ activeContext = null,
20
+ engineHealth = null,
21
+ todayPlanRanDistribution = false,
22
+ yesterdayScore = null,
23
+ } = inputs || {};
24
+
25
+ // 1) Zombie load — accept both camelCase (loader normalised) and snake_case (raw manifest)
26
+ const zombieCount = clampInt(
27
+ manifest && (manifest.zombieCount ?? manifest.zombie_count),
28
+ 0, 5
29
+ );
30
+ const zombieScore = zombieCount * 5;
31
+
32
+ // 2) Override burden (7-day window)
33
+ const overrideEntries = (manifest && (manifest.overrideHistory || manifest.override_history)) || [];
34
+ const now = Date.now();
35
+ const weekMs = 7 * 24 * 3600 * 1000;
36
+ const recentOverrides = overrideEntries.filter((o) => {
37
+ const t = Date.parse(o.date || o.timestamp || "");
38
+ return Number.isFinite(t) && now - t <= weekMs;
39
+ }).length;
40
+ const overrideScore = Math.min(25, recentOverrides * 6);
41
+
42
+ // 3) Distribution gap — days since the last logged distribution action.
43
+ // Derived from REAL per-day counts (distribution parser) — never a fabricated
44
+ // default. When we genuinely can't measure (parser unavailable AND the manifest
45
+ // has no last_distribution_at), the gap is UNKNOWN and contributes 0, rather
46
+ // than inventing a penalty (the old `distGapDays = 3`). [false-zero, T261]
47
+ const dist = inputs.distribution;
48
+ let distGapDays = null; // null = unmeasured
49
+ if (dist && dist.available !== false && Array.isArray(dist.days) && dist.days.length) {
50
+ // days[] is chronological (oldest → newest). Gap = days since the most
51
+ // recent day with count > 0; full window if there has never been one.
52
+ let gap = dist.days.length;
53
+ for (let i = dist.days.length - 1; i >= 0; i--) {
54
+ if (dist.days[i].count > 0) { gap = (dist.days.length - 1) - i; break; }
55
+ }
56
+ distGapDays = gap;
57
+ } else {
58
+ const lastDistDate = manifest && manifest.last_distribution_at;
59
+ if (lastDistDate) {
60
+ const t = Date.parse(lastDistDate);
61
+ if (Number.isFinite(t)) distGapDays = Math.max(0, Math.floor((now - t) / (24 * 3600 * 1000)));
62
+ }
63
+ }
64
+ const distGapKnown = distGapDays != null;
65
+ const distScore = distGapKnown ? Math.min(25, distGapDays * 2.5) : 0;
66
+
67
+ // 4) Stale review age — unknown (null overdueDays from an unavailable health
68
+ // scan) contributes 0, not a fabricated "no penalty". [Codex P1, T261]
69
+ const analyseOverdue = engineHealth && engineHealth.analyseDay && engineHealth.analyseDay.overdueDays != null
70
+ ? engineHealth.analyseDay.overdueDays
71
+ : null;
72
+ const reviewKnown = analyseOverdue != null;
73
+ const reviewScore = reviewKnown ? Math.min(25, analyseOverdue * 1.5) : 0;
74
+
75
+ // 5) Contradictions — phantom-strategy rows where stated != demonstrated.
76
+ // Codex v3-fidelity report flagged this as the 5th category. Weighted
77
+ // x5 with clamp 25, so 5+ contradictions = full red on this axis.
78
+ const phantomCount = clampInt(
79
+ inputs.phantomCount,
80
+ 0, 5
81
+ );
82
+ const contradictionScore = phantomCount * 5;
83
+
84
+ // Clamp to 0–100: five 25-pt axes can sum to 125. [Codex IMPORTANT, T261]
85
+ const value = Math.min(100, Math.round(zombieScore + overrideScore + distScore + reviewScore + contradictionScore));
86
+
87
+ const band =
88
+ value >= 75 ? "crit" :
89
+ value >= 50 ? "high" :
90
+ value >= 30 ? "med" : "low";
91
+
92
+ let delta = 0;
93
+ if (Number.isFinite(yesterdayScore)) {
94
+ delta = value - yesterdayScore;
95
+ }
96
+
97
+ return {
98
+ value,
99
+ delta,
100
+ band,
101
+ components: {
102
+ zombies: zombieScore,
103
+ overrides: overrideScore,
104
+ distributionGap: Math.round(distScore),
105
+ distributionGapKnown: distGapKnown,
106
+ staleReviewAge: Math.round(reviewScore),
107
+ staleReviewKnown: reviewKnown,
108
+ contradictions: contradictionScore,
109
+ },
110
+ computedAt: new Date().toISOString(),
111
+ };
112
+ }
113
+
114
+ function clampInt(n, lo, hi) {
115
+ const v = Number.isFinite(n) ? Math.floor(n) : 0;
116
+ return Math.min(hi, Math.max(lo, v));
117
+ }
118
+
119
+ module.exports = { compute };
@@ -0,0 +1,53 @@
1
+ {
2
+ "_comment": "Known-good hashes of shipped Engine DEFAULTS across versions (CRLF-normalized+trimmed, sha256). engine-writer allows overwriting a non-sentinel file ONLY if its hash matches one of these (a fresh/older install default); a hash NOT here means the user modified it = real data, refuse. Regenerate: node scripts/gen-engine-fingerprints.mjs.",
3
+ "generated": "git-history",
4
+ "files": {
5
+ "Goals.md": [
6
+ "27abf1d26d51525baa5a3ae9c8cc6f879a723f929334852b7ef9549a5fa11c57",
7
+ "30f3e24ad7e45bbb2053a0863164357b3e6cf718146540e685462495e38c996c"
8
+ ],
9
+ "Mover_Dossier.md": [
10
+ "c0d6817e390153f834d6a52e8d0e686cb3eb056e6457afecafd5e3abc0c47c49",
11
+ "21b1ecb65d7c7dbe7be03efa965fefd54f727663e3f0088be66dd453fd244677",
12
+ "72ca850dfd15972045918fcf0170315da72290a7ca7d034710362ca77e7cd1fa",
13
+ "e10ace7b0a72d17f342c70d03f015525e7029b13e43eae2c43e8c6ecd14483ee",
14
+ "bc568ebd12e25377274cf906d0569218c46d1c06e5dc9e857b98e3fdc128b667"
15
+ ],
16
+ "Active_Context.md": [
17
+ "433ef66c852de9a5c37bb65cc40c9d20e577fbcb46ecf91963c8f2d7b23f8a70",
18
+ "5dec535b04d4c7b11029222812e782a02dcbaea03faaa8979e0adb45bf9698b9",
19
+ "22b4855fc5f8337b2d1d2f077d12cf709333ea2cc3aa96c7fd1e380ea55ab7fc",
20
+ "de5b8133f2fd35ad70920bc1c08f2ab75c26edcf58f6b88fccf245a0e729c5aa",
21
+ "fc6f35e45f7b3934d0ab705b6e259ce195c637019e49381885dab7c963f3ccca",
22
+ "8eec472e3f451a1d8a121b3a3bd76065137921aa95824666fbb466fec6fb6443",
23
+ "f0bf0c38418e6c42be46b1546acd676e55aeb64cd1ba31c164dc7047b5ee6807",
24
+ "25f525f55fb86c638cd67792ef05f46de688e048586a3ccb2d980b8d1a78c7db",
25
+ "63e7db7db109bdfa78edf5df5d69e0286d614e84aa0165ae372576f6a816602c",
26
+ "52a8d57d45cafd662e3d106c2acb6fc07116572ca82bc04ff96f7e913ce47395",
27
+ "d79582e10cb0410554b9a1a669127cc42150805a1207f867edf90ce60f9effa2",
28
+ "829a755f53600266f60a8daaa9450e8e3516a7b03538eabeed9ff94ef5ec2b1b",
29
+ "aaa0e5bdac55f53db3bd36db526f841c5e6f62fb7cd8abf7626ff95b68818e14",
30
+ "4e2b064fe6d2391704a490020be147c507809a1c420da67eac34add5c85b3ebf",
31
+ "ae47ece41ab9c4dfa12df6e0c9b895de2e44ad1b788bcbc84728f43764a0a90a",
32
+ "52332d01a067494dc2cd2a5a642d741aa16cf847e49b5e73b744586e167a7faa"
33
+ ],
34
+ "Auto_Learnings.md": [
35
+ "2f02d8724b3283101be1dce7b14e69f478e841d36453ee990ede636cb0daaced",
36
+ "54fe4b57dd90bed94ef7c5d6af50ef290347afef55081d5c3ec51b4bc3181b60",
37
+ "a04fe481a5e79eaf0888c0fd71100fbbc709c1d2c8a1145b7d6fa6482d32a144",
38
+ "0d5bb1ed881384f1559b9fd2702199d28f7e151bd5ec9f9a559b9ece038e88e5",
39
+ "e1f5361fbd42cbfb655dbe4fd3a8129ec27e5d31c5bbc397615d0eb61c507438",
40
+ "10d443002d1056d6a939dea23231acaa0a5f01f1b3c6a86b808bb383efd34621",
41
+ "2ee52d9282a830ef34c52de5d38e78f19710769b1433391dc787fdfed8cf6093"
42
+ ],
43
+ "Metrics_Log.md": [
44
+ "3e3ab4011b65ddf8b435898ee31d136d31fa346cf408a741b6752f6bd9178537",
45
+ "4f3e7ba572b46cb7cf594640737badb5c2a742e8e2942a53d554ac0e4cddcb69",
46
+ "a840982311c4ad8300813316926b50a6bc6050b1c45a058ad146b6b695299dbf"
47
+ ],
48
+ "Someday_Maybe.md": [
49
+ "d9d7889a0923059c182d197aa3579a6d4ee78e19d980a7b144652bae7d7715a5",
50
+ "115ff4539c651ea87184b124a40ea75c874b5122c26d1ecfce3b5657c3d618da"
51
+ ]
52
+ }
53
+ }
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+
3
+ const {
4
+ activeContextPath,
5
+ strategyPath,
6
+ goalsPath,
7
+ moverDossierPath,
8
+ autoLearningsPath,
9
+ metricsLogPath,
10
+ voiceDnaPath,
11
+ identityPrimePath,
12
+ safeStat,
13
+ safeRead
14
+ } = require("./paths");
15
+
16
+ // Compute file staleness traffic lights using thresholds documented in src/hooks/engine-health.sh.
17
+ // Returns array of {file, status: OK|YELLOW|RED, ageDays, label, threshold}.
18
+ // Status uses 'OK' (matches UI types.ts EngineHealthStatus). The shell-side
19
+ // engine-health.sh emits 'GREEN' for terminal output; the JS parser is the
20
+ // source of truth for state.json.
21
+
22
+ const THRESHOLDS = {
23
+ Active_Context: { yellow: 3, red: 7 },
24
+ Strategy: { yellow: 30, red: 60 },
25
+ Mover_Dossier: { yellow: 60, red: 90 },
26
+ Auto_Learnings: { yellow: 30, red: 60 },
27
+ Metrics_Log: { yellow: 14, red: 30 },
28
+ Voice_DNA: { yellow: 60, red: 90 },
29
+ Identity_Prime: { yellow: 90, red: 180 },
30
+ Goals: { yellow: 30, red: 60 }
31
+ };
32
+
33
+ function ageDays(stat) {
34
+ if (!stat) return null;
35
+ const ms = Date.now() - stat.mtime.getTime();
36
+ return Math.floor(ms / (24 * 60 * 60 * 1000));
37
+ }
38
+
39
+ function statusFor(age, thresholds) {
40
+ if (age === null) return { status: "UNKNOWN", label: "missing" };
41
+ if (age >= thresholds.red) return { status: "RED", label: `${age}d` };
42
+ if (age >= thresholds.yellow) return { status: "YELLOW", label: `${age}d` };
43
+ return { status: "OK", label: `${age}d` };
44
+ }
45
+
46
+ function computeEngineHealth(vault) {
47
+ const targets = [
48
+ { key: "Active_Context", path: activeContextPath(vault) },
49
+ { key: "Strategy", path: strategyPath(vault) },
50
+ { key: "Identity_Prime", path: identityPrimePath(vault) },
51
+ { key: "Goals", path: goalsPath(vault) },
52
+ { key: "Mover_Dossier", path: moverDossierPath(vault) },
53
+ { key: "Auto_Learnings", path: autoLearningsPath(vault) },
54
+ { key: "Metrics_Log", path: metricsLogPath(vault) },
55
+ { key: "Voice_DNA", path: voiceDnaPath(vault) }
56
+ ];
57
+
58
+ const items = targets.map(t => {
59
+ const stat = safeStat(t.path);
60
+ const age = ageDays(stat);
61
+ const thresholds = THRESHOLDS[t.key] || { yellow: 30, red: 90 };
62
+ const { status, label } = statusFor(age, thresholds);
63
+ return {
64
+ file: t.key,
65
+ path: t.path,
66
+ exists: !!stat,
67
+ ageDays: age,
68
+ status,
69
+ label,
70
+ thresholdYellow: thresholds.yellow,
71
+ thresholdRed: thresholds.red,
72
+ mtime: stat ? stat.mtime : null
73
+ };
74
+ });
75
+
76
+ // Workflow-state staleness from Active_Context
77
+ const acText = safeRead(activeContextPath(vault), "");
78
+ const wfStaleness = parseWorkflowStateStaleness(acText);
79
+
80
+ // Goals expiration check — look for date fields in Goals.md
81
+ const goalsText = safeRead(goalsPath(vault), "");
82
+ const expiredGoals = scanForExpiredGoals(goalsText);
83
+
84
+ // Summary counts — include workflow staleness in the totals so the Briefing's
85
+ // Engine Health bar matches Active_Context's stated status. (Codex audit:
86
+ // "summary red count ignores workflow staleness" — fixed 2026-05-13.)
87
+ const wfRed = wfStaleness.filter(w => w.status === "RED").length;
88
+ const wfYellow = wfStaleness.filter(w => w.status === "YELLOW").length;
89
+ const summary = {
90
+ red: items.filter(i => i.status === "RED").length + (expiredGoals.length > 0 ? 1 : 0) + wfRed,
91
+ yellow: items.filter(i => i.status === "YELLOW").length + wfYellow,
92
+ green: items.filter(i => i.status === "OK").length,
93
+ unknown: items.filter(i => i.status === "UNKNOWN").length
94
+ };
95
+
96
+ return {
97
+ available: true,
98
+ scannedAt: new Date(),
99
+ items,
100
+ workflowStaleness: wfStaleness,
101
+ expiredGoals,
102
+ summary,
103
+ overallStatus: summary.red > 0 ? "RED" : summary.yellow > 0 ? "YELLOW" : "OK"
104
+ };
105
+ }
106
+
107
+ function parseWorkflowStateStaleness(text) {
108
+ const out = [];
109
+ const checks = [
110
+ { key: "log_last_run", label: "/log", yellow: 2, red: 5 },
111
+ { key: "analyse_day_last_run", label: "/analyse-day", yellow: 5, red: 10 },
112
+ { key: "weekly_review_last_run", label: "/review-week", yellow: 14, red: 21 },
113
+ { key: "monthly_review_last_run", label: "/review-month", yellow: 30, red: 60 },
114
+ { key: "pivot_strategy_last_run", label: "/pivot-strategy", yellow: 30, red: 60 }
115
+ ];
116
+ const now = Date.now();
117
+ // Prefer the canonical preface field — Active_Context.md uses **key:** YYYY-MM-DD…
118
+ // in the file preface, with optional plain `key: YYYY-MM-DD` lower in the doc.
119
+ // The old regex matched the FIRST plain occurrence, which is often a historical
120
+ // [STALE]/[DRIFT] log line months out of date. Try bold form first, then plain.
121
+ const firstHeader = text.search(/^## /m);
122
+ const preface = firstHeader > 0 ? text.slice(0, firstHeader) : text;
123
+ for (const c of checks) {
124
+ const boldPreface = preface.match(new RegExp(`\\*\\*${c.key}:?\\*\\*\\s*([\\d\\-T:]+)`, "i"));
125
+ const plainPreface = preface.match(new RegExp(`(?:^|\\n)${c.key}:\\s*([\\d\\-T:]+)`, "i"));
126
+ const docBold = text.match(new RegExp(`\\*\\*${c.key}:?\\*\\*\\s*([\\d\\-T:]+)`, "i"));
127
+ const docPlain = text.match(new RegExp(`(?:^|\\n)${c.key}:\\s*([\\d\\-T:]+)`, "i"));
128
+ const m = boldPreface || plainPreface || docBold || docPlain;
129
+ if (!m) {
130
+ out.push({ ...c, status: "UNKNOWN", ageDays: null });
131
+ continue;
132
+ }
133
+ const dt = new Date(m[1]);
134
+ if (isNaN(dt.getTime())) {
135
+ out.push({ ...c, status: "UNKNOWN", ageDays: null });
136
+ continue;
137
+ }
138
+ const age = Math.floor((now - dt.getTime()) / (24 * 60 * 60 * 1000));
139
+ let status = "OK";
140
+ if (age >= c.red) status = "RED";
141
+ else if (age >= c.yellow) status = "YELLOW";
142
+ out.push({ ...c, status, ageDays: age, lastRun: dt });
143
+ }
144
+ return out;
145
+ }
146
+
147
+ function scanForExpiredGoals(text) {
148
+ if (!text) return [];
149
+ const out = [];
150
+ // T242 bug hunt fix (B#17): old regex `|Date:` alternative matched ANY
151
+ // line containing `Date:` (Hire Date, Updated Date, etc) anywhere in
152
+ // Goals.md, including quoted person entries — false-RED expired goals.
153
+ // Drop the bare `Date:` arm. Require explicit "Target Date" semantics.
154
+ // Also: require ISO YYYY-MM-DD shape (was `[\d\-]{8,10}` which accepts
155
+ // `2-3-2026`, `0-0-0`, etc).
156
+ // Both bold conventions accepted: `**Target Date:** X` AND `**Target
157
+ // Date**: X` — the colon-outside-bold style made a 6-month-overdue goal
158
+ // silently invisible to the RED alert (R5-A false-negative twin of B#17).
159
+ const re = /(?:\*\*Target Date:?\*\*:?|^\s*Target Date:)\s*(\d{4}-\d{2}-\d{2})\b/gim;
160
+ let m;
161
+ const now = Date.now();
162
+ while ((m = re.exec(text)) !== null) {
163
+ const dt = Date.parse(`${m[1]}T00:00:00Z`);
164
+ if (!Number.isFinite(dt)) continue;
165
+ if (dt < now) {
166
+ const age = Math.floor((now - dt) / (24 * 60 * 60 * 1000));
167
+ out.push({ targetDate: m[1], expiredDays: age });
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+
173
+ module.exports = { computeEngineHealth, THRESHOLDS };