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,1503 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const {
|
|
7
|
+
dashboardOutDir,
|
|
8
|
+
dashboardIndexPath,
|
|
9
|
+
dashboardBuildLogPath
|
|
10
|
+
} = require("./lib/paths");
|
|
11
|
+
|
|
12
|
+
const { parseActiveContext } = require("./lib/active-context-parser");
|
|
13
|
+
const { parseAutoLearnings } = require("./lib/auto-learnings-parser");
|
|
14
|
+
const { loadPatternManifest } = require("./lib/pattern-manifest-loader");
|
|
15
|
+
const { readStats: readActivationStats } = require("./lib/activation-log");
|
|
16
|
+
const { scanProjects } = require("./lib/project-scanner");
|
|
17
|
+
const { computeEngineHealth } = require("./lib/engine-health");
|
|
18
|
+
const { resolveDailyNotes } = require("./lib/daily-note-resolver");
|
|
19
|
+
const { parseIdentity } = require("./lib/identity-parser");
|
|
20
|
+
const { parseStrategy } = require("./lib/strategy-parser");
|
|
21
|
+
const { parseGoals } = require("./lib/goals-parser");
|
|
22
|
+
const { computeSuggestions } = require("./lib/suggested-now");
|
|
23
|
+
const { indexLibrary } = require("./lib/library-indexer");
|
|
24
|
+
const { parseVoiceDna } = require("./lib/voice-dna-parser");
|
|
25
|
+
const { parseMetricsLog } = require("./lib/metrics-log-parser");
|
|
26
|
+
const { parseHealthProtocols } = require("./lib/health-protocols-parser");
|
|
27
|
+
const { detectAgents } = require("./lib/agent-detector");
|
|
28
|
+
const { readHookActivity } = require("./lib/hook-activity");
|
|
29
|
+
const { parseDossier } = require("./lib/dossier-parser");
|
|
30
|
+
const driftScore = require("./lib/drift-score");
|
|
31
|
+
const driftHistory = require("./lib/drift-history");
|
|
32
|
+
const phantomStrategy = require("./lib/phantom-strategy");
|
|
33
|
+
const promiseWall = require("./lib/promise-wall");
|
|
34
|
+
const forkedFuture = require("./lib/forked-future");
|
|
35
|
+
const workflowLibrary = require("./lib/workflow-library-parser");
|
|
36
|
+
const hourlyActivity = require("./lib/hourly-activity-parser");
|
|
37
|
+
const sessionTime = require("./lib/session-time-parser");
|
|
38
|
+
// Wave 2 — wiring previously-built parsers into StateJson.
|
|
39
|
+
const strategyParser = require("./lib/strategy-parser");
|
|
40
|
+
const identityParser = require("./lib/identity-parser");
|
|
41
|
+
const goalsParser = require("./lib/goals-parser");
|
|
42
|
+
const projectScanner = require("./lib/project-scanner");
|
|
43
|
+
const metricsLog = require("./lib/metrics-log-parser");
|
|
44
|
+
const libraryIndexer = require("./lib/library-indexer");
|
|
45
|
+
// Wave 5 — newest parsers
|
|
46
|
+
const distributionParser = require("./lib/distribution-parser");
|
|
47
|
+
const streakParser = require("./lib/streak-parser");
|
|
48
|
+
const overrideOutcomeParser = require("./lib/override-outcome-parser");
|
|
49
|
+
const refusalParser = require("./lib/refusal-parser");
|
|
50
|
+
const { normalizeOverrides, computeOverrideSummary } = require("./lib/override-summary");
|
|
51
|
+
const { computeGoalForecast, parseDomainProgress } = require("./lib/goal-forecast");
|
|
52
|
+
const { computeExecutionPlan } = require("./lib/execution-plan");
|
|
53
|
+
const antiIdentityDetector = require("./lib/anti-identity-detector");
|
|
54
|
+
const vaultGraphParser = require("./lib/vault-graph-parser");
|
|
55
|
+
|
|
56
|
+
// v2 dashboard indexers — net additions for the holistic rebuild.
|
|
57
|
+
// Surfaces Skills (60+ AI skill packs), Hooks (~23 scripts), CLI usage,
|
|
58
|
+
// and a richer Library index with file paths + frontmatter + previews.
|
|
59
|
+
const { indexSkills } = require("./lib/skill-indexer");
|
|
60
|
+
const { indexHooks } = require("./lib/hook-indexer");
|
|
61
|
+
const { parseCliUsage } = require("./lib/cli-usage-parser");
|
|
62
|
+
// v6 W1 parsers (Wave 1 foundation)
|
|
63
|
+
const movesUsageParser = require("./lib/moves-usage-parser");
|
|
64
|
+
const feedParser = require("./lib/feed-parser");
|
|
65
|
+
const rewindSnapshots = require("./lib/rewind-snapshots");
|
|
66
|
+
const recordScoreLib = require("./lib/record-score");
|
|
67
|
+
const { indexLibraryV2 } = require("./lib/library-indexer-v2");
|
|
68
|
+
|
|
69
|
+
// Execution Protocol parser — pulls "Daily floor" / "Daily minimum output"
|
|
70
|
+
// items per domain from Strategy.md (Work / Vitality / Faith). This is the
|
|
71
|
+
// SOURCE OF TRUTH for the Briefing route's Execution Protocol checkboxes.
|
|
72
|
+
const { parseStrategyProtocol } = require("./lib/strategy-protocol-parser");
|
|
73
|
+
|
|
74
|
+
// Lab parser — walks 02_Areas/Engine/Experiments/*.md and emits the
|
|
75
|
+
// hypothesis register: active experiments, KPI bars, sample progress,
|
|
76
|
+
// friction chips. Drives the /lab route.
|
|
77
|
+
const { buildExperiments } = require("./lib/experiments-parser");
|
|
78
|
+
|
|
79
|
+
function ensureDir(p) { fs.mkdirSync(p, { recursive: true }); }
|
|
80
|
+
|
|
81
|
+
function readTemplate(filename) {
|
|
82
|
+
const tplPath = path.join(__dirname, "templates", filename);
|
|
83
|
+
return fs.readFileSync(tplPath, "utf8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function copyStatic(outDir) {
|
|
87
|
+
const cssSrc = path.join(__dirname, "styles.css");
|
|
88
|
+
const jsSrc = path.join(__dirname, "dashboard.js");
|
|
89
|
+
if (fs.existsSync(cssSrc)) fs.copyFileSync(cssSrc, path.join(outDir, "styles.css"));
|
|
90
|
+
if (fs.existsSync(jsSrc)) fs.copyFileSync(jsSrc, path.join(outDir, "dashboard.js"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function escape(s) {
|
|
94
|
+
if (s === null || s === undefined || s === "") return "—";
|
|
95
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">","\"":""","'":"'" }[c]));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function rawHtml(s) { return s == null ? "" : String(s); }
|
|
99
|
+
|
|
100
|
+
function formatTime(d) { return `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`; }
|
|
101
|
+
function formatDate(d) {
|
|
102
|
+
const days = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
|
|
103
|
+
const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
104
|
+
return `${days[d.getDay()]} ${months[d.getMonth()]} ${d.getDate()}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function frictionClass(level) {
|
|
108
|
+
const num = parseInt(String(level || "L0").replace(/[^\d]/g,""), 10) || 0;
|
|
109
|
+
if (num >= 3) return "l3";
|
|
110
|
+
if (num >= 2) return "l2";
|
|
111
|
+
if (num >= 1) return "l1";
|
|
112
|
+
return "l0";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function frictionPct(level) {
|
|
116
|
+
const num = parseInt(String(level || "L0").replace(/[^\d]/g,""), 10) || 0;
|
|
117
|
+
return Math.min(100, num * 33);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function build({ vault, version }) {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
const errors = [];
|
|
123
|
+
|
|
124
|
+
function tryParse(name, fn, fallback = null) {
|
|
125
|
+
try { return fn(); } catch (e) { errors.push(`[${name}] ${e.message}`); return fallback; }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parse all data sources
|
|
129
|
+
const ac = tryParse("active-context", () => parseActiveContext(vault), { available: false });
|
|
130
|
+
const al = tryParse("auto-learnings", () => parseAutoLearnings(vault), { available: false, entries: [], stats: {}, activePatternsConf3Plus: [], growingPatterns: [], escalatedPatterns: [] });
|
|
131
|
+
const manifest = tryParse("pattern-manifest", () => loadPatternManifest(), { available: false });
|
|
132
|
+
const projects = tryParse("projects", () => scanProjects(vault, { limit: 8 }), { available: false, projects: [] });
|
|
133
|
+
const engineHealth = tryParse("engine-health", () => computeEngineHealth(vault), null);
|
|
134
|
+
const daily = tryParse("daily-note", () => resolveDailyNotes(vault, { lookback: 30 }), null);
|
|
135
|
+
const identity = tryParse("identity", () => parseIdentity(vault), { available: false });
|
|
136
|
+
const strategy = tryParse("strategy", () => parseStrategy(vault), { available: false });
|
|
137
|
+
const goals = tryParse("goals", () => parseGoals(vault), { available: false });
|
|
138
|
+
const library = tryParse("library", () => indexLibrary(vault), { available: false });
|
|
139
|
+
const voiceDna = tryParse("voice-dna", () => parseVoiceDna(vault), { available: false });
|
|
140
|
+
const metrics = tryParse("metrics", () => parseMetricsLog(vault), { available: false });
|
|
141
|
+
const healthProtocols = tryParse("health-protocols", () => parseHealthProtocols(vault), { available: false, protocols: [] });
|
|
142
|
+
const agents = tryParse("agents", () => detectAgents(), { available: false, agents: [] });
|
|
143
|
+
const hookActivity = tryParse("hook-activity", () => readHookActivity(), { available: false, recentMarkers: [] });
|
|
144
|
+
const dossier = tryParse("dossier", () => parseDossier(vault), { available: false });
|
|
145
|
+
|
|
146
|
+
// V5 wiring (wishlist #49 — Codex viral pick #1). Parser already shipped
|
|
147
|
+
// but the UI module + state.json field never landed. Walks Daily Notes
|
|
148
|
+
// for FLAG/VERDICT/ROAST markers + Auto_Learnings escalations.
|
|
149
|
+
const { compute: computeRoastWall } = require("./lib/roast-wall-parser");
|
|
150
|
+
const roastWall = tryParse(
|
|
151
|
+
"roast-wall",
|
|
152
|
+
() => computeRoastWall({ vault, autoLearnings: al, daysLookback: 60 }),
|
|
153
|
+
{ available: false, count: 0, roasts: [], stats: { high: 0, med: 0, low: 0 } }
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Phase 41.7 — Workflow read/write graph. Scans src/workflows/*.md for
|
|
157
|
+
// reads/writes annotations + engine file references; builds the
|
|
158
|
+
// directed graph powering Library → Workflow Graph tab.
|
|
159
|
+
const { scan: scanWorkflowGraph } = require("./lib/workflow-graph-parser");
|
|
160
|
+
const bundleRoot = path.join(__dirname, "..", "..");
|
|
161
|
+
const workflowGraph = tryParse(
|
|
162
|
+
"workflow-graph",
|
|
163
|
+
() => scanWorkflowGraph({ bundleRoot }),
|
|
164
|
+
{ available: false, workflowCount: 0, workflows: [], edges: [], files: [] }
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Black Box Correlations (V5_CLOSED_LOOP wishlist). Pearson r between
|
|
168
|
+
// Battery signals (sleep/protein/energy) and output signals
|
|
169
|
+
// (tasksDone/sessionCount/energy). Honest empty states when data sparse.
|
|
170
|
+
const { compute: computeCorrelations } = require("./lib/correlations-parser");
|
|
171
|
+
const correlations = tryParse(
|
|
172
|
+
"correlations",
|
|
173
|
+
() => computeCorrelations({ vault, windowDays: 30 }),
|
|
174
|
+
{ available: false, reason: "parser_error" }
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const now = new Date();
|
|
178
|
+
|
|
179
|
+
// Audit pass 2 (find-bugs #4): snoozes are loaded later into state.json
|
|
180
|
+
// but computeSuggestions ran without them, so a snoozed workflow stayed
|
|
181
|
+
// pinned to suggestedNow[0] until the rebuild after expiry. Load snoozes
|
|
182
|
+
// here and pass through so live snoozes filter out of recommendations.
|
|
183
|
+
const snoozeList = (() => {
|
|
184
|
+
try {
|
|
185
|
+
const snoozePath = path.join(os.homedir(), ".mover", "dashboard", "snoozes.json");
|
|
186
|
+
if (!fs.existsSync(snoozePath)) return [];
|
|
187
|
+
const raw = JSON.parse(fs.readFileSync(snoozePath, "utf8"));
|
|
188
|
+
return Array.isArray(raw) ? raw : [];
|
|
189
|
+
} catch (_) { return []; }
|
|
190
|
+
})();
|
|
191
|
+
const suggestions = tryParse("suggested-now", () => computeSuggestions({
|
|
192
|
+
now, ac, manifest, daily, engineHealth, strategy, projects, snoozes: snoozeList
|
|
193
|
+
}, { limit: 3 }), []);
|
|
194
|
+
|
|
195
|
+
// Compute "Today's Call" from energy + Single Test
|
|
196
|
+
const todayCall = computeTodayCall({ ac, manifest, engineHealth, friction: manifest ? manifest.frictionLevel : "L0" });
|
|
197
|
+
|
|
198
|
+
// Days to Single Test target
|
|
199
|
+
const daysToTest = computeDaysToTarget(strategy, manifest);
|
|
200
|
+
|
|
201
|
+
// Compute today's progress percentage
|
|
202
|
+
const todayProgress = computeTodayProgress(daily);
|
|
203
|
+
const studioVerdict = computeStudioVerdict({
|
|
204
|
+
todayCall,
|
|
205
|
+
manifest,
|
|
206
|
+
engineHealth,
|
|
207
|
+
daysToTest,
|
|
208
|
+
strategy
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Render template
|
|
212
|
+
const template = readTemplate("index.html");
|
|
213
|
+
|
|
214
|
+
const tokens = {
|
|
215
|
+
// Header
|
|
216
|
+
header_date: formatDate(now),
|
|
217
|
+
header_time: formatTime(now),
|
|
218
|
+
header_completion: dailyCompletionString(daily),
|
|
219
|
+
header_streak: daily ? String(daily.streak || 0) : "—",
|
|
220
|
+
header_friction: manifest ? (manifest.frictionLevel || "L0") : "—",
|
|
221
|
+
header_friction_class: frictionClass(manifest ? manifest.frictionLevel : "L0"),
|
|
222
|
+
header_engine_health: engineHealth ? engineHealth.overallStatus : "—",
|
|
223
|
+
header_engine_health_class: engineHealth ? engineHealth.overallStatus.toLowerCase() : "unknown",
|
|
224
|
+
header_days_to_test: daysToTest != null ? String(daysToTest) : "—",
|
|
225
|
+
studio_verdict_title: escape(studioVerdict.title),
|
|
226
|
+
studio_verdict_subline: escape(studioVerdict.subline),
|
|
227
|
+
|
|
228
|
+
// Hero
|
|
229
|
+
hero_archetype: identity && identity.archetype ? escape(identity.archetype.split("(")[0].trim()) : "Mover OS Operator",
|
|
230
|
+
hero_law: identity && identity.theLaw ? escape(identity.theLaw.replace(/^["']|["']$/g, "")) : "your AI knows you",
|
|
231
|
+
hero_core_drive: identity && identity.coreDrive ? escape(identity.coreDrive.split("(")[0].trim()) : "—",
|
|
232
|
+
hero_fatal_flaw: identity && identity.fatalFlaw ? escape(identity.fatalFlaw.split("(")[0].trim()) : "—",
|
|
233
|
+
hero_single_test: manifest && manifest.singleTest ? escape(manifest.singleTest) : (strategy && strategy.singleTest ? escape(strategy.singleTest) : "Define a Single Test in Strategy.md"),
|
|
234
|
+
hero_sacrifice: manifest && manifest.sacrifice ? escape(shortenForCallout(manifest.sacrifice)) : "—",
|
|
235
|
+
|
|
236
|
+
// Today's Call
|
|
237
|
+
today_call_class: todayCall.class,
|
|
238
|
+
today_call_status: todayCall.status,
|
|
239
|
+
today_call_text: escape(todayCall.text),
|
|
240
|
+
today_progress_pct: todayProgress.pct,
|
|
241
|
+
ring_offset: String(todayProgress.ringOffset),
|
|
242
|
+
|
|
243
|
+
// Strategy header
|
|
244
|
+
strategy_target_date: strategy && strategy.targetDate ? escape(strategy.targetDate) : "no date",
|
|
245
|
+
|
|
246
|
+
// Suggested Now
|
|
247
|
+
suggested_now_cards: rawHtml(renderSuggestedNow(suggestions, manifest)),
|
|
248
|
+
|
|
249
|
+
// Tactical
|
|
250
|
+
tactical_focus: shortenForMetric(extractFromAC(ac, "primary_metric") || extractFromAC(ac, "today_s_focus")) || "—",
|
|
251
|
+
tactical_hard_thing: shortenForMetric(extractFromAC(ac, "active_sacrifices") || extractFromAC(ac, "hard_thing"), 100) || "—",
|
|
252
|
+
tactical_floor: shortenForMetric(extractFromAC(ac, "current_floors") || extractFromAC(ac, "floor"), 100) || "—",
|
|
253
|
+
tactical_mode: shortenForMetric(extractFromAC(ac, "oscillation_trend") || extractFromAC(ac, "mode")) || "—",
|
|
254
|
+
|
|
255
|
+
// Energy
|
|
256
|
+
energy_battery: shortBattery(ac) || "—",
|
|
257
|
+
energy_zone: shortZone(ac) || "—",
|
|
258
|
+
energy_oscillation: shortOscillation(ac) || "—",
|
|
259
|
+
energy_session_age: ac && ac.workflowState && ac.workflowState.log_last_run ? humanizeTimeAgo(new Date(ac.workflowState.log_last_run), now) : "—",
|
|
260
|
+
|
|
261
|
+
// Patterns
|
|
262
|
+
patterns_list: rawHtml(renderPatterns(al)),
|
|
263
|
+
patterns_stat_count: al && al.activePatternsConf3Plus ? String(al.activePatternsConf3Plus.length) : "0",
|
|
264
|
+
patterns_stat_growing: al && al.growingPatterns ? String(al.growingPatterns.length) : "0",
|
|
265
|
+
patterns_stat_escalated: al && al.escalatedPatterns ? String(al.escalatedPatterns.length) : "0",
|
|
266
|
+
|
|
267
|
+
// Friction
|
|
268
|
+
friction_level: manifest ? (manifest.frictionLevel || "L0") : "L0",
|
|
269
|
+
friction_class: frictionClass(manifest ? manifest.frictionLevel : "L0"),
|
|
270
|
+
friction_pct: String(frictionPct(manifest ? manifest.frictionLevel : "L0")),
|
|
271
|
+
friction_zombie_count: manifest ? String(manifest.zombieCount || 0) : "0",
|
|
272
|
+
friction_override_count: manifest ? recentOverrideCount(manifest.overrideHistory, 14) : "0",
|
|
273
|
+
friction_zombies: manifest && manifest.zombieTasks && manifest.zombieTasks.length > 0
|
|
274
|
+
? manifest.zombieTasks.slice(0, 3).map(t => escape(t)).join(" · ")
|
|
275
|
+
: "no zombie tasks",
|
|
276
|
+
|
|
277
|
+
// Vitality
|
|
278
|
+
vitality_hrv: metrics && metrics.hrvLatest ? metrics.hrvLatest + " ms" : (extractFromAC(ac, "hrv") || "—"),
|
|
279
|
+
vitality_sleep: metrics && metrics.sleepLatest ? metrics.sleepLatest + " h" : "—",
|
|
280
|
+
vitality_rhr: metrics && metrics.rhrLatest ? metrics.rhrLatest + " bpm" : "—",
|
|
281
|
+
vitality_weight: metrics && metrics.weightLatest ? metrics.weightLatest + " kg" : "—",
|
|
282
|
+
health_protocols_block: rawHtml(renderHealthProtocols(healthProtocols) || renderHealthProtocolEmpty()),
|
|
283
|
+
|
|
284
|
+
// Engine Health
|
|
285
|
+
engine_health_items: rawHtml(renderEngineHealth(engineHealth)),
|
|
286
|
+
|
|
287
|
+
// Agents
|
|
288
|
+
agents_grid: rawHtml(renderAgentsGrid(agents)),
|
|
289
|
+
agents_summary: agents && agents.installedCount ? `${agents.installedCount} installed · ${agents.runnableCount} runnable · ${agents.hookSupportedCount} hook-enabled` : "no agents detected",
|
|
290
|
+
|
|
291
|
+
// Streaks
|
|
292
|
+
streak_current: daily ? String(daily.streak || 0) : "—",
|
|
293
|
+
streak_month: daily ? String(daily.totalThisMonth || 0) : "—",
|
|
294
|
+
streak_year: daily ? String(daily.totalThisYear || 0) : "—",
|
|
295
|
+
heatmap: rawHtml(renderHeatmap(daily)),
|
|
296
|
+
|
|
297
|
+
// Projects
|
|
298
|
+
projects_list: rawHtml(renderProjects(projects)),
|
|
299
|
+
|
|
300
|
+
// Strategy
|
|
301
|
+
strategy_version: strategy && strategy.version ? "V" + escape(strategy.version) : "—",
|
|
302
|
+
strategy_updated: strategy && strategy.lastUpdatedField ? escape(strategy.lastUpdatedField) : "—",
|
|
303
|
+
strategy_hypothesis: strategy && strategy.hypothesisBody ? escape(extractFirstSentences(strategy.hypothesisBody, 2)) : "—",
|
|
304
|
+
strategy_validation: strategy && strategy.validationCriteria ? escape(strategy.validationCriteria) : "—",
|
|
305
|
+
strategy_fail: strategy && strategy.failCondition ? escape(strategy.failCondition) : "—",
|
|
306
|
+
|
|
307
|
+
// Goals
|
|
308
|
+
goals_list: rawHtml(renderGoals(goals)),
|
|
309
|
+
|
|
310
|
+
// Library
|
|
311
|
+
library_counts: rawHtml(renderLibraryCounts(library)),
|
|
312
|
+
library_recent: rawHtml(renderLibraryRecent(library)),
|
|
313
|
+
|
|
314
|
+
// Entities
|
|
315
|
+
entities_grid: rawHtml(renderEntities(library)),
|
|
316
|
+
|
|
317
|
+
// Voice DNA
|
|
318
|
+
voice_samples: voiceDna && voiceDna.totalSamples ? String(voiceDna.totalSamples) : "0",
|
|
319
|
+
voice_banned: voiceDna && voiceDna.bannedPhrases ? String(voiceDna.bannedPhrases.length) : "0",
|
|
320
|
+
voice_traits: voiceDna && voiceDna.available
|
|
321
|
+
? renderVoiceTraits(voiceDna)
|
|
322
|
+
: "no voice dna file",
|
|
323
|
+
|
|
324
|
+
// Identity
|
|
325
|
+
identity_evolved: identity && identity.evolvedSelf ? escape(extractFirstSentences(identity.evolvedSelf, 2)) : (identity && identity.theLaw ? escape(identity.theLaw) : "—"),
|
|
326
|
+
identity_anti: identity && identity.antiIdentityBody ? escape(extractFirstSentences(identity.antiIdentityBody, 2)) : "—",
|
|
327
|
+
|
|
328
|
+
// Active_Context lists
|
|
329
|
+
backlog_list: rawHtml(renderBulletList((ac && ac.backlog) || [])),
|
|
330
|
+
waiting_list: rawHtml(renderBulletList((ac && ac.waitingFor) || [])),
|
|
331
|
+
commitments_list: rawHtml(renderBulletList((ac && ac.commitments) || [])),
|
|
332
|
+
override_list: rawHtml(renderOverrides(manifest)),
|
|
333
|
+
|
|
334
|
+
// Hook activity
|
|
335
|
+
hook_stats: rawHtml(renderHookStats(hookActivity)),
|
|
336
|
+
hook_recent: rawHtml(renderHookRecent(hookActivity)),
|
|
337
|
+
|
|
338
|
+
// Dossier
|
|
339
|
+
dossier_list: rawHtml(renderDossier(dossier)),
|
|
340
|
+
|
|
341
|
+
// Footer
|
|
342
|
+
build_time: formatTime(now),
|
|
343
|
+
version: version || "unknown",
|
|
344
|
+
vault_short: vault ? path.basename(vault) : "—"
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
let html = template;
|
|
348
|
+
for (const [key, val] of Object.entries(tokens)) {
|
|
349
|
+
// Audit M8 — keys are developer-controlled today, but escaping
|
|
350
|
+
// regex metacharacters before embedding into `new RegExp(...)`
|
|
351
|
+
// prevents ReDoS / mis-replacement if any key ever contains a `.`,
|
|
352
|
+
// `*`, `+`, `(`, etc. (e.g. a parser leaks a user field into the
|
|
353
|
+
// tokens map).
|
|
354
|
+
const safeKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
355
|
+
html = html.replace(new RegExp("{{\\s*" + safeKey + "\\s*}}", "g"), val == null ? "—" : String(val));
|
|
356
|
+
}
|
|
357
|
+
// Replace any remaining tokens
|
|
358
|
+
html = html.replace(/{{\s*[a-z_]+\s*}}/g, "—");
|
|
359
|
+
|
|
360
|
+
// Write output
|
|
361
|
+
const outDir = dashboardOutDir();
|
|
362
|
+
ensureDir(outDir);
|
|
363
|
+
fs.writeFileSync(dashboardIndexPath(), html, "utf8");
|
|
364
|
+
copyStatic(outDir);
|
|
365
|
+
|
|
366
|
+
if (errors.length > 0) {
|
|
367
|
+
const log = `[${new Date().toISOString()}] errors=${errors.length}\n` + errors.map(e => " - " + e).join("\n") + "\n";
|
|
368
|
+
fs.appendFileSync(dashboardBuildLogPath(), log);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const builtAt = new Date();
|
|
372
|
+
|
|
373
|
+
// ───────── StateJson — the typed contract the React UI reads ─────────
|
|
374
|
+
// Matches src/dashboard/ui/src/lib/types.ts. Single source of truth for v3.
|
|
375
|
+
// Per-section fallbacks keep the API alive even when individual parsers fail.
|
|
376
|
+
|
|
377
|
+
// Compute phantom rows up front so drift-score can incorporate them as
|
|
378
|
+
// its "contradictions" component (Codex v3-fidelity #10 — 5th breakdown axis).
|
|
379
|
+
const phantomRowsForDrift = phantomRowsForState(strategy, al, manifest);
|
|
380
|
+
// Look up yesterday's persisted value to compute a real delta (was always 0
|
|
381
|
+
// because we passed yesterdayScore: null). drift-history.js stores one
|
|
382
|
+
// snapshot per calendar day in ~/.mover/drift-history.json.
|
|
383
|
+
const driftHistEntries = tryParse("drift-history-read", () => driftHistory.getHistory(90), []);
|
|
384
|
+
const yesterdayKey = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
385
|
+
const yesterdayEntry = driftHistEntries.find(e => e.date === yesterdayKey);
|
|
386
|
+
// Distribution computed up-front so drift derives the gap from REAL per-day
|
|
387
|
+
// counts instead of a fabricated default. [false-zero discipline, T261]
|
|
388
|
+
const distributionForStateJson = tryParse("distribution", () => distributionParser.compute({ vault, daysLookback: 365 }), { available: false, days: [], todayCount: null, total7d: null, total30d: null, delta7d: null });
|
|
389
|
+
const drift = tryParse("drift-score", () => driftScore.compute({
|
|
390
|
+
manifest,
|
|
391
|
+
activeContext: ac,
|
|
392
|
+
engineHealth: engineHealthForState(engineHealth),
|
|
393
|
+
distribution: distributionForStateJson,
|
|
394
|
+
todayPlanRanDistribution: !!(daily && daily.today && daily.today.exists),
|
|
395
|
+
yesterdayScore: yesterdayEntry ? yesterdayEntry.value : null,
|
|
396
|
+
phantomCount: phantomRowsForDrift.length,
|
|
397
|
+
}), null);
|
|
398
|
+
// Persist today's value back to history so tomorrow's run has a delta
|
|
399
|
+
// and the dashboard sparkline gets a real time-series.
|
|
400
|
+
if (drift && Number.isFinite(drift.value)) {
|
|
401
|
+
tryParse("drift-history-write", () => driftHistory.recordSnapshot({
|
|
402
|
+
value: drift.value,
|
|
403
|
+
components: drift.components,
|
|
404
|
+
}), null);
|
|
405
|
+
}
|
|
406
|
+
// Re-read after the write so the history array we expose includes today.
|
|
407
|
+
const driftHistoryArr = tryParse("drift-history-final", () => driftHistory.getHistory(30), []);
|
|
408
|
+
if (drift) drift.history = driftHistoryArr;
|
|
409
|
+
|
|
410
|
+
const forked = tryParse("forked-future", () => forkedFuture.compute({
|
|
411
|
+
driftScore: drift,
|
|
412
|
+
recent7d: [],
|
|
413
|
+
activePatterns: (al && al.activePatternsConf3Plus) || [],
|
|
414
|
+
horizonDays: 14,
|
|
415
|
+
}), null);
|
|
416
|
+
|
|
417
|
+
// distributionForStateJson computed earlier (before drift). [T261]
|
|
418
|
+
const overrideEntries = normalizeOverrides(manifest);
|
|
419
|
+
const overrideSummary = computeOverrideSummary(overrideEntries, builtAt);
|
|
420
|
+
const engineHealthState = engineHealthForState(engineHealth);
|
|
421
|
+
const goalForecast = tryParse("goal-forecast", () => computeGoalForecast({
|
|
422
|
+
strategy,
|
|
423
|
+
activeContext: ac,
|
|
424
|
+
distribution: distributionForStateJson,
|
|
425
|
+
engineHealth: engineHealthState,
|
|
426
|
+
daily,
|
|
427
|
+
now: builtAt
|
|
428
|
+
}), { available: false, adjustments: [], workflows: [] });
|
|
429
|
+
// Per-domain measurement forecast — dispatches each domain on its declared shape
|
|
430
|
+
// (single | chain | scalar | streak), honest-null until that domain logs data.
|
|
431
|
+
const domainForecasts = tryParse("domain-forecasts", () => {
|
|
432
|
+
const decl = (strategy && strategy.measurement) || {};
|
|
433
|
+
return {
|
|
434
|
+
work: parseDomainProgress({ domain: "work", decl: decl.work, goalForecast, metricsLog: metrics }),
|
|
435
|
+
vitality: parseDomainProgress({ domain: "vitality", decl: decl.vitality, goalForecast, metricsLog: metrics }),
|
|
436
|
+
faith: parseDomainProgress({ domain: "faith", decl: decl.faith, goalForecast, metricsLog: metrics }),
|
|
437
|
+
};
|
|
438
|
+
}, { work: { available: false, type: null }, vitality: { available: false, type: null }, faith: { available: false, type: null } });
|
|
439
|
+
const executionPlan = tryParse("execution-plan", () => computeExecutionPlan({
|
|
440
|
+
suggestions,
|
|
441
|
+
goalForecast,
|
|
442
|
+
healthProtocols,
|
|
443
|
+
projects,
|
|
444
|
+
engineHealth: engineHealthState,
|
|
445
|
+
daily,
|
|
446
|
+
overrides: overrideSummary,
|
|
447
|
+
}), { available: false, items: [], capacityScore: 0, checked: 0, total: 0, lanes: [] });
|
|
448
|
+
|
|
449
|
+
// What to call the operator: config userName (user-editable in Configure)
|
|
450
|
+
// wins, then the Identity_Prime H1 name written by onboarding. Never a
|
|
451
|
+
// folder or account name — an honest null greets namelessly instead.
|
|
452
|
+
const cfgForName = tryParse("config", () => require("./lib/config-parser").read(), {});
|
|
453
|
+
const profileName =
|
|
454
|
+
(cfgForName && typeof cfgForName.userName === "string" && cfgForName.userName.trim()) ||
|
|
455
|
+
(identity && identity.name) || null;
|
|
456
|
+
|
|
457
|
+
const stateJson = {
|
|
458
|
+
builtAt: builtAt.toISOString(),
|
|
459
|
+
vaultPath: vault,
|
|
460
|
+
profileName,
|
|
461
|
+
version: version || "unknown",
|
|
462
|
+
engineHealth: engineHealthState,
|
|
463
|
+
activeContext: (function() {
|
|
464
|
+
// Inject typed active patterns from Auto_Learnings parser before mapping
|
|
465
|
+
const typed = ((al && al.activePatternsConf3Plus) || []).map(p => ({
|
|
466
|
+
id: p.name || p.id || "",
|
|
467
|
+
description: extractPatternHeadline(p),
|
|
468
|
+
confidence: typeof p.confidence === "number" ? p.confidence : 0,
|
|
469
|
+
status: p.status || "",
|
|
470
|
+
severity: p.severity || "",
|
|
471
|
+
polarity: p.polarity || "neutral",
|
|
472
|
+
category: p.category || "watching",
|
|
473
|
+
firstDetected: p.firstDetected || "",
|
|
474
|
+
lastUpdated: p.lastUpdated || "",
|
|
475
|
+
sourcePath: (al && al.filePath) || ""
|
|
476
|
+
}));
|
|
477
|
+
const acWithTyped = ac ? { ...ac, __activePatternsTyped: typed } : ac;
|
|
478
|
+
return activeContextForState(acWithTyped, manifest);
|
|
479
|
+
})(),
|
|
480
|
+
driftScore: drift,
|
|
481
|
+
forkedFuture: forked,
|
|
482
|
+
phantomRows: phantomRowsForDrift,
|
|
483
|
+
suggestedNow: (suggestions || []).map(s => ({
|
|
484
|
+
workflow: s.workflow || s.command || "",
|
|
485
|
+
reason: s.reason || s.explanation || "",
|
|
486
|
+
score: typeof s.score === "number" ? s.score : 0,
|
|
487
|
+
signalKey: s.signal || s.key || ""
|
|
488
|
+
})),
|
|
489
|
+
// V5.0 activation stats (Codex #3 wiring fix, 2026-05-24). Reads from
|
|
490
|
+
// ~/.mover/activation-events.jsonl. Local-only, telemetry-never.
|
|
491
|
+
// Cheap (~1ms typical) — re-read on every build.
|
|
492
|
+
activation: tryParse("activation-log", () => readActivationStats(), {
|
|
493
|
+
available: false,
|
|
494
|
+
installDays: 0,
|
|
495
|
+
dashboardOpens: 0,
|
|
496
|
+
dashboardOpens7d: 0,
|
|
497
|
+
workflowRuns: 0,
|
|
498
|
+
workflowRuns7d: 0,
|
|
499
|
+
logCompletions7d: 0,
|
|
500
|
+
distinctOpenDays: 0,
|
|
501
|
+
activated: false,
|
|
502
|
+
firstEventAt: null,
|
|
503
|
+
}),
|
|
504
|
+
agents: (agents && agents.agents ? agents.agents : []).map(a => ({
|
|
505
|
+
id: a.id || a.name || "",
|
|
506
|
+
name: a.name || a.id || "",
|
|
507
|
+
// agent-detector.js now intersects config-dir + binary-on-PATH. Pass through.
|
|
508
|
+
installed: !!a.installed,
|
|
509
|
+
runnable: !!a.runnable,
|
|
510
|
+
hookSupport: !!a.hookSupport,
|
|
511
|
+
detectedPath: a.configPath || a.path || a.detectedPath || undefined
|
|
512
|
+
})),
|
|
513
|
+
workflows: (() => {
|
|
514
|
+
try {
|
|
515
|
+
// bundleRoot = src/dashboard/build.js → ../../ = bundle root
|
|
516
|
+
const wl = workflowLibrary.indexWorkflows(path.resolve(__dirname, "..", ".."));
|
|
517
|
+
return (wl.workflows || []).map(w => ({
|
|
518
|
+
name: w.name,
|
|
519
|
+
description: w.description || "",
|
|
520
|
+
risk: w.risk || "LOW",
|
|
521
|
+
target: w.target || "",
|
|
522
|
+
reads: w.reads || [],
|
|
523
|
+
writes: w.writes || [],
|
|
524
|
+
handsOffTo: w.handsOffTo || [],
|
|
525
|
+
// v6 SURFACE 1 — Say-it card copy + structured io contract
|
|
526
|
+
does: w.does,
|
|
527
|
+
long: w.long,
|
|
528
|
+
needsYou: w.needsYou,
|
|
529
|
+
io: w.io ?? null
|
|
530
|
+
}));
|
|
531
|
+
} catch (e) {
|
|
532
|
+
errors.push(`[workflow-library] ${e.message}`);
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
})(),
|
|
536
|
+
// Wave 2 — newly-wired parsers
|
|
537
|
+
strategy: (() => {
|
|
538
|
+
const out = tryParse("strategy", () => strategyParser.parseStrategy(vault), { available: false });
|
|
539
|
+
if (!out || !out.available) return { available: false };
|
|
540
|
+
return {
|
|
541
|
+
available: true,
|
|
542
|
+
version: out.version || null,
|
|
543
|
+
primeObjective: out.primeObjective || null,
|
|
544
|
+
stretchTarget: out.stretchTarget || null,
|
|
545
|
+
currentState: out.currentState || null,
|
|
546
|
+
singleTest: out.singleTest || null,
|
|
547
|
+
targetDate: out.targetDate || null,
|
|
548
|
+
measurement: out.measurement || null,
|
|
549
|
+
lastUpdated: out.lastUpdated ? out.lastUpdated.toISOString() : (out.lastUpdatedField || null),
|
|
550
|
+
hypotheses: (out.hypotheses || []).map(h => ({
|
|
551
|
+
domain: h.domain,
|
|
552
|
+
body: (h.body || '').slice(0, 1200) // cap body so StateJson stays lean
|
|
553
|
+
}))
|
|
554
|
+
};
|
|
555
|
+
})(),
|
|
556
|
+
goalForecast,
|
|
557
|
+
domainForecasts,
|
|
558
|
+
executionPlan,
|
|
559
|
+
identity: (() => {
|
|
560
|
+
const out = tryParse("identity", () => identityParser.parseIdentity(vault), { available: false });
|
|
561
|
+
if (!out || !out.available) return { available: false };
|
|
562
|
+
return {
|
|
563
|
+
available: true,
|
|
564
|
+
archetype: out.archetype || null,
|
|
565
|
+
coreDrive: out.coreDrive || null,
|
|
566
|
+
fatalFlaw: out.fatalFlaw || null,
|
|
567
|
+
theLaw: out.theLaw || null,
|
|
568
|
+
antiIdentity: out.antiIdentity || null,
|
|
569
|
+
antiIdentityBody: out.antiIdentityBody || null,
|
|
570
|
+
psychFirewall: out.psychFirewall || null
|
|
571
|
+
};
|
|
572
|
+
})(),
|
|
573
|
+
// F19 — Anti-Identity Detector hits, cross-referencing patterns +
|
|
574
|
+
// overrides against the user's anti-identity statement.
|
|
575
|
+
antiIdentityHits: (() => {
|
|
576
|
+
const identity = tryParse("identity", () => identityParser.parseIdentity(vault), { available: false });
|
|
577
|
+
// ac is the parsed active-context. Patterns live in its activePatterns array.
|
|
578
|
+
const patterns = (ac && ac.activePatterns) || [];
|
|
579
|
+
return tryParse(
|
|
580
|
+
"anti-identity-detect",
|
|
581
|
+
() => antiIdentityDetector.detect({ identity, patterns, overrides: overrideEntries }),
|
|
582
|
+
{ available: false }
|
|
583
|
+
);
|
|
584
|
+
})(),
|
|
585
|
+
// F39 — Vault wiki-link graph (distinct from PublicProofGraph which
|
|
586
|
+
// shows behavioral patterns). Walks every md file in the vault to
|
|
587
|
+
// build a node-edge map of [[wiki-links]]. Capped at 200 nodes for
|
|
588
|
+
// payload reasonableness. Connections route renders this.
|
|
589
|
+
vaultGraph: tryParse(
|
|
590
|
+
"vault-graph",
|
|
591
|
+
() => vaultGraphParser.buildVaultGraph(vault, { limit: 200 }),
|
|
592
|
+
{ available: false, nodes: [], edges: [] }
|
|
593
|
+
),
|
|
594
|
+
// Voice DNA — surface the REAL Voice_DNA.md calibration to the dashboard
|
|
595
|
+
// (the Library's "Your writing voice" panel). The parser already ran above
|
|
596
|
+
// (voiceDna); it was only feeding flat CLI strings, never the state tree,
|
|
597
|
+
// so the dashboard showed "harvest your voice" even though the file is
|
|
598
|
+
// already calibrated. honest-empty when the file is absent. No capped
|
|
599
|
+
// numbers leak out: sampleCount is the true total; bannedPhrases is capped
|
|
600
|
+
// in the parser so we don't surface it as a count.
|
|
601
|
+
voice: (() => {
|
|
602
|
+
if (!voiceDna || !voiceDna.available) return { available: false };
|
|
603
|
+
const traits = [];
|
|
604
|
+
if (voiceDna.lowercaseI) traits.push("lowercase 'i'");
|
|
605
|
+
if (voiceDna.commaSpliced) traits.push("comma-spliced");
|
|
606
|
+
if (voiceDna.usesBrother) traits.push('says "brother"');
|
|
607
|
+
if (voiceDna.urlInline) traits.push("inline urls");
|
|
608
|
+
if ((voiceDna.totalSamples || 0) > 0 && Number.isFinite(voiceDna.shortestSample)) {
|
|
609
|
+
traits.push(`${voiceDna.shortestSample}-${voiceDna.longestSample} char range`);
|
|
610
|
+
}
|
|
611
|
+
// Count writing rules + banned phrases UNCAPPED from the raw file (the
|
|
612
|
+
// parser caps bannedPhrases at 12, which would under-report). A voice is
|
|
613
|
+
// "calibrated" when the file defines rules / banned phrases / traits —
|
|
614
|
+
// sample passages are a bonus, not the bar (the owner's file has rules
|
|
615
|
+
// but no fenced samples, and is correctly already calibrated).
|
|
616
|
+
const raw = voiceDna.raw || "";
|
|
617
|
+
// Pull the actual bullet TEXT under a heading (until the next h2), so the
|
|
618
|
+
// Library can SHOW the voice (how it's defined + what it avoids), not just
|
|
619
|
+
// a status badge. Real lines only; never invented.
|
|
620
|
+
const sectionBullets = (headingRe) => {
|
|
621
|
+
const m = raw.match(headingRe);
|
|
622
|
+
if (!m) return [];
|
|
623
|
+
const rest = raw.slice(m.index + m[0].length);
|
|
624
|
+
const end = rest.search(/\n##\s/); // stop at the next h2
|
|
625
|
+
const section = end >= 0 ? rest.slice(0, end) : rest;
|
|
626
|
+
return (section.match(/^\s*[-*]\s+.+$/gm) || [])
|
|
627
|
+
.map((l) => l.replace(/^\s*[-*]\s+/, "").trim())
|
|
628
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
629
|
+
};
|
|
630
|
+
const ruleLines = sectionBullets(/##\s*Writing Rules[^\n]*\n/i);
|
|
631
|
+
const bannedLines = sectionBullets(/##\s*Banned[^\n]*\n/i);
|
|
632
|
+
const rulesCount = ruleLines.length;
|
|
633
|
+
const bannedCount = bannedLines.length;
|
|
634
|
+
return {
|
|
635
|
+
available: true,
|
|
636
|
+
calibrated: rulesCount > 0 || bannedCount > 0 || (voiceDna.totalSamples || 0) > 0 || traits.length > 0,
|
|
637
|
+
lastUpdated: voiceDna.lastUpdated || null,
|
|
638
|
+
rulesCount,
|
|
639
|
+
bannedCount,
|
|
640
|
+
sampleCount: voiceDna.totalSamples || 0,
|
|
641
|
+
traits,
|
|
642
|
+
// a few REAL rules / banned phrases — the voice's actual definition
|
|
643
|
+
rules: ruleLines.slice(0, 6).map((s) => (s.length > 130 ? s.slice(0, 130).trim() + "…" : s)),
|
|
644
|
+
banned: bannedLines.slice(0, 10).map((s) => s.replace(/^["'""]|["'""]$/g, "").trim()),
|
|
645
|
+
// up to 3 real writing samples (trimmed) — literally the user's own voice
|
|
646
|
+
samples: (voiceDna.samples || [])
|
|
647
|
+
.filter((s) => s && s.trim().length > 0)
|
|
648
|
+
.slice(0, 3)
|
|
649
|
+
.map((s) => (s.length > 220 ? s.slice(0, 220).trim() + "…" : s.trim())),
|
|
650
|
+
path: voiceDna.filePath || null,
|
|
651
|
+
};
|
|
652
|
+
})(),
|
|
653
|
+
goals: (() => {
|
|
654
|
+
const out = tryParse("goals", () => goalsParser.parseGoals(vault), { available: false });
|
|
655
|
+
if (!out || !out.available) return { available: false, horizons: [] };
|
|
656
|
+
return {
|
|
657
|
+
available: true,
|
|
658
|
+
horizons: (out.horizons || []).slice(0, 5).map(h => ({
|
|
659
|
+
title: h.title || "",
|
|
660
|
+
body: (h.body || "").slice(0, 600)
|
|
661
|
+
}))
|
|
662
|
+
};
|
|
663
|
+
})(),
|
|
664
|
+
projects: (() => {
|
|
665
|
+
const out = tryParse("projects", () => projectScanner.scanProjects(vault, { limit: 12 }), { available: false, projects: [] });
|
|
666
|
+
if (!out || !out.available) return { available: false, projects: [], totalCount: 0 };
|
|
667
|
+
return {
|
|
668
|
+
available: true,
|
|
669
|
+
totalCount: out.totalCount || 0,
|
|
670
|
+
activeCount: typeof out.activeCount === "number" ? out.activeCount : undefined,
|
|
671
|
+
projects: (out.projects || []).map(p => ({
|
|
672
|
+
name: p.name || "",
|
|
673
|
+
path: p.path || "",
|
|
674
|
+
planPath: p.planPath || "",
|
|
675
|
+
totalTasks: p.totalTasks || 0,
|
|
676
|
+
doneTasks: p.doneTasks || 0,
|
|
677
|
+
openTasks: p.openTasks || 0,
|
|
678
|
+
unverifiedTasks: p.unverifiedTasks || 0,
|
|
679
|
+
zombieTasks: p.zombieTasks || 0,
|
|
680
|
+
lastModified: p.lastModified ? p.lastModified.toISOString() : null,
|
|
681
|
+
stateExcerpt: p.stateExcerpt || null,
|
|
682
|
+
recentExecutionLog: (p.recentExecutionLog || []).slice(0, 3)
|
|
683
|
+
}))
|
|
684
|
+
};
|
|
685
|
+
})(),
|
|
686
|
+
metrics: (() => {
|
|
687
|
+
const out = tryParse("metrics-log", () => metricsLog.parseMetricsLog(vault), { available: false });
|
|
688
|
+
if (!out || !out.available) return { available: false };
|
|
689
|
+
return {
|
|
690
|
+
available: true,
|
|
691
|
+
sleepLatest: out.sleepLatest || null,
|
|
692
|
+
rhrLatest: out.rhrLatest || null,
|
|
693
|
+
weightLatest: out.weightLatest || null,
|
|
694
|
+
rolling30: out.rolling30 || null,
|
|
695
|
+
days: (out.days || []).slice(-30)
|
|
696
|
+
};
|
|
697
|
+
})(),
|
|
698
|
+
library: (() => {
|
|
699
|
+
const out = tryParse("library", () => libraryIndexer.indexLibrary(vault), { available: false });
|
|
700
|
+
if (!out || !out.available) return { available: false };
|
|
701
|
+
return {
|
|
702
|
+
available: true,
|
|
703
|
+
mocs: out.mocs || 0,
|
|
704
|
+
principles: out.principles || 0,
|
|
705
|
+
cheatsheets: out.cheatsheets || 0,
|
|
706
|
+
sops: out.sops || 0,
|
|
707
|
+
entities: out.entities || 0,
|
|
708
|
+
inputs: out.inputs || 0,
|
|
709
|
+
scripts: out.scripts || 0,
|
|
710
|
+
recentHarvests: (out.recentHarvests || []).slice(0, 5)
|
|
711
|
+
};
|
|
712
|
+
})(),
|
|
713
|
+
// v2 indexers (net additions for the holistic dashboard rebuild)
|
|
714
|
+
libraryV2: tryParse("library-v2", () => indexLibraryV2(vault), { available: false }),
|
|
715
|
+
skills: tryParse("skills", () => indexSkills(), { available: false, count: 0, byCategory: { system: [], specialist: [], domain: [] }, all: [] }),
|
|
716
|
+
hooks: tryParse("hooks", () => indexHooks(), { available: false, count: 0, byEvent: {}, registered: [], unregistered: [] }),
|
|
717
|
+
cliUsage: tryParse("cli-usage", () => parseCliUsage(), { available: false, commands: [] }),
|
|
718
|
+
// Briefing route's Execution Protocol — daily floor items per domain
|
|
719
|
+
// pulled from Strategy.md (not Daily Note tasks, which were the wrong
|
|
720
|
+
// source in the previous iteration).
|
|
721
|
+
strategyProtocol: tryParse("strategy-protocol", () => parseStrategyProtocol(vault), { available: false }),
|
|
722
|
+
experiments: tryParse("experiments", () => buildExperiments(vault), { available: false, all: [], active: [], past: [], byDomain: { work: [], vitality: [], faith: [] }, count: 0, activeCount: 0, pastCount: 0 }),
|
|
723
|
+
// v6 W1 — live work feed, moves-usage lighting, rewind keyframes, journal calibration gap.
|
|
724
|
+
// False-zero: every fallback matches the full shape; absent source -> available:false, never a fabricated 0.
|
|
725
|
+
feed: tryParse("feed", () => feedParser.compute({ vault, limit: 40 }), { available: false, caption: "", events: [], activeSessions: [] }),
|
|
726
|
+
movesUsage: tryParse("moves-usage", () => movesUsageParser.computeMovesUsage({ vault }), { available: false, source: "", totalRuns: null, byCommand: {} }),
|
|
727
|
+
rewind: tryParse("rewind", () => rewindSnapshots.buildRewind({ vault }), { available: false, keyframes: [], dailies: {}, gapBefore: "", gapAfter: "" }),
|
|
728
|
+
journalGap: (() => {
|
|
729
|
+
const ev = daily && daily.today && daily.today.journal ? daily.today.journal.evening : null;
|
|
730
|
+
const selfScore = ev && ev.score != null ? ev.score : null;
|
|
731
|
+
let rec = { score: null, why: null, basis: "not enough logged to score today" };
|
|
732
|
+
try { rec = recordScoreLib.computeRecordScore({ daily, distribution: distributionForStateJson }) || rec; } catch (_) {}
|
|
733
|
+
const recordScore = rec && rec.score != null ? rec.score : null;
|
|
734
|
+
// Trend reads the RAW resolver output (daily.recentNotes carries journal via
|
|
735
|
+
// analyzeNoteText); honest self-score history, empty on day 1.
|
|
736
|
+
const trend = (daily && daily.recentNotes ? daily.recentNotes : [])
|
|
737
|
+
.filter(n => n && n.journal && n.journal.evening && n.journal.evening.score != null)
|
|
738
|
+
.slice(0, 7)
|
|
739
|
+
.map(n => ({ date: n.date, score: n.journal.evening.score }));
|
|
740
|
+
return {
|
|
741
|
+
available: selfScore != null || recordScore != null,
|
|
742
|
+
selfScore,
|
|
743
|
+
recordScore,
|
|
744
|
+
gap: selfScore != null && recordScore != null ? selfScore - recordScore : null,
|
|
745
|
+
recordWhy: rec.why || null,
|
|
746
|
+
trend,
|
|
747
|
+
basis: rec.basis || null
|
|
748
|
+
};
|
|
749
|
+
})(),
|
|
750
|
+
// Wave 5 parsers
|
|
751
|
+
distribution: (() => {
|
|
752
|
+
return distributionForStateJson || { available: false, days: [], todayCount: null, total7d: null, total30d: null, delta7d: null };
|
|
753
|
+
})(),
|
|
754
|
+
streak: (() => {
|
|
755
|
+
// False-zero discipline (T261): the tryParse fallback must match the FULL
|
|
756
|
+
// StreakData shape — a bare { available:false } leaves days/currentStreak/
|
|
757
|
+
// coverage undefined and a consumer that reads them before checking
|
|
758
|
+
// `available` crashes (or worse, treats undefined as a clean streak).
|
|
759
|
+
const FULL_STREAK_FALLBACK = { available: false, days: [], currentStreak: 0, streakThroughYesterday: 0, todayExists: false, longestStreak: 0, totalDays: 0, coverage: 0 };
|
|
760
|
+
const out = tryParse("streak", () => streakParser.compute({ vault, daysLookback: 365 }), FULL_STREAK_FALLBACK);
|
|
761
|
+
return out || FULL_STREAK_FALLBACK;
|
|
762
|
+
})(),
|
|
763
|
+
overrideOutcomes: (() => {
|
|
764
|
+
try {
|
|
765
|
+
const out = tryParse("override-outcomes", () => overrideOutcomeParser.compute({ vault, overrides: overrideEntries.slice(-100), windowDays: 90 }), { available: false });
|
|
766
|
+
return out || { available: false, outcomes: [] };
|
|
767
|
+
} catch (_) { return { available: false, outcomes: [] }; }
|
|
768
|
+
})(),
|
|
769
|
+
overrideSummary,
|
|
770
|
+
refusals: (() => {
|
|
771
|
+
// False-zero discipline (T261): full RefusalsData shape on parse failure
|
|
772
|
+
// so count/count7d/recent are never undefined under available:false.
|
|
773
|
+
const FULL_REFUSALS_FALLBACK = { available: false, count: 0, count7d: 0, delta7d: 0, recent: [] };
|
|
774
|
+
const out = tryParse("refusals", () => refusalParser.compute({ daysLookback: 30 }), FULL_REFUSALS_FALLBACK);
|
|
775
|
+
return out || FULL_REFUSALS_FALLBACK;
|
|
776
|
+
})(),
|
|
777
|
+
hourlyActivity: (() => {
|
|
778
|
+
// False-zero discipline (T261): full HourlyActivity shape on both parse
|
|
779
|
+
// failure and the !available guard — previous empty-state was missing
|
|
780
|
+
// daysScanned + peakCount, so PerformanceCurve read undefined for the
|
|
781
|
+
// "last N days" label and the peak-band math under available:false.
|
|
782
|
+
const FULL_HOURLY_FALLBACK = { available: false, daysScanned: 0, hours: [], peakHour: null, peakCount: 0, todayHours: [], events: [] };
|
|
783
|
+
const out = tryParse("hourly-activity", () => hourlyActivity.compute({ vault, daysLookback: 14 }), FULL_HOURLY_FALLBACK);
|
|
784
|
+
if (!out || !out.available) return FULL_HOURLY_FALLBACK;
|
|
785
|
+
return {
|
|
786
|
+
available: true,
|
|
787
|
+
daysScanned: out.daysScanned || 0,
|
|
788
|
+
hours: out.hours || [],
|
|
789
|
+
peakHour: out.peakHour,
|
|
790
|
+
peakCount: out.peakCount || 0,
|
|
791
|
+
todayHours: out.todayHours || [],
|
|
792
|
+
events: out.events || []
|
|
793
|
+
};
|
|
794
|
+
})(),
|
|
795
|
+
// REAL tracked time from Session Log start-end ranges (the honest core of
|
|
796
|
+
// "did hours"). NOT a per-domain split — sessions tag a [project], never a
|
|
797
|
+
// Work/Vitality/Faith domain, so a domain breakdown would be fabricated.
|
|
798
|
+
sessionTime: (() => {
|
|
799
|
+
const FULL_SESSIONTIME_FALLBACK = {
|
|
800
|
+
available: false, daysScanned: 0,
|
|
801
|
+
today: { minutes: 0, sessions: 0 },
|
|
802
|
+
last7d: { minutes: 0, sessions: 0, untimed: 0 },
|
|
803
|
+
last30d: { minutes: 0, sessions: 0, untimed: 0 },
|
|
804
|
+
byProject7d: [], longest: null,
|
|
805
|
+
src: "Dailies session-log start-end ranges",
|
|
806
|
+
};
|
|
807
|
+
const out = tryParse("session-time", () => sessionTime.compute({ vault, daysLookback: 30 }), FULL_SESSIONTIME_FALLBACK);
|
|
808
|
+
return out || FULL_SESSIONTIME_FALLBACK;
|
|
809
|
+
})(),
|
|
810
|
+
daily: (() => {
|
|
811
|
+
if (!daily) {
|
|
812
|
+
return {
|
|
813
|
+
available: false,
|
|
814
|
+
today: null,
|
|
815
|
+
streak: 0,
|
|
816
|
+
recentNotes: [],
|
|
817
|
+
totalThisMonth: 0,
|
|
818
|
+
totalThisYear: 0
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
available: true,
|
|
823
|
+
today: daily.today ? {
|
|
824
|
+
date: daily.today.date || null,
|
|
825
|
+
path: daily.today.path || null,
|
|
826
|
+
exists: !!daily.today.exists,
|
|
827
|
+
tasksTotal: daily.today.tasksTotal || 0,
|
|
828
|
+
tasksDone: daily.today.tasksDone || 0,
|
|
829
|
+
// W12: per-task lines (text + checked) so Home checkboxes can write
|
|
830
|
+
// back through /api/checkbox/toggle. null on older parser output.
|
|
831
|
+
tasks: daily.today.tasks ?? null,
|
|
832
|
+
length: daily.today.length || 0,
|
|
833
|
+
// v6 SURFACE 1 — the real plan focus for the Say-it friction rule.
|
|
834
|
+
// Was dropped here; the parser computes it (daily-note-resolver).
|
|
835
|
+
focus: daily.today.focus ?? null,
|
|
836
|
+
// v6 W1 — today's-call + `## Journal` sub-anchors + plan/got. All
|
|
837
|
+
// false-zero: null when the section is absent, never a guessed value.
|
|
838
|
+
todaysCall: daily.today.todaysCall ?? null,
|
|
839
|
+
journal: daily.today.journal ?? null,
|
|
840
|
+
plan: daily.today.plan ?? null,
|
|
841
|
+
got: daily.today.got ?? null
|
|
842
|
+
} : null,
|
|
843
|
+
streak: daily.streak || 0,
|
|
844
|
+
recentNotes: (daily.recentNotes || []).slice(0, 14).map(n => ({
|
|
845
|
+
date: n.date,
|
|
846
|
+
exists: !!n.exists,
|
|
847
|
+
tasksTotal: n.tasksTotal || 0,
|
|
848
|
+
tasksDone: n.tasksDone || 0,
|
|
849
|
+
length: n.length || 0,
|
|
850
|
+
// v6 W1 — per-date journal + plan for the calibration trend + Rewind
|
|
851
|
+
journal: n.journal ?? null,
|
|
852
|
+
todaysCall: n.todaysCall ?? null,
|
|
853
|
+
plan: n.plan ?? null
|
|
854
|
+
})),
|
|
855
|
+
totalThisMonth: daily.totalThisMonth || 0,
|
|
856
|
+
totalThisYear: daily.totalThisYear || 0
|
|
857
|
+
};
|
|
858
|
+
})(),
|
|
859
|
+
healthProtocols: (() => {
|
|
860
|
+
if (!healthProtocols || !healthProtocols.available) return { available: false, protocols: [] };
|
|
861
|
+
return {
|
|
862
|
+
available: true,
|
|
863
|
+
protocols: healthProtocols.protocols.map(p => ({
|
|
864
|
+
title: p.title || "",
|
|
865
|
+
filePath: p.filePath || "",
|
|
866
|
+
lastUpdated: p.lastUpdated ? p.lastUpdated.toISOString() : null,
|
|
867
|
+
current: p.current ? {
|
|
868
|
+
heading: p.current.heading || "",
|
|
869
|
+
daysIntoPhase: p.daysIntoPhase != null ? p.daysIntoPhase : null,
|
|
870
|
+
daysRemaining: p.daysRemaining != null ? p.daysRemaining : null,
|
|
871
|
+
items: (p.current.items || []).map(it => ({ text: it.text, checked: !!it.checked })),
|
|
872
|
+
done: p.current.done || 0,
|
|
873
|
+
total: p.current.total || 0
|
|
874
|
+
} : null
|
|
875
|
+
}))
|
|
876
|
+
};
|
|
877
|
+
})(),
|
|
878
|
+
snoozes: (() => {
|
|
879
|
+
try {
|
|
880
|
+
const snoozePath = path.join(os.homedir(), ".mover", "dashboard", "snoozes.json");
|
|
881
|
+
if (!fs.existsSync(snoozePath)) return [];
|
|
882
|
+
const raw = JSON.parse(fs.readFileSync(snoozePath, "utf8"));
|
|
883
|
+
return Array.isArray(raw) ? raw : [];
|
|
884
|
+
} catch (_) { return []; }
|
|
885
|
+
})(),
|
|
886
|
+
roastWall: roastWall || { available: false, count: 0, roasts: [], stats: { high: 0, med: 0, low: 0 } },
|
|
887
|
+
workflowGraph: workflowGraph || { available: false, workflowCount: 0, workflows: [], edges: [], files: [] },
|
|
888
|
+
correlations: correlations || { available: false, reason: "parser_error" },
|
|
889
|
+
overrides: (() => {
|
|
890
|
+
try {
|
|
891
|
+
return overrideEntries.slice(-30);
|
|
892
|
+
} catch (_) { return []; }
|
|
893
|
+
})(),
|
|
894
|
+
// Strip absolute filesystem paths from error messages before they reach
|
|
895
|
+
// /api/state.json (code-bug-audit H5). Node's ENOENT errors include the
|
|
896
|
+
// full vault/home path which leaked filesystem layout to any browser tab.
|
|
897
|
+
errors: errors.map(msg => ({
|
|
898
|
+
source: msg.match(/^\[([^\]]+)\]/)?.[1] || "unknown",
|
|
899
|
+
message: msg.replace(/['"]?\/[^\s'":,]+/g, "[path]"),
|
|
900
|
+
}))
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
builtAt,
|
|
905
|
+
durationMs: Date.now() - startedAt,
|
|
906
|
+
sectionCount: 6,
|
|
907
|
+
errors,
|
|
908
|
+
state: stateJson
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ───────── StateJson shape mappers ─────────
|
|
913
|
+
|
|
914
|
+
function engineHealthForState(engineHealth) {
|
|
915
|
+
if (!engineHealth) {
|
|
916
|
+
// No-blackbox: the scan couldn't run. Emit nulls + available:false so the UI
|
|
917
|
+
// shows "health unavailable", NOT a fabricated all-OK / 0-days-stale vault.
|
|
918
|
+
// [false-zero discipline, T261]
|
|
919
|
+
return {
|
|
920
|
+
available: false,
|
|
921
|
+
unavailableReason: "parser_error",
|
|
922
|
+
scannedAt: new Date().toISOString(),
|
|
923
|
+
strategy: { staleDays: null, status: null, lastUpdated: "" },
|
|
924
|
+
dossier: { staleDays: null, status: null, lastUpdated: "" },
|
|
925
|
+
goals: { status: null, expired: [] },
|
|
926
|
+
analyseDay: { overdueDays: null, status: null, lastRun: null },
|
|
927
|
+
weeklyReview: { overdueDays: null, status: null, lastRun: null }
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const findItem = (key) => (engineHealth.items || []).find(i => i.file === key) || null;
|
|
931
|
+
const findWf = (lbl) => (engineHealth.workflowStaleness || []).find(w => w.label === lbl) || null;
|
|
932
|
+
// B1 fix: engine-health.js emits `file: "Strategy"` (capital, no .md) — see
|
|
933
|
+
// engine-health.js:45-53. Look up by THAT key first, then fall back to variants.
|
|
934
|
+
// A missing item now yields null (honest "no data"), never 0/"OK" — the old
|
|
935
|
+
// `|| 0` masked the 2026-05-14 "0D STALE on a weeks-old file" key-mismatch bug.
|
|
936
|
+
const strat = findItem("Strategy") || findItem("strategy") || findItem("Strategy.md") || {};
|
|
937
|
+
const doss = findItem("Mover_Dossier") || findItem("dossier") || findItem("Mover_Dossier.md") || findItem("Architect_Dossier.md") || {};
|
|
938
|
+
const ad = findWf("/analyse-day") || {};
|
|
939
|
+
const rw = findWf("/review-week") || {};
|
|
940
|
+
return {
|
|
941
|
+
available: true,
|
|
942
|
+
unavailableReason: null,
|
|
943
|
+
scannedAt: (engineHealth.scannedAt instanceof Date) ? engineHealth.scannedAt.toISOString() : String(engineHealth.scannedAt || ""),
|
|
944
|
+
strategy: { staleDays: strat.ageDays != null ? strat.ageDays : null, status: strat.status || null, lastUpdated: strat.mtime ? String(strat.mtime) : "" },
|
|
945
|
+
dossier: { staleDays: doss.ageDays != null ? doss.ageDays : null, status: doss.status || null, lastUpdated: doss.mtime ? String(doss.mtime) : "" },
|
|
946
|
+
goals: { status: (engineHealth.expiredGoals || []).length > 0 ? "RED" : "OK", expired: engineHealth.expiredGoals || [] },
|
|
947
|
+
analyseDay: { overdueDays: ad.ageDays != null ? ad.ageDays : null, status: ad.status || null, lastRun: ad.lastRun || null },
|
|
948
|
+
weeklyReview: { overdueDays: rw.ageDays != null ? rw.ageDays : null, status: rw.status || null, lastRun: rw.lastRun || null }
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Derive a concise headline from an Auto_Learnings pattern entry.
|
|
953
|
+
// Prefers the entry's "name" (header), falling back to first non-empty
|
|
954
|
+
// evidence line, then to a body excerpt.
|
|
955
|
+
function extractPatternHeadline(p) {
|
|
956
|
+
if (!p) return "";
|
|
957
|
+
if (p.name) return p.name;
|
|
958
|
+
if (p.evidence && p.evidence.length) {
|
|
959
|
+
const first = p.evidence[0];
|
|
960
|
+
return typeof first === "string" ? first : (first.text || first.summary || "");
|
|
961
|
+
}
|
|
962
|
+
return (p.body || "").split("\n").find(l => l.trim().length > 12) || "";
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function activeContextForState(ac, manifest) {
|
|
966
|
+
if (!ac || !ac.available) {
|
|
967
|
+
return {
|
|
968
|
+
available: false, lastUpdated: "", singleTest: null, sacrifice: [],
|
|
969
|
+
frictionLevel: null, zombieCount: null,
|
|
970
|
+
waitingFor: [], activeSessions: [], backlog: [], commitments: [], activePatterns: [],
|
|
971
|
+
graveyard: []
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
// T225: derive deadline LIVE from the manifest's Single Test prose
|
|
975
|
+
// instead of hardcoding null. Codex root-cause: TheBet + StudioShell
|
|
976
|
+
// T-MINUS chip + formatDeadline all key off `deadline`. With deadline
|
|
977
|
+
// null, the UI falls back to regex parsing the same text again, which
|
|
978
|
+
// it did correctly — but the deadline ISO was unavailable for any code
|
|
979
|
+
// path that needed a real Date object (countdown math, sorting,
|
|
980
|
+
// analytics). The parser already has battle-tested extractDateFromText
|
|
981
|
+
// that handles "by Month Day, Year" + ISO + month-name variants.
|
|
982
|
+
const { extractDateFromText } = require("./lib/strategy-parser");
|
|
983
|
+
const liveDeadline = manifest && manifest.singleTest
|
|
984
|
+
? extractDateFromText(manifest.singleTest)
|
|
985
|
+
: null;
|
|
986
|
+
// T242 bug hunt fix (C#20): status was hardcoded "unknown" so every UI
|
|
987
|
+
// surface keyed off `singleTest.status` always showed the gray branch.
|
|
988
|
+
// Derive from deadline distance to now: failed | at-risk | on-track.
|
|
989
|
+
// (Matches the types.ts union; goalForecast.status is computed elsewhere
|
|
990
|
+
// for richer signal but the singleTest carries a self-sufficient one.)
|
|
991
|
+
let liveStatus = "unknown";
|
|
992
|
+
if (liveDeadline) {
|
|
993
|
+
const deadlineMs = Date.parse(liveDeadline + "T23:59:59Z");
|
|
994
|
+
const nowMs = Date.now();
|
|
995
|
+
if (Number.isFinite(deadlineMs)) {
|
|
996
|
+
if (nowMs > deadlineMs) liveStatus = "failed";
|
|
997
|
+
else if (nowMs > deadlineMs - 14 * 24 * 3600 * 1000) liveStatus = "at-risk";
|
|
998
|
+
else liveStatus = "on-track";
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const singleTest = manifest && manifest.singleTest
|
|
1002
|
+
? { text: manifest.singleTest, deadline: liveDeadline || null, status: liveStatus }
|
|
1003
|
+
: null;
|
|
1004
|
+
const sacrifice = manifest && manifest.sacrifice
|
|
1005
|
+
? (Array.isArray(manifest.sacrifice) ? manifest.sacrifice : [manifest.sacrifice])
|
|
1006
|
+
: [];
|
|
1007
|
+
// false-zero (F5/T261): friction is only known from the manifest, and we read
|
|
1008
|
+
// the RAW field — the loader (pattern-manifest-loader.js) defaults a missing
|
|
1009
|
+
// friction_level to "L0", which would erase the absent-vs-real-0 distinction
|
|
1010
|
+
// before we can act on it. A present friction_level (incl. a real 0) → its
|
|
1011
|
+
// number; an absent field, or no manifest at all → null.
|
|
1012
|
+
const rawManifest = manifest && manifest.manifest ? manifest.manifest : null;
|
|
1013
|
+
const frictionLevel = rawManifest && rawManifest.friction_level != null
|
|
1014
|
+
? (parseInt(String(rawManifest.friction_level).replace(/[^\d]/g, ""), 10) || 0)
|
|
1015
|
+
: null;
|
|
1016
|
+
return {
|
|
1017
|
+
available: true,
|
|
1018
|
+
// T242 bug hunt fix (C#10): String(Date) emits Date.toString() which
|
|
1019
|
+
// is non-spec for cross-browser Date.parse. Force canonical ISO.
|
|
1020
|
+
lastUpdated: ac.lastUpdated ? new Date(ac.lastUpdated).toISOString() : "",
|
|
1021
|
+
singleTest,
|
|
1022
|
+
sacrifice,
|
|
1023
|
+
frictionLevel,
|
|
1024
|
+
// false-zero (F11/T261): read the RAW zombie_count — the loader defaults a
|
|
1025
|
+
// missing field to 0, erasing absent-vs-real-0. Present 0 → real 0; absent → null.
|
|
1026
|
+
zombieCount: rawManifest && Number.isFinite(rawManifest.zombie_count) ? rawManifest.zombie_count : null,
|
|
1027
|
+
waitingFor: ac.waitingFor || [],
|
|
1028
|
+
activeSessions: ac.activeSessions || [],
|
|
1029
|
+
backlog: ac.backlog || [],
|
|
1030
|
+
commitments: ac.commitments || [],
|
|
1031
|
+
// v6 SURFACE 5 — buried ideas, for the Say-it graveyard echo. Exposed
|
|
1032
|
+
// raw; the client extracts name/reason/date/cost (strict gate: no stanza
|
|
1033
|
+
// unless all parse).
|
|
1034
|
+
graveyard: (ac.graveyard || []).map(g => ({ text: g.text, raw: g.raw, label: g.label })),
|
|
1035
|
+
// Switched from ac.activePatterns (descriptions only, no structured
|
|
1036
|
+
// confidence/status) to al.activePatternsConf3Plus which carries the
|
|
1037
|
+
// typed fields from Auto_Learnings parser. Fixes CONF 0 bug where the
|
|
1038
|
+
// UI confidence badge always showed 0.
|
|
1039
|
+
activePatterns: (function () {
|
|
1040
|
+
const fromAL = (manifest && manifest._auto_learnings) || null; // not set, fall back
|
|
1041
|
+
if (Array.isArray(ac.__activePatternsTyped)) return ac.__activePatternsTyped;
|
|
1042
|
+
return (ac.activePatterns || []).map(p => ({
|
|
1043
|
+
id: p.id || p.name || "",
|
|
1044
|
+
description: p.description || p.text || "",
|
|
1045
|
+
confidence: p.confidence || 0,
|
|
1046
|
+
status: p.status || ""
|
|
1047
|
+
}));
|
|
1048
|
+
})()
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function phantomRowsForState(strategy, autoLearnings, manifest) {
|
|
1053
|
+
// Now delegates to phantom-strategy.js (built Phase 6.2). Pure function.
|
|
1054
|
+
try {
|
|
1055
|
+
return phantomStrategy.compute({ strategy, autoLearnings, manifest });
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
return [];
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// ─────────── Renderers ───────────
|
|
1062
|
+
|
|
1063
|
+
function renderSuggestedNow(suggestions, manifest) {
|
|
1064
|
+
if (!suggestions || suggestions.length === 0) {
|
|
1065
|
+
return `<div class="suggest-card" style="grid-column:span 3;">
|
|
1066
|
+
<div class="suggest-workflow">All clear</div>
|
|
1067
|
+
<div class="suggest-reason">No urgent workflows surface right now. Keep shipping.</div>
|
|
1068
|
+
<div class="suggest-meta"><span class="status-pill status-green">CLEAR</span><span class="suggest-score">—</span></div>
|
|
1069
|
+
</div>`;
|
|
1070
|
+
}
|
|
1071
|
+
return suggestions.map(s => `
|
|
1072
|
+
<div class="suggest-card" data-workflow="${escape(s.workflow)}" data-reason="${escape(s.reason)}">
|
|
1073
|
+
<div class="suggest-workflow">${escape(s.workflow)}</div>
|
|
1074
|
+
<div class="suggest-reason">${escape(s.reason)}</div>
|
|
1075
|
+
<div class="suggest-meta">
|
|
1076
|
+
<span class="status-pill status-${escape((s.urgency||"info").toLowerCase())}">${escape(s.urgency||"info")}</span>
|
|
1077
|
+
<span class="suggest-score">score ${s.score}</span>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div class="suggest-actions">
|
|
1080
|
+
<button class="suggest-action-btn" data-action="run" data-workflow="${escape(s.workflow)}">run</button>
|
|
1081
|
+
<button class="suggest-action-btn" data-action="snooze" data-workflow="${escape(s.workflow)}">snooze 4h</button>
|
|
1082
|
+
<button class="suggest-action-btn" data-action="skip" data-workflow="${escape(s.workflow)}">skip today</button>
|
|
1083
|
+
</div>
|
|
1084
|
+
</div>
|
|
1085
|
+
`).join("");
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function renderPatterns(al) {
|
|
1089
|
+
const list = (al && al.activePatternsConf3Plus) || [];
|
|
1090
|
+
if (list.length === 0) {
|
|
1091
|
+
return `<div class="pattern"><div class="pattern-name" style="color:var(--text-dim);">No active patterns at Confidence 3+</div></div>`;
|
|
1092
|
+
}
|
|
1093
|
+
return list.slice(0, 6).map(p => `
|
|
1094
|
+
<div class="pattern">
|
|
1095
|
+
<div class="pattern-head">
|
|
1096
|
+
<span class="pattern-name">${escape(p.name)}</span>
|
|
1097
|
+
<span class="pattern-conf pattern-conf-${p.confidence||0}">Conf ${p.confidence||"?"}</span>
|
|
1098
|
+
</div>
|
|
1099
|
+
<div class="pattern-meta">
|
|
1100
|
+
${p.status ? `<span>· ${escape(p.status.replace(/_\(.*$/,"").replace(/_/g," "))}</span>` : ""}
|
|
1101
|
+
${p.severity ? `<span>severity ${escape(p.severity)}</span>` : ""}
|
|
1102
|
+
${p.domain ? `<span>${escape(p.domain)}</span>` : ""}
|
|
1103
|
+
${p.evidence && p.evidence.length ? `<span>${p.evidence.length} evidence</span>` : ""}
|
|
1104
|
+
</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
`).join("");
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function renderHealthProtocols(hp) {
|
|
1110
|
+
if (!hp || !hp.available || !hp.protocols.length) return "";
|
|
1111
|
+
// For now render the first protocol's current phase as a checklist.
|
|
1112
|
+
// Multi-protocol UI can come later.
|
|
1113
|
+
const p = hp.protocols[0];
|
|
1114
|
+
if (!p.current) return "";
|
|
1115
|
+
const items = p.current.items;
|
|
1116
|
+
const checked = items.filter(i => i.checked).length;
|
|
1117
|
+
const total = items.length;
|
|
1118
|
+
const pct = total > 0 ? Math.round((checked / total) * 100) : 0;
|
|
1119
|
+
const phaseName = p.current.heading.replace(/^PHASE\s+\d+\s*—\s*/i, "").replace(/\s*\(.*\)\s*$/, "");
|
|
1120
|
+
const dayLabel = (p.daysRemaining !== null && p.daysIntoPhase !== null)
|
|
1121
|
+
? `Day ${p.daysIntoPhase + 1} · ${p.daysRemaining}d left`
|
|
1122
|
+
: "";
|
|
1123
|
+
const listHtml = items.slice(0, 8).map(i => `
|
|
1124
|
+
<div class="health-item ${i.checked ? "checked" : ""}">
|
|
1125
|
+
<span class="health-check">${i.checked ? "✓" : "○"}</span>
|
|
1126
|
+
<span class="health-text">${escape(i.text.replace(/\*\*/g, "").slice(0, 90))}</span>
|
|
1127
|
+
</div>
|
|
1128
|
+
`).join("");
|
|
1129
|
+
return `
|
|
1130
|
+
<div class="health-protocol">
|
|
1131
|
+
<div class="health-header">
|
|
1132
|
+
<span class="health-title">${escape(p.title)}</span>
|
|
1133
|
+
<span class="health-meta">${escape(phaseName)} · ${dayLabel}</span>
|
|
1134
|
+
</div>
|
|
1135
|
+
<div class="health-progress"><span style="width: ${pct}%"></span></div>
|
|
1136
|
+
<div class="health-progress-label">${checked}/${total} done</div>
|
|
1137
|
+
<div class="health-list">${listHtml}</div>
|
|
1138
|
+
</div>
|
|
1139
|
+
`;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function renderHealthProtocolEmpty() {
|
|
1143
|
+
return `
|
|
1144
|
+
<div class="health-protocol empty">
|
|
1145
|
+
<div class="health-header">
|
|
1146
|
+
<span class="health-title">No protocol loaded</span>
|
|
1147
|
+
<span class="health-meta">local markdown</span>
|
|
1148
|
+
</div>
|
|
1149
|
+
<div class="health-progress"><span style="width: 0%"></span></div>
|
|
1150
|
+
<div class="health-progress-label">0/0 done</div>
|
|
1151
|
+
<div class="health-list">
|
|
1152
|
+
<div class="health-item"><span class="health-check">○</span><span class="health-text">Add a protocol in 02_Areas/Health</span></div>
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
`;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function renderEngineHealth(eh) {
|
|
1159
|
+
if (!eh || !eh.items) return `<div class="engine-health-row"><span>—</span></div>`;
|
|
1160
|
+
return eh.items.map(i => `
|
|
1161
|
+
<div class="engine-health-row">
|
|
1162
|
+
<span class="file">${escape(i.file)}</span>
|
|
1163
|
+
<span class="age">${i.exists ? escape(i.label) : "missing"}</span>
|
|
1164
|
+
<span class="status-pill status-${i.status.toLowerCase()}">${escape(i.status)}</span>
|
|
1165
|
+
</div>
|
|
1166
|
+
`).join("");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function renderAgentsGrid(agents) {
|
|
1170
|
+
if (!agents || !agents.agents) return `<div class="agent-card"><span>—</span></div>`;
|
|
1171
|
+
return agents.agents.slice(0, 8).map(a => `
|
|
1172
|
+
<div class="agent-card${a.installed ? ' installed' : ''}">
|
|
1173
|
+
<span class="agent-name">${escape(a.name)}</span>
|
|
1174
|
+
<span class="agent-meta">${a.installed ? `${a.runnable ? "runnable" : "installed"}${a.hookSupport ? " · hooks" : ""}` : "not detected"}</span>
|
|
1175
|
+
<span class="agent-dot${a.installed ? ' installed' : ''}"></span>
|
|
1176
|
+
</div>
|
|
1177
|
+
`).join("");
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function renderHeatmap(daily) {
|
|
1181
|
+
if (!daily || !daily.heatmap) return "";
|
|
1182
|
+
return daily.heatmap.map(d => `<div class="heatmap-day${d.exists ? ' filled' : ''}" title="${d.date}${d.exists ? ' · ✓' : ''}"></div>`).join("");
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function renderProjects(scan) {
|
|
1186
|
+
if (!scan || !scan.projects || scan.projects.length === 0) return `<div class="project-row"><span class="project-name" style="color:var(--text-dim);">No active projects detected.</span></div>`;
|
|
1187
|
+
return scan.projects.map(p => {
|
|
1188
|
+
const total = p.totalTasks || 0;
|
|
1189
|
+
const done = (p.doneTasks || 0) + (p.unverifiedTasks || 0);
|
|
1190
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
1191
|
+
return `<div class="project-row">
|
|
1192
|
+
<span class="project-name">${escape(p.name)}</span>
|
|
1193
|
+
<div class="project-progress"><div class="project-progress-bar" style="width:${pct}%"></div></div>
|
|
1194
|
+
<span class="project-counts">${done}/${total} (${pct}%)</span>
|
|
1195
|
+
</div>`;
|
|
1196
|
+
}).join("");
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function renderGoals(g) {
|
|
1200
|
+
if (!g || !g.horizons || g.horizons.length === 0) return `<div class="goal-horizon"><div class="goal-horizon-title" style="color:var(--text-dim);">No goals defined.</div></div>`;
|
|
1201
|
+
return g.horizons.slice(0, 4).map(h => {
|
|
1202
|
+
const isExpired = h.targetDate && new Date(h.targetDate).getTime() < Date.now();
|
|
1203
|
+
return `<div class="goal-horizon">
|
|
1204
|
+
<div class="goal-horizon-title">${escape(h.title)}</div>
|
|
1205
|
+
${h.targetDate ? `<div class="goal-horizon-date">${escape(h.targetDate)}${isExpired ? ' · <span class="status-pill status-red">EXPIRED</span>' : ''}</div>` : ""}
|
|
1206
|
+
<ul class="goal-list">${h.goals.slice(0, 4).map(x => `<li>${escape(x)}</li>`).join("")}</ul>
|
|
1207
|
+
</div>`;
|
|
1208
|
+
}).join("");
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function renderLibraryCounts(lib) {
|
|
1212
|
+
if (!lib || !lib.sections) return `<div class="library-count-item"><span class="library-count-num">—</span><span class="library-count-label">library</span></div>`;
|
|
1213
|
+
return lib.sections.map(s => `
|
|
1214
|
+
<div class="library-count-item">
|
|
1215
|
+
<span class="library-count-num">${s.count}</span>
|
|
1216
|
+
<span class="library-count-label">${escape(s.name)}</span>
|
|
1217
|
+
</div>
|
|
1218
|
+
`).join("");
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function renderLibraryRecent(lib) {
|
|
1222
|
+
if (!lib || !lib.sections) return "no recent harvests";
|
|
1223
|
+
const recent = [];
|
|
1224
|
+
for (const s of lib.sections) {
|
|
1225
|
+
for (const r of (s.recent || [])) recent.push({ ...r, section: s.name });
|
|
1226
|
+
}
|
|
1227
|
+
recent.sort((a, b) => b.mtime - a.mtime);
|
|
1228
|
+
if (recent.length === 0) return "no recent harvests";
|
|
1229
|
+
return recent.slice(0, 5).map(r => `
|
|
1230
|
+
<div>· <strong>${escape(r.section)}</strong> · ${escape(r.name)} <span style="color:var(--text-muted);">${r.ageDays}d</span></div>
|
|
1231
|
+
`).join("");
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function renderEntities(lib) {
|
|
1235
|
+
if (!lib || !lib.entities) return `<div class="entity-block"><span>—</span></div>`;
|
|
1236
|
+
return lib.entities.map(e => {
|
|
1237
|
+
const recent = (e.recent || []).slice(0, 4).map(r => escape(r.name)).join(", ");
|
|
1238
|
+
return `<div class="entity-block">
|
|
1239
|
+
<div class="kicker-inline">${escape(e.name)} (${e.count})</div>
|
|
1240
|
+
<div class="entity-name-list">${recent || "no entries"}</div>
|
|
1241
|
+
</div>`;
|
|
1242
|
+
}).join("");
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function renderVoiceTraits(voiceDna) {
|
|
1246
|
+
const traits = [];
|
|
1247
|
+
if (voiceDna.lowercaseI) traits.push("lowercase 'i'");
|
|
1248
|
+
if (voiceDna.commaSpliced) traits.push("comma-spliced");
|
|
1249
|
+
if (voiceDna.usesBrother) traits.push('"brother"');
|
|
1250
|
+
if (voiceDna.urlInline) traits.push("url inline");
|
|
1251
|
+
if (voiceDna.totalSamples > 0) traits.push(`${voiceDna.shortestSample}-${voiceDna.longestSample} chars`);
|
|
1252
|
+
return traits.length > 0 ? traits.map(t => escape(t)).join(" · ") : "no traits extracted";
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function renderBulletList(items) {
|
|
1256
|
+
if (!items || items.length === 0) return `<div class="bullet" style="color:var(--text-muted);">empty</div>`;
|
|
1257
|
+
return items.slice(0, 6).map(i => {
|
|
1258
|
+
const cls = i.checked === true ? "checked" : "open";
|
|
1259
|
+
return `<div class="bullet ${cls}"><span class="text">${escape(i.text)}</span></div>`;
|
|
1260
|
+
}).join("");
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function renderOverrides(manifest) {
|
|
1264
|
+
if (!manifest || !manifest.overrideHistory) return `<div style="color:var(--text-muted);font-family:var(--font-mono);font-size:11px;">no recent overrides</div>`;
|
|
1265
|
+
const cutoff = Date.now() - 14 * 86400000;
|
|
1266
|
+
const recent = manifest.overrideHistory.filter(o => {
|
|
1267
|
+
if (!o.date) return false;
|
|
1268
|
+
const d = new Date(o.date);
|
|
1269
|
+
return !isNaN(d.getTime()) && d.getTime() >= cutoff;
|
|
1270
|
+
}).slice(0, 8);
|
|
1271
|
+
if (recent.length === 0) return `<div style="color:var(--text-muted);font-family:var(--font-mono);font-size:11px;">no overrides in last 14 days · L0 friction</div>`;
|
|
1272
|
+
return recent.map(o => `
|
|
1273
|
+
<div class="override-row">
|
|
1274
|
+
<span class="date">${escape(o.date)}</span>
|
|
1275
|
+
<span>${escape((o.what || "").slice(0, 40))}</span>
|
|
1276
|
+
<span>over: ${escape((o.over || "").slice(0, 40))}</span>
|
|
1277
|
+
<span style="color:var(--text-dim);">${escape((o.reason || "").slice(0, 80))}</span>
|
|
1278
|
+
</div>
|
|
1279
|
+
`).join("");
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function renderHookStats(hooks) {
|
|
1283
|
+
if (!hooks || !hooks.hookStats) return "—";
|
|
1284
|
+
const s = hooks.hookStats;
|
|
1285
|
+
return `
|
|
1286
|
+
<div class="hook-stat"><div class="hook-stat-name">/log</div><div class="hook-stat-num">${s.log}</div></div>
|
|
1287
|
+
<div class="hook-stat"><div class="hook-stat-name">session start</div><div class="hook-stat-num">${s.sessionStart}</div></div>
|
|
1288
|
+
<div class="hook-stat"><div class="hook-stat-name">engine health</div><div class="hook-stat-num">${s.engineHealth}</div></div>
|
|
1289
|
+
<div class="hook-stat"><div class="hook-stat-name">plan sync</div><div class="hook-stat-num">${s.planSync}</div></div>
|
|
1290
|
+
`;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function renderHookRecent(hooks) {
|
|
1294
|
+
if (!hooks || !hooks.recentMarkers || hooks.recentMarkers.length === 0) return "no hook firings in last 7 days";
|
|
1295
|
+
return hooks.recentMarkers.slice(0, 5).map(m => `
|
|
1296
|
+
<div>· ${escape(m.agent)} · ${escape(m.name.replace(/^mover_/, "").slice(0, 40))} · ${m.ageHours.toFixed(1)}h</div>
|
|
1297
|
+
`).join("");
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function renderDossier(d) {
|
|
1301
|
+
if (!d || !d.available) return `<div class="dossier-section">no dossier data</div>`;
|
|
1302
|
+
const sections = [
|
|
1303
|
+
{ title: "Skills", items: d.skills },
|
|
1304
|
+
{ title: "Capital", items: d.capital },
|
|
1305
|
+
{ title: "Audience", items: d.audience },
|
|
1306
|
+
{ title: "Network", items: d.network }
|
|
1307
|
+
].filter(s => s.items && s.items.length > 0);
|
|
1308
|
+
if (sections.length === 0) return `<div class="dossier-section">no dossier sections found</div>`;
|
|
1309
|
+
return sections.map(s => `
|
|
1310
|
+
<div class="dossier-section">
|
|
1311
|
+
<div class="dossier-section-title">${escape(s.title)}</div>
|
|
1312
|
+
${s.items.slice(0, 3).map(i => `<div>· ${escape(i)}</div>`).join("")}
|
|
1313
|
+
</div>
|
|
1314
|
+
`).join("");
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// ─────────── Helpers ───────────
|
|
1318
|
+
|
|
1319
|
+
function dailyCompletionString(daily) {
|
|
1320
|
+
if (!daily || !daily.today || !daily.today.exists) return "no daily note";
|
|
1321
|
+
const t = daily.today;
|
|
1322
|
+
if (!t.tasksTotal) return "no tasks";
|
|
1323
|
+
return `${t.tasksDone}/${t.tasksTotal} done`;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function extractFromAC(ac, keyRegex) {
|
|
1327
|
+
if (!ac) return null;
|
|
1328
|
+
const candidates = [];
|
|
1329
|
+
if (ac.sections) for (const sec of Object.values(ac.sections)) if (sec.fields) candidates.push(sec.fields);
|
|
1330
|
+
if (ac.fields) candidates.push(ac.fields);
|
|
1331
|
+
// Audit pass 2 (find-bugs #6): every current caller passes a literal
|
|
1332
|
+
// string, but the param is typed as `keyRegex` and would happily compile
|
|
1333
|
+
// a future user-controlled key. Escape regex metacharacters so a value
|
|
1334
|
+
// like `[*` from a parsed section header can't ReDoS or partial-match
|
|
1335
|
+
// unintended keys. Callers needing real regex behavior can opt-in with
|
|
1336
|
+
// a leading sentinel later if the use case appears.
|
|
1337
|
+
const safeKey = String(keyRegex).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1338
|
+
const re = new RegExp(safeKey, "i");
|
|
1339
|
+
for (const fields of candidates) for (const [k, v] of Object.entries(fields)) if (re.test(k)) return v;
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function recentOverrideCount(history, days) {
|
|
1344
|
+
if (!history || history.length === 0) return "0";
|
|
1345
|
+
const cutoff = Date.now() - days * 86400000;
|
|
1346
|
+
const c = history.filter(o => {
|
|
1347
|
+
if (!o.date) return false;
|
|
1348
|
+
const d = new Date(o.date);
|
|
1349
|
+
return !isNaN(d.getTime()) && d.getTime() >= cutoff;
|
|
1350
|
+
}).length;
|
|
1351
|
+
return String(c);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function humanizeTimeAgo(then, now) {
|
|
1355
|
+
if (!then || !now) return "—";
|
|
1356
|
+
const ms = now.getTime() - then.getTime();
|
|
1357
|
+
const min = Math.round(ms / 60000);
|
|
1358
|
+
if (min < 1) return "now";
|
|
1359
|
+
if (min < 60) return `${min}m`;
|
|
1360
|
+
const hr = Math.round(min / 60);
|
|
1361
|
+
if (hr < 24) return `${hr}h`;
|
|
1362
|
+
const day = Math.round(hr / 24);
|
|
1363
|
+
return `${day}d`;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function shortBattery(ac) {
|
|
1367
|
+
const v = extractFromAC(ac, "battery_state") || extractFromAC(ac, "battery");
|
|
1368
|
+
if (!v) return null;
|
|
1369
|
+
return v.split(/[(,]/)[0].trim();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function shortZone(ac) {
|
|
1373
|
+
const v = extractFromAC(ac, "discipline_zone") || extractFromAC(ac, "zone");
|
|
1374
|
+
if (!v) return null;
|
|
1375
|
+
const m = v.match(/^([0-9]+)/);
|
|
1376
|
+
return m ? m[1] : v.split(/[(,]/)[0].trim();
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function shortOscillation(ac) {
|
|
1380
|
+
const v = extractFromAC(ac, "oscillation_trend") || extractFromAC(ac, "oscillation");
|
|
1381
|
+
if (!v) return null;
|
|
1382
|
+
return v.split(/[(,—]/)[0].trim().slice(0, 24);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function shortenForMetric(text, maxLen = 60) {
|
|
1386
|
+
if (!text) return null;
|
|
1387
|
+
const cleaned = String(text).replace(/\s+/g, " ").trim();
|
|
1388
|
+
if (cleaned.length <= maxLen) return cleaned;
|
|
1389
|
+
return cleaned.slice(0, maxLen - 1) + "…";
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function shortenForCallout(text) {
|
|
1393
|
+
if (!text) return "—";
|
|
1394
|
+
// First sentence
|
|
1395
|
+
const cleaned = String(text).replace(/\s+/g, " ").trim();
|
|
1396
|
+
const firstSentence = cleaned.split(/(?<=\.)\s/)[0];
|
|
1397
|
+
return firstSentence.slice(0, 100);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function extractFirstSentences(text, n) {
|
|
1401
|
+
if (!text) return "";
|
|
1402
|
+
const cleaned = text.replace(/^[#*\-]+\s*/gm, "").replace(/\n+/g, " ").trim();
|
|
1403
|
+
const parts = cleaned.split(/(?<=[.!?])\s+/);
|
|
1404
|
+
return parts.slice(0, n).join(" ").slice(0, 280);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function computeTodayCall({ ac, manifest, engineHealth, friction }) {
|
|
1408
|
+
const fLevel = parseInt(String(friction || "L0").replace(/[^\d]/g, ""), 10) || 0;
|
|
1409
|
+
// null = health scan unavailable — NOT "OK". Don't claim "All clear" from
|
|
1410
|
+
// absence. [false-zero — Codex P1, T261]
|
|
1411
|
+
const ehStatus = engineHealth ? engineHealth.overallStatus : null;
|
|
1412
|
+
if (ehStatus === "RED" || fLevel >= 3) {
|
|
1413
|
+
return {
|
|
1414
|
+
class: "red", status: "RED",
|
|
1415
|
+
text: "Engine drift detected. Address what your AI keeps catching before adding more output."
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
if (ehStatus === "YELLOW" || fLevel >= 1) {
|
|
1419
|
+
return {
|
|
1420
|
+
class: "amber", status: "AMBER",
|
|
1421
|
+
text: "Caution. Some signals stale or friction firing. Run a workflow before deeper work."
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
if (ehStatus == null) {
|
|
1425
|
+
return {
|
|
1426
|
+
class: "amber", status: "UNKNOWN",
|
|
1427
|
+
text: "Engine health unavailable — can't read your Engine files to confirm the brief is clear."
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
return {
|
|
1431
|
+
class: "green", status: "OK",
|
|
1432
|
+
text: "All clear. Push hard on the Single Test."
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function computeStudioVerdict({ todayCall, manifest, engineHealth, daysToTest, strategy }) {
|
|
1437
|
+
const fLevel = parseInt(String((manifest && manifest.frictionLevel) || "L0").replace(/[^\d]/g, ""), 10) || 0;
|
|
1438
|
+
// null = health scan unavailable — NOT "OK". [false-zero — Codex P1, T261]
|
|
1439
|
+
const engineStatus = engineHealth ? engineHealth.overallStatus : null;
|
|
1440
|
+
const zombieCount = manifest ? (manifest.zombieCount || 0) : 0;
|
|
1441
|
+
const test = manifest && manifest.singleTest ? manifest.singleTest : (strategy && strategy.singleTest ? strategy.singleTest : "the current Single Test");
|
|
1442
|
+
const days = daysToTest == null ? "the deadline" : `${daysToTest}d`;
|
|
1443
|
+
|
|
1444
|
+
if (engineStatus === "RED" || fLevel >= 3) {
|
|
1445
|
+
return {
|
|
1446
|
+
title: zombieCount > 0
|
|
1447
|
+
? `You're drifting. ${zombieCount} zombie ${zombieCount === 1 ? "task is" : "tasks are"} still open.`
|
|
1448
|
+
: "You're drifting. The system is catching repeated avoidance.",
|
|
1449
|
+
subline: `${todayCall.text} ${shortenForMetric(test, 86)} is ${days} away.`
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (engineStatus === "YELLOW" || fLevel >= 1) {
|
|
1454
|
+
return {
|
|
1455
|
+
title: "Caution. The brief found friction before output.",
|
|
1456
|
+
subline: `${todayCall.text} Run the workflow before opening another build surface.`
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (engineStatus == null) {
|
|
1461
|
+
return {
|
|
1462
|
+
title: "Brief incomplete — engine health unavailable.",
|
|
1463
|
+
subline: `${todayCall.text} Can't confirm a clear brief without reading your Engine files.`
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
return {
|
|
1468
|
+
title: "Clear brief. Push the Single Test.",
|
|
1469
|
+
subline: `${todayCall.text} The next move is distribution, not more system surface.`
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function computeDaysToTarget(strategy, manifest) {
|
|
1474
|
+
// Try strategy.targetDate first
|
|
1475
|
+
let date = null;
|
|
1476
|
+
if (strategy && strategy.targetDate) {
|
|
1477
|
+
const d = new Date(strategy.targetDate);
|
|
1478
|
+
if (!isNaN(d.getTime())) date = d;
|
|
1479
|
+
}
|
|
1480
|
+
// Try to extract date from singleTest text e.g. "by May 15"
|
|
1481
|
+
if (!date && manifest && manifest.singleTest) {
|
|
1482
|
+
const m = manifest.singleTest.match(/by ([A-Za-z]+ \d+)(?:,? (\d{4}))?/);
|
|
1483
|
+
if (m) {
|
|
1484
|
+
const year = m[2] || new Date().getFullYear();
|
|
1485
|
+
const d = new Date(`${m[1]}, ${year}`);
|
|
1486
|
+
if (!isNaN(d.getTime())) date = d;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (!date) return null;
|
|
1490
|
+
return Math.max(0, Math.ceil((date.getTime() - Date.now()) / 86400000));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function computeTodayProgress(daily) {
|
|
1494
|
+
if (!daily || !daily.today || !daily.today.exists) return { pct: 0, ringOffset: 0 };
|
|
1495
|
+
const t = daily.today;
|
|
1496
|
+
if (!t.tasksTotal) return { pct: 0, ringOffset: 0 };
|
|
1497
|
+
const pct = Math.round((t.tasksDone / t.tasksTotal) * 100);
|
|
1498
|
+
const circumference = 2 * Math.PI * 70; // matches r=70 in template
|
|
1499
|
+
const offset = Math.round((pct / 100) * circumference);
|
|
1500
|
+
return { pct: String(pct), ringOffset: offset };
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
module.exports = { build };
|