mover-os 4.7.6 → 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 +763 -70
- 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,83 @@
|
|
|
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
|
+
|
|
13
|
+
const HISTORY_PATH = path.join(os.homedir(), ".mover", "drift-history.json");
|
|
14
|
+
const MAX_DAYS = 90;
|
|
15
|
+
|
|
16
|
+
function todayKey() {
|
|
17
|
+
return new Date().toISOString().slice(0, 10);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureDir(filePath) {
|
|
21
|
+
const dir = path.dirname(filePath);
|
|
22
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function load() {
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(HISTORY_PATH)) return { version: 1, history: [] };
|
|
28
|
+
const raw = fs.readFileSync(HISTORY_PATH, "utf8");
|
|
29
|
+
const data = JSON.parse(raw);
|
|
30
|
+
if (!data || !Array.isArray(data.history)) return { version: 1, history: [] };
|
|
31
|
+
return data;
|
|
32
|
+
} catch {
|
|
33
|
+
return { version: 1, history: [] };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function save(data) {
|
|
38
|
+
try {
|
|
39
|
+
ensureDir(HISTORY_PATH);
|
|
40
|
+
// Atomic write (find-bugs #8): write to a tmp file then rename in.
|
|
41
|
+
// POSIX rename is atomic per inode; modern NTFS is effectively atomic
|
|
42
|
+
// on same-filesystem rename. A crash mid-write previously truncated
|
|
43
|
+
// the history file — JSON.parse threw on next load and dropped all
|
|
44
|
+
// drift history silently to {history: []}.
|
|
45
|
+
const tmp = `${HISTORY_PATH}.tmp-${process.pid}-${Date.now()}`;
|
|
46
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
47
|
+
fs.renameSync(tmp, HISTORY_PATH);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Persistence failure is non-fatal — the dashboard still renders
|
|
50
|
+
// with current snapshot.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Record today's value. Overwrites the same-day entry if present so the
|
|
55
|
+
// last computed score wins; appends a new entry for a new day. Trims to
|
|
56
|
+
// MAX_DAYS to keep the file small.
|
|
57
|
+
function recordSnapshot({ value, components }) {
|
|
58
|
+
if (!Number.isFinite(value)) return load();
|
|
59
|
+
const data = load();
|
|
60
|
+
const day = todayKey();
|
|
61
|
+
const idx = data.history.findIndex(e => e.date === day);
|
|
62
|
+
const entry = { date: day, value, components: components || {} };
|
|
63
|
+
if (idx >= 0) {
|
|
64
|
+
data.history[idx] = entry;
|
|
65
|
+
} else {
|
|
66
|
+
data.history.push(entry);
|
|
67
|
+
data.history.sort((a, b) => a.date.localeCompare(b.date));
|
|
68
|
+
if (data.history.length > MAX_DAYS) {
|
|
69
|
+
data.history = data.history.slice(-MAX_DAYS);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
save(data);
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Return the trailing N days as [{date, value}] sorted ASC. Used by the
|
|
77
|
+
// /api/state.json driftScore.history field.
|
|
78
|
+
function getHistory(days = 30) {
|
|
79
|
+
const data = load();
|
|
80
|
+
return data.history.slice(-days).map(e => ({ date: e.date, value: e.value }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
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,170 @@
|
|
|
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
|
+
const re = /(?:\*\*Target Date:?\*\*|^\s*Target Date:)\s*(\d{4}-\d{2}-\d{2})\b/gim;
|
|
157
|
+
let m;
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
while ((m = re.exec(text)) !== null) {
|
|
160
|
+
const dt = Date.parse(`${m[1]}T00:00:00Z`);
|
|
161
|
+
if (!Number.isFinite(dt)) continue;
|
|
162
|
+
if (dt < now) {
|
|
163
|
+
const age = Math.floor((now - dt) / (24 * 60 * 60 * 1000));
|
|
164
|
+
out.push({ targetDate: m[1], expiredDays: age });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { computeEngineHealth, THRESHOLDS };
|