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,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Activation event log — local-only, append-only, telemetry-never.
|
|
4
|
+
//
|
|
5
|
+
// V5.0 scaffold (per Phase 41.0 in dev/plan.md). Records when a user
|
|
6
|
+
// actually USES Mover OS so V5 features can be measured against real
|
|
7
|
+
// behavior instead of founder intuition.
|
|
8
|
+
//
|
|
9
|
+
// Codex blind-spot fix (2026-05-24 gpt-5 review): an activation-based goal
|
|
10
|
+
// (e.g. "N activated installs by <date>") needs "activated" operationalized.
|
|
11
|
+
// Without local activation signals, every activation feature is a guess
|
|
12
|
+
// about what causes activation.
|
|
13
|
+
//
|
|
14
|
+
// Constraints:
|
|
15
|
+
// - LOCAL ONLY. File at ~/.mover/activation-events.jsonl. Never sent
|
|
16
|
+
// anywhere. Respects the dashboard's "TELEMETRY NEVER" promise.
|
|
17
|
+
// - APPEND ONLY. One event per line (jsonl). New writes never modify
|
|
18
|
+
// existing lines.
|
|
19
|
+
// - SIZE CAPPED. 10MB ceiling; oldest events rotated to .jsonl.bak.
|
|
20
|
+
// - BEST EFFORT. Failures (disk full, perms) log to console.warn and
|
|
21
|
+
// return silently. Activation logging must never block the server.
|
|
22
|
+
//
|
|
23
|
+
// Event shape:
|
|
24
|
+
// {
|
|
25
|
+
// "ts": "2026-05-24T02:30:00.000Z", // ISO
|
|
26
|
+
// "type": "dashboard_open", // enum (see EVENT_TYPES)
|
|
27
|
+
// "version": "4.7.7", // package version at time of event
|
|
28
|
+
// "session": "pid-12345", // process id, lets us de-dupe within a session
|
|
29
|
+
// "meta": { ... } // optional, per-event payload
|
|
30
|
+
// }
|
|
31
|
+
|
|
32
|
+
const fs = require("fs");
|
|
33
|
+
const path = require("path");
|
|
34
|
+
const os = require("os");
|
|
35
|
+
|
|
36
|
+
// Allowed event types. Adding a new type? Extend this list AND update
|
|
37
|
+
// the readStats() aggregation in build.js when Phase 41.0.4 wires it.
|
|
38
|
+
const EVENT_TYPES = Object.freeze([
|
|
39
|
+
"dashboard_open", // /api/health hit (first time per process)
|
|
40
|
+
"workflow_run", // /api/workflow/run or /api/workflow/open-in-terminal called
|
|
41
|
+
"log_completed", // /log workflow finished (Stop hook signal)
|
|
42
|
+
"onboarding_finished", // first-run flow completed
|
|
43
|
+
"file_edit_in_obsidian", // user clicked obsidian:// deep link
|
|
44
|
+
"referral_mentioned", // user mentioned someone else installed (manual capture)
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const MAX_FILE_BYTES = 10 * 1024 * 1024; // 10MB
|
|
48
|
+
const ROTATION_SUFFIX = ".bak";
|
|
49
|
+
|
|
50
|
+
function logFilePath() {
|
|
51
|
+
const home = os.homedir();
|
|
52
|
+
return path.join(home, ".mover", "activation-events.jsonl");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rotationPath() {
|
|
56
|
+
return logFilePath() + ROTATION_SUFFIX;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ensureLogDir() {
|
|
60
|
+
const dir = path.dirname(logFilePath());
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
} catch (_) {
|
|
64
|
+
// Best effort. Caller will see the appendFileSync error if dir really
|
|
65
|
+
// can't be created.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function rotateIfNeeded(pendingBytes = 0) {
|
|
70
|
+
// Audit H4: TOCTOU race between stat and rename across concurrent processes.
|
|
71
|
+
// We can't fully eliminate it without a file lock, but we can ensure the
|
|
72
|
+
// failure mode is "no-op" instead of "lose data" by catching ENOENT on
|
|
73
|
+
// rename (means another process already rotated). Returns true if a
|
|
74
|
+
// rotation succeeded so the caller can retry the write to a fresh file.
|
|
75
|
+
//
|
|
76
|
+
// Audit pass 2 #10: include `pendingBytes` so a write that would push
|
|
77
|
+
// the file past the cap rotates first instead of overshooting. This
|
|
78
|
+
// makes 10MB the actual ceiling, not a soft target.
|
|
79
|
+
try {
|
|
80
|
+
const st = fs.statSync(logFilePath());
|
|
81
|
+
if (st.size + pendingBytes > MAX_FILE_BYTES) {
|
|
82
|
+
// Pass 3 #4 regression fix: previous version read CURRENT + OLD into
|
|
83
|
+
// archive THEN renamed CURRENT → .bak. That left CURRENT both in the
|
|
84
|
+
// new .bak (from rename) AND in archive — duplicating data and
|
|
85
|
+
// re-duplicating on each subsequent rotation.
|
|
86
|
+
//
|
|
87
|
+
// Correct order: read the about-to-be-clobbered OLD .bak into
|
|
88
|
+
// archive FIRST (preserving prior generations), THEN rename so
|
|
89
|
+
// CURRENT becomes the new .bak. CURRENT only ends up in .bak (once),
|
|
90
|
+
// OLD only ends up in archive (once).
|
|
91
|
+
try {
|
|
92
|
+
if (fs.existsSync(rotationPath())) {
|
|
93
|
+
const archive = rotationPath() + ".archive";
|
|
94
|
+
const oldBak = fs.readFileSync(rotationPath(), "utf8");
|
|
95
|
+
fs.appendFileSync(archive, oldBak, { encoding: "utf8" });
|
|
96
|
+
}
|
|
97
|
+
} catch (_) { /* non-fatal */ }
|
|
98
|
+
try {
|
|
99
|
+
fs.renameSync(logFilePath(), rotationPath());
|
|
100
|
+
return true;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err && err.code === "ENOENT") {
|
|
103
|
+
// Another process already rotated — fine, our future appendFileSync
|
|
104
|
+
// will recreate the file.
|
|
105
|
+
return true;
|
|
106
|
+
} else if (err && err.code === "EBUSY") {
|
|
107
|
+
// Windows file lock contention — skip this write rather than
|
|
108
|
+
// appending to an oversized log unbounded (audit pass 2 #11).
|
|
109
|
+
// The caller's appendFileSync will get EBUSY too and skip cleanly.
|
|
110
|
+
return false;
|
|
111
|
+
} else {
|
|
112
|
+
// Other errors are unexpected but non-fatal.
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (_) {
|
|
118
|
+
// File doesn't exist yet — nothing to rotate.
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function packageVersion() {
|
|
124
|
+
try {
|
|
125
|
+
const pkg = require("../../../package.json");
|
|
126
|
+
return pkg.version || "unknown";
|
|
127
|
+
} catch {
|
|
128
|
+
return "unknown";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Write one event. Best-effort. Never throws.
|
|
133
|
+
//
|
|
134
|
+
// type: one of EVENT_TYPES
|
|
135
|
+
// meta: optional object (will be JSON-stringified)
|
|
136
|
+
//
|
|
137
|
+
// Returns true if write succeeded, false otherwise.
|
|
138
|
+
function logEvent(type, meta) {
|
|
139
|
+
if (!EVENT_TYPES.includes(type)) {
|
|
140
|
+
console.warn(`[activation-log] unknown event type: ${type}`);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
// Sanitize meta (code-bug-audit M9): handle circular refs + reject huge
|
|
144
|
+
// payloads. JSON.stringify throws on circular refs; we don't want the
|
|
145
|
+
// logEvent call to lose the event because the caller passed an Error
|
|
146
|
+
// with a .cause chain.
|
|
147
|
+
let safeMeta;
|
|
148
|
+
if (meta && typeof meta === "object") {
|
|
149
|
+
try {
|
|
150
|
+
const json = JSON.stringify(meta);
|
|
151
|
+
// Cap at 16KB — beyond that, the event is metadata-dump territory
|
|
152
|
+
// and not useful for activation analysis. Replace with marker.
|
|
153
|
+
if (json && json.length <= 16 * 1024) {
|
|
154
|
+
safeMeta = JSON.parse(json);
|
|
155
|
+
} else {
|
|
156
|
+
safeMeta = { error: "meta-too-large", size: json.length };
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
safeMeta = { error: "unserializable-meta" };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const event = {
|
|
163
|
+
ts: new Date().toISOString(),
|
|
164
|
+
type,
|
|
165
|
+
version: packageVersion(),
|
|
166
|
+
session: `pid-${process.pid}`,
|
|
167
|
+
...(safeMeta ? { meta: safeMeta } : {}),
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
ensureLogDir();
|
|
171
|
+
const line = JSON.stringify(event) + "\n";
|
|
172
|
+
// Pass 3 #7 fix: previously appendFileSync ran even when
|
|
173
|
+
// rotateIfNeeded couldn't rotate (EBUSY, perms) — the file grew
|
|
174
|
+
// unbounded past the 10MB cap. Now: check whether rotation was
|
|
175
|
+
// needed AND succeeded. If a rotation was needed but failed, skip
|
|
176
|
+
// the write so the cap holds.
|
|
177
|
+
const sizeBeforeWrite = (() => {
|
|
178
|
+
try { return fs.statSync(logFilePath()).size; } catch (_) { return 0; }
|
|
179
|
+
})();
|
|
180
|
+
const pending = Buffer.byteLength(line, "utf8");
|
|
181
|
+
const willOvershoot = sizeBeforeWrite + pending > MAX_FILE_BYTES;
|
|
182
|
+
if (willOvershoot) {
|
|
183
|
+
const rotated = rotateIfNeeded(pending);
|
|
184
|
+
if (!rotated) {
|
|
185
|
+
// Couldn't rotate. Skip rather than break the cap.
|
|
186
|
+
console.warn("[activation-log] skipping write — cap reached and rotation failed");
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Always call (cheap no-op if no rotation needed) so caller stays
|
|
191
|
+
// simple.
|
|
192
|
+
rotateIfNeeded(pending);
|
|
193
|
+
}
|
|
194
|
+
fs.appendFileSync(logFilePath(), line, { encoding: "utf8" });
|
|
195
|
+
return true;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.warn(`[activation-log] write failed: ${err.message}`);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Read all events. Returns array of parsed event objects. Never throws.
|
|
203
|
+
// Lines that fail to parse are silently skipped (forward-compat: future
|
|
204
|
+
// event shapes won't break the reader).
|
|
205
|
+
function readEvents(opts = {}) {
|
|
206
|
+
const { sinceIso, types, limit } = opts;
|
|
207
|
+
let raw;
|
|
208
|
+
try {
|
|
209
|
+
raw = fs.readFileSync(logFilePath(), "utf8");
|
|
210
|
+
} catch (_) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
const since = sinceIso ? Date.parse(sinceIso) : 0;
|
|
214
|
+
const typeFilter = Array.isArray(types) && types.length ? new Set(types) : null;
|
|
215
|
+
const events = [];
|
|
216
|
+
let corruptLines = 0;
|
|
217
|
+
const lines = raw.split("\n");
|
|
218
|
+
// Walk from end (newest) so `limit` returns most recent N efficiently.
|
|
219
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
220
|
+
const line = lines[i];
|
|
221
|
+
if (!line) continue;
|
|
222
|
+
let ev;
|
|
223
|
+
try { ev = JSON.parse(line); } catch { corruptLines += 1; continue; }
|
|
224
|
+
if (typeFilter && !typeFilter.has(ev.type)) continue;
|
|
225
|
+
if (since && ev.ts && Date.parse(ev.ts) < since) continue;
|
|
226
|
+
events.push(ev);
|
|
227
|
+
if (limit && events.length >= limit) break;
|
|
228
|
+
}
|
|
229
|
+
events.reverse(); // chronological order
|
|
230
|
+
// Audit pass 2 #12: expose corruption count so the Briefing tile can
|
|
231
|
+
// distinguish "day 0" from "log file damaged." Attach as a sentinel
|
|
232
|
+
// on the array (non-enumerable so iteration is unaffected).
|
|
233
|
+
Object.defineProperty(events, "corruptLines", { value: corruptLines, enumerable: false });
|
|
234
|
+
return events;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Aggregate stats for the dashboard's state.json. Cheap to compute on
|
|
238
|
+
// every state rebuild (~once per request). Used by Phase 41.0.4 +
|
|
239
|
+
// 41.0.5 to render the Briefing activation tile.
|
|
240
|
+
function readStats() {
|
|
241
|
+
const events = readEvents();
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const week = 7 * 86400000;
|
|
244
|
+
const day = 86400000;
|
|
245
|
+
let firstEventTs = null;
|
|
246
|
+
let dashboardOpens = 0;
|
|
247
|
+
let workflowRuns = 0;
|
|
248
|
+
let dashboardOpens7d = 0;
|
|
249
|
+
let workflowRuns7d = 0;
|
|
250
|
+
let logCompletions7d = 0;
|
|
251
|
+
const distinctOpenDays = new Set();
|
|
252
|
+
for (const ev of events) {
|
|
253
|
+
const tMs = ev.ts ? Date.parse(ev.ts) : 0;
|
|
254
|
+
if (!firstEventTs || tMs < firstEventTs) firstEventTs = tMs;
|
|
255
|
+
if (ev.type === "dashboard_open") {
|
|
256
|
+
dashboardOpens += 1;
|
|
257
|
+
if (now - tMs <= week) dashboardOpens7d += 1;
|
|
258
|
+
if (ev.ts) distinctOpenDays.add(ev.ts.slice(0, 10));
|
|
259
|
+
} else if (ev.type === "workflow_run") {
|
|
260
|
+
workflowRuns += 1;
|
|
261
|
+
if (now - tMs <= week) workflowRuns7d += 1;
|
|
262
|
+
} else if (ev.type === "log_completed") {
|
|
263
|
+
if (now - tMs <= week) logCompletions7d += 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const installDays = firstEventTs ? Math.max(0, Math.round((now - firstEventTs) / day)) : 0;
|
|
267
|
+
// Activation definition (proposed): 2+ dashboard opens on separate days
|
|
268
|
+
// AND 1+ workflow run. Phase 41.0.6 will codify this in
|
|
269
|
+
// dev/V5_ACTIVATION_METRICS.md and test against the configured target.
|
|
270
|
+
const activated = distinctOpenDays.size >= 2 && workflowRuns >= 1;
|
|
271
|
+
return {
|
|
272
|
+
available: events.length > 0,
|
|
273
|
+
firstEventAt: firstEventTs ? new Date(firstEventTs).toISOString() : null,
|
|
274
|
+
installDays,
|
|
275
|
+
dashboardOpens,
|
|
276
|
+
dashboardOpens7d,
|
|
277
|
+
workflowRuns,
|
|
278
|
+
workflowRuns7d,
|
|
279
|
+
logCompletions7d,
|
|
280
|
+
distinctOpenDays: distinctOpenDays.size,
|
|
281
|
+
activated,
|
|
282
|
+
// Audit pass 2 #12: surfaces JSONL corruption to the Briefing tile.
|
|
283
|
+
// Non-zero => the log file has malformed lines (mid-write crash,
|
|
284
|
+
// editor edit, etc.); UI can show a repair hint rather than treat
|
|
285
|
+
// corruption as "no events."
|
|
286
|
+
corruptLines: events.corruptLines || 0,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
EVENT_TYPES,
|
|
292
|
+
logEvent,
|
|
293
|
+
readEvents,
|
|
294
|
+
readStats,
|
|
295
|
+
// Exported for tests:
|
|
296
|
+
__logFilePath: logFilePath,
|
|
297
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { activeContextPath, safeRead, safeStat } = require("./paths");
|
|
4
|
+
|
|
5
|
+
// Forgiving Active_Context.md parser. Returns {available, lastUpdated, sections{...}, fields{...}}.
|
|
6
|
+
// Never throws. On any error, returns {available:false, error}.
|
|
7
|
+
|
|
8
|
+
function parseActiveContext(vault) {
|
|
9
|
+
const filePath = activeContextPath(vault);
|
|
10
|
+
const stat = safeStat(filePath);
|
|
11
|
+
const text = safeRead(filePath);
|
|
12
|
+
if (!text) return { available: false, error: "Active_Context.md not found", filePath };
|
|
13
|
+
|
|
14
|
+
const result = {
|
|
15
|
+
available: true,
|
|
16
|
+
filePath,
|
|
17
|
+
lastUpdated: stat ? stat.mtime : null,
|
|
18
|
+
raw: text,
|
|
19
|
+
sectionsRaw: {},
|
|
20
|
+
sections: {},
|
|
21
|
+
fields: {}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Top-of-file fields like **Last Updated:** YYYY-MM-DD or "log_last_run: YYYY-MM-DD"
|
|
25
|
+
// Extract before first ## header
|
|
26
|
+
const firstHeader = text.search(/^## /m);
|
|
27
|
+
const preface = firstHeader > 0 ? text.slice(0, firstHeader) : text;
|
|
28
|
+
const fieldRe = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
|
|
29
|
+
let m;
|
|
30
|
+
while ((m = fieldRe.exec(preface)) !== null) {
|
|
31
|
+
const k = m[1].trim().toLowerCase().replace(/\s+/g, "_");
|
|
32
|
+
result.fields[k] = m[2].trim();
|
|
33
|
+
}
|
|
34
|
+
// Plain field lines like "log_last_run: 2026-05-06T13:19"
|
|
35
|
+
const plainRe = /^([a-z_]+):\s*([^\n]+)$/gm;
|
|
36
|
+
while ((m = plainRe.exec(preface)) !== null) {
|
|
37
|
+
const k = m[1].trim();
|
|
38
|
+
if (!result.fields[k]) result.fields[k] = m[2].trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Split file into ## sections
|
|
42
|
+
const lines = text.split("\n");
|
|
43
|
+
let current = null;
|
|
44
|
+
let buf = [];
|
|
45
|
+
const sections = [];
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const h = line.match(/^##\s+(.+?)\s*$/);
|
|
48
|
+
if (h) {
|
|
49
|
+
if (current) sections.push({ title: current, body: buf.join("\n") });
|
|
50
|
+
current = h[1];
|
|
51
|
+
buf = [];
|
|
52
|
+
} else if (current) {
|
|
53
|
+
buf.push(line);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (current) sections.push({ title: current, body: buf.join("\n") });
|
|
57
|
+
|
|
58
|
+
for (const sec of sections) {
|
|
59
|
+
result.sectionsRaw[sec.title] = sec.body;
|
|
60
|
+
// Try to extract bold-key fields per section
|
|
61
|
+
const fields = {};
|
|
62
|
+
const fr = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
|
|
63
|
+
while ((m = fr.exec(sec.body)) !== null) {
|
|
64
|
+
const k = m[1].trim().toLowerCase().replace(/\s+/g, "_");
|
|
65
|
+
fields[k] = m[2].trim();
|
|
66
|
+
}
|
|
67
|
+
// Plain key: value lines
|
|
68
|
+
const pf = /^([a-z_]+):\s*([^\n]+)$/gm;
|
|
69
|
+
while ((m = pf.exec(sec.body)) !== null) {
|
|
70
|
+
const k = m[1].trim();
|
|
71
|
+
if (!fields[k]) fields[k] = m[2].trim();
|
|
72
|
+
}
|
|
73
|
+
result.sectionsRaw[sec.title] = sec.body;
|
|
74
|
+
result.sections[sec.title] = { fields, body: sec.body };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Helpful named accessors for known sections
|
|
78
|
+
result.findSection = (needle) => {
|
|
79
|
+
const lower = String(needle).toLowerCase();
|
|
80
|
+
for (const [title, sec] of Object.entries(result.sections)) {
|
|
81
|
+
if (title.toLowerCase().includes(lower)) return { title, ...sec };
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
result.workflowState = (() => {
|
|
87
|
+
const ws = result.findSection("workflow state");
|
|
88
|
+
if (ws && ws.fields) return ws.fields;
|
|
89
|
+
// Fallback: top-of-file fields
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const k of ["log_last_run", "analyse_day_last_run", "weekly_review_last_run", "pivot_strategy_last_run", "monthly_review_last_run", "review_week_day", "friction_escalation"]) {
|
|
92
|
+
if (result.fields[k]) out[k] = result.fields[k];
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
})();
|
|
96
|
+
|
|
97
|
+
result.engineHealth = (() => {
|
|
98
|
+
const eh = result.findSection("engine health");
|
|
99
|
+
if (!eh) return null;
|
|
100
|
+
return eh.fields || {};
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
// Bullet items from S8 BACKLOG, S3.5 WAITING FOR, S8.5 COMMITMENTS.
|
|
104
|
+
// T242 bug hunt fix (C#3/C#4): the parser emitted {label} but the UI
|
|
105
|
+
// (PromiseWall, ExecutionCockpit, WeeklyLearnings) read {from, to,
|
|
106
|
+
// expected} — different field names per list semantics. Alias the
|
|
107
|
+
// bold-name capture into the right field per list:
|
|
108
|
+
// commitments[].to = who we owe
|
|
109
|
+
// waitingFor[].from = who we're waiting on
|
|
110
|
+
// waitingFor[].expected = same as due (when expected)
|
|
111
|
+
result.backlog = parseBulletList(result.findSection("BACKLOG") || result.findSection("Backlog"), "backlog");
|
|
112
|
+
result.waitingFor = parseBulletList(result.findSection("WAITING FOR") || result.findSection("Waiting For"), "waitingFor");
|
|
113
|
+
result.commitments = parseBulletList(result.findSection("COMMITMENTS") || result.findSection("Commitments"), "commitments");
|
|
114
|
+
result.graveyard = parseBulletList(result.findSection("GRAVEYARD") || result.findSection("Graveyard"));
|
|
115
|
+
result.activeSessions = parseBulletList(result.findSection("ACTIVE SESSIONS") || result.findSection("Active Sessions"));
|
|
116
|
+
result.activePatterns = parseBulletList(result.findSection("ACTIVE PATTERNS") || result.findSection("Active Patterns"));
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseBulletList(section, kind) {
|
|
122
|
+
if (!section || !section.body) return [];
|
|
123
|
+
const lines = section.body.split("\n");
|
|
124
|
+
const items = [];
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const m = line.match(/^[-*]\s*\[(.)\]\s*(.+)$/) || line.match(/^[-*]\s+(.+)$/);
|
|
127
|
+
if (m) {
|
|
128
|
+
if (m.length === 3) {
|
|
129
|
+
const checked = m[1] === "x" || m[1] === "X";
|
|
130
|
+
const text = m[2].trim();
|
|
131
|
+
const meta = extractCommitmentMeta(text);
|
|
132
|
+
// Alias label → kind-appropriate role field. Keep label too for
|
|
133
|
+
// legacy consumers; UIs already reading {label} get no change.
|
|
134
|
+
if (meta.label) {
|
|
135
|
+
if (kind === "commitments") meta.to = meta.to || meta.label;
|
|
136
|
+
else if (kind === "waitingFor" || kind === "backlog") meta.from = meta.from || meta.label;
|
|
137
|
+
}
|
|
138
|
+
// For waitingFor, the UI uses .expected (not .due). Alias.
|
|
139
|
+
if (kind === "waitingFor" && meta.due) meta.expected = meta.expected || meta.due;
|
|
140
|
+
items.push({
|
|
141
|
+
checked,
|
|
142
|
+
done: checked,
|
|
143
|
+
text,
|
|
144
|
+
raw: line.trim(),
|
|
145
|
+
...meta,
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
items.push({ checked: null, done: null, text: m[1].trim(), raw: line.trim() });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return items;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A1 fix supplement: pull party/date/added from free-form commitment prose
|
|
156
|
+
// so the PromiseWall row layout has real data to render instead of `—` and
|
|
157
|
+
// `0d open`. Conservative — only emit fields when a pattern matches.
|
|
158
|
+
function extractCommitmentMeta(text) {
|
|
159
|
+
const meta = {};
|
|
160
|
+
// Date added → "[Added: 2026-05-15]" or "[Added: 2026-05-15T20:05]"
|
|
161
|
+
const addedM = text.match(/\[Added:\s*([\d\-T:]{8,20})\]/i);
|
|
162
|
+
if (addedM) meta.added = addedM[1];
|
|
163
|
+
// Due date → "due May 28", "due 2026-05-28", "by Thursday May 1"
|
|
164
|
+
const dueIso = text.match(/\b(?:due|by)\s+(20\d{2}-[01]\d-[0-3]\d)\b/i);
|
|
165
|
+
const dueLoose = text.match(/\b(?:due|by)\s+([A-Z][a-z]+\s+\d{1,2})(?:[,\s]+(20\d{2}))?/);
|
|
166
|
+
if (dueIso) meta.due = dueIso[1];
|
|
167
|
+
else if (dueLoose) meta.due = dueLoose[2] ? `${dueLoose[1]} ${dueLoose[2]}` : dueLoose[1];
|
|
168
|
+
// Bold-quoted label → "**Thing name** — rest of description". Tolerate a
|
|
169
|
+
// leading 💀/emoji + whitespace (graveyard entries lead with a skull) before
|
|
170
|
+
// the bold label so `label` populates for graveyard rows too (v6 W1-S2).
|
|
171
|
+
const labelText = text.replace(/^[\s\u{1F000}-\u{1FAFF}☀-➿←-⇿️]+/u, "");
|
|
172
|
+
const labelM = labelText.match(/^\*\*([^*]+)\*\*/);
|
|
173
|
+
if (labelM) meta.label = labelM[1].trim();
|
|
174
|
+
return meta;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { parseActiveContext };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// agent-command.js — one-shot agent argv assembly for /api/workflow/run and
|
|
2
|
+
// /api/agent/run (extracted from server.js so the provider matrix is pure and
|
|
3
|
+
// unit-testable; the RunRegistry spawn stays a passthrough).
|
|
4
|
+
//
|
|
5
|
+
// #112 provider/model/effort control. Every flag below was probed against the
|
|
6
|
+
// binaries installed on this machine on 2026-07-02 (`claude --help`,
|
|
7
|
+
// `codex exec --help`, `gemini --help`), not recalled from training data:
|
|
8
|
+
// claude --model <m> --effort <low|medium|high|xhigh|max>
|
|
9
|
+
// codex -m <m> -c model_reasoning_effort="<v>" (TOML override)
|
|
10
|
+
// gemini -m <m> --approval-mode <default|auto_edit|yolo|plan>, no effort
|
|
11
|
+
// Gemini's old hardcoded --yolo becomes a visible approval-mode choice; yolo
|
|
12
|
+
// stays the default because a headless run cannot answer approval prompts.
|
|
13
|
+
|
|
14
|
+
const EFFORTS = {
|
|
15
|
+
"claude-code": ["low", "medium", "high", "xhigh", "max"],
|
|
16
|
+
codex: ["low", "medium", "high", "xhigh"],
|
|
17
|
+
gemini: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const GEMINI_APPROVAL = ["default", "auto_edit", "yolo", "plan"];
|
|
21
|
+
|
|
22
|
+
const ALIASES = {
|
|
23
|
+
"gemini-cli": "gemini",
|
|
24
|
+
"claude": "claude-code",
|
|
25
|
+
"codex-cli": "codex",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// These strings land in spawn argv. Model ids look like "claude-fable-5",
|
|
29
|
+
// "o3", "gemini-2.5-pro"; anything that could read as a flag (leading dash),
|
|
30
|
+
// carry spaces, or smuggle shell metacharacters is rejected, not escaped.
|
|
31
|
+
function cleanToken(v, max) {
|
|
32
|
+
const s = String(v == null ? "" : v).trim();
|
|
33
|
+
if (!s || s.length > (max || 64)) return null;
|
|
34
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(s)) return null;
|
|
35
|
+
return s;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function agentCommand(agent, opts) {
|
|
39
|
+
const o = opts || {};
|
|
40
|
+
const model = cleanToken(o.model, 64);
|
|
41
|
+
const effort = cleanToken(o.effort, 12);
|
|
42
|
+
const map = {
|
|
43
|
+
"claude-code": {
|
|
44
|
+
cmd: "claude",
|
|
45
|
+
stdin: false,
|
|
46
|
+
args(prompt) {
|
|
47
|
+
const a = [];
|
|
48
|
+
if (model) a.push("--model", model);
|
|
49
|
+
if (effort && EFFORTS["claude-code"].includes(effort)) a.push("--effort", effort);
|
|
50
|
+
a.push("-p", prompt);
|
|
51
|
+
return a;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
codex: {
|
|
55
|
+
cmd: "codex",
|
|
56
|
+
stdin: true,
|
|
57
|
+
args() {
|
|
58
|
+
const a = ["exec", "--skip-git-repo-check", "--ephemeral"];
|
|
59
|
+
if (model) a.push("-m", model);
|
|
60
|
+
// value is parsed as TOML by codex; the embedded quotes make it an
|
|
61
|
+
// unambiguous string (same shape as ~/.codex/config.toml uses).
|
|
62
|
+
if (effort && EFFORTS.codex.includes(effort)) a.push("-c", 'model_reasoning_effort="' + effort + '"');
|
|
63
|
+
a.push("-");
|
|
64
|
+
return a;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
gemini: {
|
|
68
|
+
cmd: "gemini",
|
|
69
|
+
stdin: false,
|
|
70
|
+
args(prompt) {
|
|
71
|
+
const a = [];
|
|
72
|
+
if (model) a.push("-m", model);
|
|
73
|
+
const mode = GEMINI_APPROVAL.includes(o.approvalMode) ? o.approvalMode : "yolo";
|
|
74
|
+
a.push("--approval-mode", mode);
|
|
75
|
+
a.push("-p", prompt);
|
|
76
|
+
return a;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const id = ALIASES[agent] || agent;
|
|
81
|
+
const entry = map[id] || null;
|
|
82
|
+
if (!entry) return null;
|
|
83
|
+
// surfaced so the run preamble can echo exactly what launched
|
|
84
|
+
entry.applied = {
|
|
85
|
+
agent: id,
|
|
86
|
+
model: model || null,
|
|
87
|
+
effort: entry === map.gemini ? null : (effort && EFFORTS[id].includes(effort) ? effort : null),
|
|
88
|
+
approvalMode: id === "gemini" ? (GEMINI_APPROVAL.includes(o.approvalMode) ? o.approvalMode : "yolo") : null,
|
|
89
|
+
};
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { agentCommand, cleanToken, EFFORTS, GEMINI_APPROVAL, ALIASES };
|