mover-os 4.7.7 → 4.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -24
- package/install.js +2868 -251
- package/package.json +15 -3
- package/src/dashboard/build.js +1541 -0
- package/src/dashboard/dashboard.js +276 -0
- package/src/dashboard/index.js +319 -0
- package/src/dashboard/lib/activation-log.js +297 -0
- package/src/dashboard/lib/active-context-parser.js +189 -0
- package/src/dashboard/lib/agent-command.js +93 -0
- package/src/dashboard/lib/agent-detect.js +255 -0
- package/src/dashboard/lib/agent-detector.js +92 -0
- package/src/dashboard/lib/agent-session.js +483 -0
- package/src/dashboard/lib/anti-identity-detector.js +116 -0
- package/src/dashboard/lib/approval-registry.js +170 -0
- package/src/dashboard/lib/auto-learnings-parser.js +183 -0
- package/src/dashboard/lib/cli-usage-parser.js +92 -0
- package/src/dashboard/lib/config-parser.js +109 -0
- package/src/dashboard/lib/connect-recommender.js +131 -0
- package/src/dashboard/lib/correlations-parser.js +231 -0
- package/src/dashboard/lib/daily-note-resolver.js +228 -0
- package/src/dashboard/lib/date-utils.js +43 -0
- package/src/dashboard/lib/distribution-parser.js +137 -0
- package/src/dashboard/lib/dossier-parser.js +64 -0
- package/src/dashboard/lib/drift-history.js +88 -0
- package/src/dashboard/lib/drift-score.js +119 -0
- package/src/dashboard/lib/engine-default-fingerprints.json +53 -0
- package/src/dashboard/lib/engine-health.js +173 -0
- package/src/dashboard/lib/engine-writer.js +1831 -0
- package/src/dashboard/lib/execution-plan.js +125 -0
- package/src/dashboard/lib/experiments-parser.js +429 -0
- package/src/dashboard/lib/feed-parser.js +294 -0
- package/src/dashboard/lib/forked-future.js +60 -0
- package/src/dashboard/lib/goal-forecast.js +427 -0
- package/src/dashboard/lib/goals-parser.js +67 -0
- package/src/dashboard/lib/health-protocols-parser.js +133 -0
- package/src/dashboard/lib/hook-activity.js +48 -0
- package/src/dashboard/lib/hook-indexer.js +169 -0
- package/src/dashboard/lib/hourly-activity-parser.js +143 -0
- package/src/dashboard/lib/identity-parser.js +85 -0
- package/src/dashboard/lib/ingestion.js +418 -0
- package/src/dashboard/lib/library-indexer-v2.js +226 -0
- package/src/dashboard/lib/library-indexer.js +105 -0
- package/src/dashboard/lib/library-search.js +290 -0
- package/src/dashboard/lib/log-activation.sh +61 -0
- package/src/dashboard/lib/memory-curator.js +97 -0
- package/src/dashboard/lib/memory-gardener.js +177 -0
- package/src/dashboard/lib/memory-gepa.js +102 -0
- package/src/dashboard/lib/memory-index.js +519 -0
- package/src/dashboard/lib/memory-rerank.js +72 -0
- package/src/dashboard/lib/memory-text.js +136 -0
- package/src/dashboard/lib/metrics-log-parser.js +76 -0
- package/src/dashboard/lib/moves-usage-parser.js +184 -0
- package/src/dashboard/lib/onboarding-forge.js +70 -0
- package/src/dashboard/lib/override-outcome-parser.js +68 -0
- package/src/dashboard/lib/override-summary.js +73 -0
- package/src/dashboard/lib/paths.js +192 -0
- package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
- package/src/dashboard/lib/phantom-strategy.js +129 -0
- package/src/dashboard/lib/pid-markers.js +80 -0
- package/src/dashboard/lib/project-scanner.js +121 -0
- package/src/dashboard/lib/promise-wall.js +88 -0
- package/src/dashboard/lib/record-score.js +173 -0
- package/src/dashboard/lib/redaction.js +140 -0
- package/src/dashboard/lib/refusal-parser.js +44 -0
- package/src/dashboard/lib/regenerate-manifest.js +206 -0
- package/src/dashboard/lib/rewind-snapshots.js +689 -0
- package/src/dashboard/lib/roast-wall-parser.js +200 -0
- package/src/dashboard/lib/run-registry.js +286 -0
- package/src/dashboard/lib/safe-write.js +63 -0
- package/src/dashboard/lib/session-log-parser.js +145 -0
- package/src/dashboard/lib/session-time-parser.js +158 -0
- package/src/dashboard/lib/skill-index.js +171 -0
- package/src/dashboard/lib/skill-indexer.js +118 -0
- package/src/dashboard/lib/skill-recommender.js +689 -0
- package/src/dashboard/lib/state-core/backfill.js +298 -0
- package/src/dashboard/lib/state-core/dryrun.js +188 -0
- package/src/dashboard/lib/state-core/event-log.js +615 -0
- package/src/dashboard/lib/state-core/events.js +265 -0
- package/src/dashboard/lib/state-core/projections.js +376 -0
- package/src/dashboard/lib/state-core/start-close.js +162 -0
- package/src/dashboard/lib/state-core/trial.js +96 -0
- package/src/dashboard/lib/strategy-parser.js +248 -0
- package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
- package/src/dashboard/lib/streak-parser.js +95 -0
- package/src/dashboard/lib/suggested-now.js +254 -0
- package/src/dashboard/lib/tool-awareness.js +125 -0
- package/src/dashboard/lib/transcript-parser.js +371 -0
- package/src/dashboard/lib/vault-graph-parser.js +205 -0
- package/src/dashboard/lib/view-generator.js +163 -0
- package/src/dashboard/lib/voice-dna-parser.js +57 -0
- package/src/dashboard/lib/walkthrough-script.js +140 -0
- package/src/dashboard/lib/workflow-graph-parser.js +170 -0
- package/src/dashboard/lib/workflow-library-parser.js +146 -0
- package/src/dashboard/server.js +2402 -0
- package/src/dashboard/shortcut.js +284 -0
- package/src/dashboard/static/setup-poc.html +306 -0
- package/src/dashboard/static/walkthrough-poc.html +580 -0
- package/src/dashboard/styles.css +1201 -0
- package/src/dashboard/templates/index.html +278 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/hero.png +0 -0
- package/src/dashboard/ui/dist/assets/index-ByVKPvLf.js +34 -0
- package/src/dashboard/ui/dist/assets/index-CCoKjUcC.js +161 -0
- package/src/dashboard/ui/dist/assets/index-CZWNQDt5.css +1 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
- package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
- package/src/dashboard/ui/dist/icon.svg +4 -0
- package/src/dashboard/ui/dist/index.html +18 -0
- package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
- package/src/dashboard/ui/dist/registerSW.js +1 -0
- package/src/dashboard/ui/dist/sw.js +1 -0
- package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* approval-registry.js — packet #2 rider: dashboard-owned Engine-write
|
|
5
|
+
* approval (OWNER-DECISIONS.md, "Packet #2 VERIFICATION VERDICT").
|
|
6
|
+
*
|
|
7
|
+
* WHY: Codex/Gemini PreToolUse "ask" on a core Engine file currently converts
|
|
8
|
+
* to a hard deny in mover-hook-adapter.js (isPermissionDecision branch),
|
|
9
|
+
* because neither agent supports an interactive terminal prompt. The owner's
|
|
10
|
+
* rider: for a DASHBOARD-launched run, Mover Studio owns the UI, so the
|
|
11
|
+
* daemon can hold a real pending approval and let the user answer it from
|
|
12
|
+
* the browser instead of always denying.
|
|
13
|
+
*
|
|
14
|
+
* This registry is the server-held half of that flow: create a pending
|
|
15
|
+
* approval (tokened POST from the hook adapter), poll its status (tokened
|
|
16
|
+
* GET, the hook's poll loop), list pending ones (GET, dashboard UI), and
|
|
17
|
+
* resolve one (tokened POST, dashboard UI click). In-memory only, mirrors
|
|
18
|
+
* RunRegistry / AgentSessionManager: a daemon restart clears everything,
|
|
19
|
+
* which is correct — a pending approval only makes sense against a live run.
|
|
20
|
+
*
|
|
21
|
+
* TTL expiry resolves as DENY (fail closed), same contract as the hook
|
|
22
|
+
* adapter's existing unconditional deny it is meant to soften, never worsen.
|
|
23
|
+
*
|
|
24
|
+
* NOTE (2026-07-11): nothing currently calls request() in production. The
|
|
25
|
+
* hook-adapter wiring that would create these (mover-hook-adapter.js's
|
|
26
|
+
* ask->deny conversion) was evaluated and NOT changed — the Codex/Gemini
|
|
27
|
+
* host hook timeout (5s, ~/.codex/hooks.json PreToolUse Edit|Write|apply_patch
|
|
28
|
+
* / install.js generateGeminiHooks BeforeTool write_file|replace) leaves no
|
|
29
|
+
* safety-margin room for a human-reviewable wait; see dev/plan.md
|
|
30
|
+
* T-PACKET2-RIDER-1 for the full evidence trail. This module is real,
|
|
31
|
+
* tested, standalone infrastructure ready for that follow-up.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const crypto = require("crypto");
|
|
35
|
+
|
|
36
|
+
const DEFAULTS = {
|
|
37
|
+
ttlMs: 60 * 1000, // ask (owner rider): TTL expiry resolves as deny
|
|
38
|
+
maxPending: 50, // bound memory; evict oldest non-pending first
|
|
39
|
+
sweepMs: 30 * 1000, // periodic cleanup cadence
|
|
40
|
+
graceMs: 5 * 60 * 1000, // keep a decided/expired record this long so a
|
|
41
|
+
// late poll still reads the real outcome
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const MAX_LEN = { agent: 32, file: 1024, tool: 64, excerpt: 4000 };
|
|
45
|
+
|
|
46
|
+
function capString(v, max, field) {
|
|
47
|
+
const s = String(v == null ? "" : v);
|
|
48
|
+
if (!s.trim()) throw new Error(`${field} is required`);
|
|
49
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ApprovalRegistry {
|
|
53
|
+
constructor(opts = {}) {
|
|
54
|
+
this.opt = { ...DEFAULTS, ...opts };
|
|
55
|
+
this.pending = new Map(); // id -> record
|
|
56
|
+
this._sweep = setInterval(() => this._evict(), this.opt.sweepMs);
|
|
57
|
+
if (this._sweep.unref) this._sweep.unref();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a pending approval. spec: { agent, file, tool, excerpt }.
|
|
62
|
+
* Returns the record (id, expiresAt, status:"pending").
|
|
63
|
+
*/
|
|
64
|
+
request(spec) {
|
|
65
|
+
const s = spec || {};
|
|
66
|
+
const agent = capString(s.agent, MAX_LEN.agent, "agent");
|
|
67
|
+
const file = capString(s.file, MAX_LEN.file, "file");
|
|
68
|
+
const tool = String(s.tool == null ? "" : s.tool).slice(0, MAX_LEN.tool);
|
|
69
|
+
const excerpt = String(s.excerpt == null ? "" : s.excerpt).slice(0, MAX_LEN.excerpt);
|
|
70
|
+
|
|
71
|
+
this._trimCount();
|
|
72
|
+
|
|
73
|
+
const id = crypto.randomBytes(16).toString("hex");
|
|
74
|
+
const createdAt = Date.now();
|
|
75
|
+
const rec = {
|
|
76
|
+
id,
|
|
77
|
+
agent,
|
|
78
|
+
file,
|
|
79
|
+
tool,
|
|
80
|
+
excerpt,
|
|
81
|
+
createdAt,
|
|
82
|
+
expiresAt: createdAt + this.opt.ttlMs,
|
|
83
|
+
status: "pending",
|
|
84
|
+
decidedAt: null,
|
|
85
|
+
decidedReason: null,
|
|
86
|
+
};
|
|
87
|
+
this.pending.set(id, rec);
|
|
88
|
+
return rec;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Look up a record and resolve TTL expiry lazily (a poll after the TTL
|
|
93
|
+
* elapsed sees "denied", even if no sweep has run yet). Returns null if
|
|
94
|
+
* the id was never issued or has aged out of the grace window.
|
|
95
|
+
*/
|
|
96
|
+
get(id) {
|
|
97
|
+
const rec = this.pending.get(String(id || ""));
|
|
98
|
+
if (!rec) return null;
|
|
99
|
+
if (rec.status === "pending" && Date.now() > rec.expiresAt) {
|
|
100
|
+
rec.status = "denied";
|
|
101
|
+
rec.decidedAt = Date.now();
|
|
102
|
+
rec.decidedReason = "timeout";
|
|
103
|
+
}
|
|
104
|
+
return rec;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** All currently PENDING (not yet decided, not yet expired) approvals. */
|
|
108
|
+
listPending() {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const id of this.pending.keys()) {
|
|
111
|
+
const rec = this.get(id); // applies TTL-expiry lazily
|
|
112
|
+
if (rec && rec.status === "pending") out.push({ ...rec });
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve a pending approval. Returns { ok, code, status, error }.
|
|
119
|
+
* - unknown/aged-out id -> { ok:false, code:404 }
|
|
120
|
+
* - already decided (approved/denied/timeout) -> { ok:false, code:409 }
|
|
121
|
+
* - success -> { ok:true, status: "approved"|"denied" }
|
|
122
|
+
*/
|
|
123
|
+
respond(id, approve) {
|
|
124
|
+
const rec = this.get(id);
|
|
125
|
+
if (!rec) return { ok: false, code: 404, error: "no such approval" };
|
|
126
|
+
if (rec.status !== "pending") {
|
|
127
|
+
return { ok: false, code: 409, error: `already ${rec.status}` };
|
|
128
|
+
}
|
|
129
|
+
rec.status = approve === true ? "approved" : "denied";
|
|
130
|
+
rec.decidedAt = Date.now();
|
|
131
|
+
rec.decidedReason = "response";
|
|
132
|
+
return { ok: true, status: rec.status };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Cap the number of retained records: drop the oldest DECIDED ones first,
|
|
136
|
+
* never a live pending one, so a burst of writes can't push out a request
|
|
137
|
+
* a human hasn't answered yet. */
|
|
138
|
+
_trimCount() {
|
|
139
|
+
if (this.pending.size < this.opt.maxPending) return;
|
|
140
|
+
const decided = [...this.pending.values()]
|
|
141
|
+
.filter((r) => r.status !== "pending")
|
|
142
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
143
|
+
while (this.pending.size >= this.opt.maxPending && decided.length) {
|
|
144
|
+
this.pending.delete(decided.shift().id);
|
|
145
|
+
}
|
|
146
|
+
// Degenerate case: still at cap and every record is genuinely pending
|
|
147
|
+
// (a flood). Drop the oldest pending rather than grow unbounded; it
|
|
148
|
+
// will read as a denial to whichever caller is waiting on it, which is
|
|
149
|
+
// the fail-closed outcome anyway.
|
|
150
|
+
if (this.pending.size >= this.opt.maxPending) {
|
|
151
|
+
const oldest = [...this.pending.values()].sort((a, b) => a.createdAt - b.createdAt)[0];
|
|
152
|
+
if (oldest) this.pending.delete(oldest.id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_evict() {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
for (const rec of [...this.pending.values()]) {
|
|
159
|
+
const decidedAt = rec.status === "pending" ? null : (rec.decidedAt || rec.expiresAt);
|
|
160
|
+
if (decidedAt && now - decidedAt > this.opt.graceMs) this.pending.delete(rec.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Test/shutdown hygiene: stop the sweep timer. */
|
|
165
|
+
stop() {
|
|
166
|
+
if (this._sweep) { clearInterval(this._sweep); this._sweep = null; }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { ApprovalRegistry, DEFAULTS };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { autoLearningsPath, safeRead, safeStat } = require("./paths");
|
|
4
|
+
|
|
5
|
+
// Parse Auto_Learnings.md — splits on `## (PATTERN|CORRECTION|BELIEF SHIFT|REFLEXION): Name` headers.
|
|
6
|
+
// REFLEXION (C3) is the AI's own error→recovery post-mortem, distinct from a user CORRECTION.
|
|
7
|
+
// Returns {available, entries: [...], stats: {patterns, corrections, beliefShifts, reflexions, byConfidence}}
|
|
8
|
+
|
|
9
|
+
function parseAutoLearnings(vault) {
|
|
10
|
+
const filePath = autoLearningsPath(vault);
|
|
11
|
+
const text = safeRead(filePath);
|
|
12
|
+
const stat = safeStat(filePath);
|
|
13
|
+
if (!text) return { available: false, error: "Auto_Learnings.md not found", entries: [], stats: emptyStats() };
|
|
14
|
+
|
|
15
|
+
const lines = text.split("\n");
|
|
16
|
+
const entries = [];
|
|
17
|
+
let current = null;
|
|
18
|
+
let buf = [];
|
|
19
|
+
// Track current section context so we can tag entries with polarity
|
|
20
|
+
// when authored under `## Anti-Patterns` / `## Positive Patterns`
|
|
21
|
+
// sectional headers. Defaults to null (use heuristic).
|
|
22
|
+
let sectionPolarity = null;
|
|
23
|
+
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
// Section-context header (Anti-Patterns / Positive Patterns)
|
|
26
|
+
const sec = line.match(/^##\s+(Anti-Patterns?|Positive Patterns?|Strengths?|Rituals?)\s*$/i);
|
|
27
|
+
if (sec) {
|
|
28
|
+
if (current) {
|
|
29
|
+
current.body = buf.join("\n");
|
|
30
|
+
entries.push(finalizeEntry(current, sectionPolarity));
|
|
31
|
+
current = null;
|
|
32
|
+
buf = [];
|
|
33
|
+
}
|
|
34
|
+
const tok = sec[1].toLowerCase();
|
|
35
|
+
sectionPolarity = /anti|leak/.test(tok) ? "anti"
|
|
36
|
+
: /positive|strength/.test(tok) ? "lift"
|
|
37
|
+
: /ritual/.test(tok) ? "ritual"
|
|
38
|
+
: null;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const m = line.match(/^##\s+(PATTERN|CORRECTION|BELIEF SHIFT|REFLEXION):\s*(.+?)\s*$/i);
|
|
43
|
+
if (m) {
|
|
44
|
+
if (current) {
|
|
45
|
+
current.body = buf.join("\n");
|
|
46
|
+
entries.push(finalizeEntry(current, sectionPolarity));
|
|
47
|
+
}
|
|
48
|
+
current = { type: m[1].toUpperCase(), name: m[2].trim() };
|
|
49
|
+
buf = [];
|
|
50
|
+
} else if (current) {
|
|
51
|
+
buf.push(line);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (current) {
|
|
55
|
+
current.body = buf.join("\n");
|
|
56
|
+
entries.push(finalizeEntry(current, sectionPolarity));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stats = computeStats(entries);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
available: true,
|
|
63
|
+
filePath,
|
|
64
|
+
lastUpdated: stat ? stat.mtime : null,
|
|
65
|
+
entries,
|
|
66
|
+
stats,
|
|
67
|
+
activePatternsConf3Plus: entries.filter(e =>
|
|
68
|
+
e.type === "PATTERN" &&
|
|
69
|
+
typeof e.confidence === "number" &&
|
|
70
|
+
e.confidence >= 3 &&
|
|
71
|
+
!["RESOLVED", "AUTOMATIC"].includes((e.status || "").toUpperCase())
|
|
72
|
+
),
|
|
73
|
+
growingPatterns: entries.filter(e =>
|
|
74
|
+
e.type === "PATTERN" && (e.status || "").toUpperCase() === "GROWING"
|
|
75
|
+
),
|
|
76
|
+
escalatedPatterns: entries.filter(e =>
|
|
77
|
+
e.type === "PATTERN" && (e.status || "").toUpperCase() === "ESCALATED"
|
|
78
|
+
),
|
|
79
|
+
recentBeliefShifts: entries.filter(e => e.type === "BELIEF SHIFT").slice(0, 5),
|
|
80
|
+
recentCorrections: entries.filter(e => e.type === "CORRECTION").slice(-10).reverse(),
|
|
81
|
+
recentReflexions: entries.filter(e => e.type === "REFLEXION").slice(-10).reverse()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function finalizeEntry(entry, sectionPolarity) {
|
|
86
|
+
const body = entry.body || "";
|
|
87
|
+
const fields = parseBoldFields(body);
|
|
88
|
+
const evidence = parseEvidence(body);
|
|
89
|
+
|
|
90
|
+
const severity = (fields.severity || "").toUpperCase() || null;
|
|
91
|
+
const status = (fields.status || "").toUpperCase().replace(/\s+/g, "_") || null;
|
|
92
|
+
// Polarity: explicit section context > severity heuristic
|
|
93
|
+
let polarity = sectionPolarity;
|
|
94
|
+
if (!polarity) {
|
|
95
|
+
if (severity && /POSITIVE/.test(severity)) polarity = "lift";
|
|
96
|
+
else if (severity === "HIGH" || severity === "CRIT" || severity === "CRITICAL") polarity = "anti";
|
|
97
|
+
else if (status === "GROWING" || status === "BAKED_INTO_STRATEGY_MD") polarity = "watching";
|
|
98
|
+
else polarity = "neutral";
|
|
99
|
+
}
|
|
100
|
+
// 5-type taxonomy mapping (LEAK / DRIFT / STRENGTH / RECOVERY / RITUAL)
|
|
101
|
+
let category = "watching";
|
|
102
|
+
if (polarity === "anti") {
|
|
103
|
+
if (/distribution|gap|spree|avoidance|procrastinat|phantom/i.test(entry.name)) category = "drift";
|
|
104
|
+
else category = "leak";
|
|
105
|
+
} else if (polarity === "lift" || polarity === "strength") category = "strength";
|
|
106
|
+
else if (polarity === "ritual") category = "ritual";
|
|
107
|
+
else if (status === "GROWING") category = "recovery";
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
type: entry.type,
|
|
111
|
+
name: entry.name,
|
|
112
|
+
body,
|
|
113
|
+
fields,
|
|
114
|
+
evidence,
|
|
115
|
+
confidence: parseInt(fields.confidence || "0", 10) || null,
|
|
116
|
+
status,
|
|
117
|
+
severity,
|
|
118
|
+
polarity,
|
|
119
|
+
category,
|
|
120
|
+
domain: fields.domain || null,
|
|
121
|
+
type_subtype: fields.type || null,
|
|
122
|
+
firstDetected: fields.first_detected || null,
|
|
123
|
+
lastUpdated: fields.last_updated || null,
|
|
124
|
+
lastValidated: fields.last_validated || null,
|
|
125
|
+
confrontationCount: parseInt(fields.confrontation_count || "0", 10) || null,
|
|
126
|
+
trigger: fields.trigger || null,
|
|
127
|
+
counterAction: fields.counter_action || null,
|
|
128
|
+
suspectedVariable: fields.suspected_variable || null,
|
|
129
|
+
hypothesis: fields.hypothesis || null,
|
|
130
|
+
resistanceForm: fields.resistance_form || null,
|
|
131
|
+
beliefTier: fields.belief_tier || null,
|
|
132
|
+
previousBelief: fields.previous_belief || null,
|
|
133
|
+
newBelief: fields.new_belief || null,
|
|
134
|
+
error: fields.error || null,
|
|
135
|
+
fix: fields.fix || null
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseBoldFields(body) {
|
|
140
|
+
const fields = {};
|
|
141
|
+
const re = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
|
|
142
|
+
let m;
|
|
143
|
+
while ((m = re.exec(body)) !== null) {
|
|
144
|
+
const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
|
|
145
|
+
fields[k] = m[2].trim();
|
|
146
|
+
}
|
|
147
|
+
return fields;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseEvidence(body) {
|
|
151
|
+
const evidence = [];
|
|
152
|
+
const lines = body.split("\n");
|
|
153
|
+
let inEvidence = false;
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
if (/^###\s+Evidence/i.test(line)) { inEvidence = true; continue; }
|
|
156
|
+
if (inEvidence && /^###\s+/.test(line)) inEvidence = false;
|
|
157
|
+
if (inEvidence) {
|
|
158
|
+
const m = line.match(/^[-*]\s+(.+)$/);
|
|
159
|
+
if (m) evidence.push(m[1].trim());
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return evidence;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function emptyStats() {
|
|
166
|
+
return { total: 0, patterns: 0, corrections: 0, beliefShifts: 0, reflexions: 0, byConfidence: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, byStatus: {} };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function computeStats(entries) {
|
|
170
|
+
const stats = emptyStats();
|
|
171
|
+
stats.total = entries.length;
|
|
172
|
+
for (const e of entries) {
|
|
173
|
+
if (e.type === "PATTERN") stats.patterns++;
|
|
174
|
+
else if (e.type === "CORRECTION") stats.corrections++;
|
|
175
|
+
else if (e.type === "BELIEF SHIFT") stats.beliefShifts++;
|
|
176
|
+
else if (e.type === "REFLEXION") stats.reflexions++;
|
|
177
|
+
if (e.confidence && e.confidence >= 1 && e.confidence <= 5) stats.byConfidence[e.confidence]++;
|
|
178
|
+
if (e.status) stats.byStatus[e.status] = (stats.byStatus[e.status] || 0) + 1;
|
|
179
|
+
}
|
|
180
|
+
return stats;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { parseAutoLearnings };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { safeReadJson, moverConfigPath } = require("./paths");
|
|
4
|
+
|
|
5
|
+
// Parse moveros CLI usage from ~/.mover/config.json cfg.cli_usage map.
|
|
6
|
+
// Cross-references with the 17-entry CLI_COMMANDS registry the server
|
|
7
|
+
// exposes at /api/cli/index so the dashboard can show:
|
|
8
|
+
// - count: how many times this command has been run
|
|
9
|
+
// - lastRun: ISO timestamp of last invocation
|
|
10
|
+
// - ageDays: days since lastRun
|
|
11
|
+
|
|
12
|
+
// Canonical command list from the moveros CLI install.js. Mirror of
|
|
13
|
+
// /api/cli/index — kept in sync via the install package's CLI_COMMANDS.
|
|
14
|
+
const COMMANDS = [
|
|
15
|
+
{ name: "install", description: "Fresh install wizard" },
|
|
16
|
+
{ name: "update", description: "Comprehensive update: agents, rules, skills" },
|
|
17
|
+
{ name: "uninstall", description: "Remove Mover OS from vault and agents" },
|
|
18
|
+
{ name: "doctor", description: "Health check across all installed agents" },
|
|
19
|
+
{ name: "pulse", description: "Terminal dashboard: energy, tasks, streaks" },
|
|
20
|
+
{ name: "capture", description: "Quick capture: tasks, links, ideas" },
|
|
21
|
+
{ name: "who", description: "Entity memory lookup" },
|
|
22
|
+
{ name: "diff", description: "Engine file evolution viewer" },
|
|
23
|
+
{ name: "sync", description: "Cross-agent synchronization" },
|
|
24
|
+
{ name: "replay", description: "Session replay from Daily Notes" },
|
|
25
|
+
{ name: "context", description: "Debug what each agent sees" },
|
|
26
|
+
{ name: "settings", description: "View/edit config" },
|
|
27
|
+
{ name: "backup", description: "Manual backup wizard" },
|
|
28
|
+
{ name: "restore", description: "Restore from backup" },
|
|
29
|
+
{ name: "prayer", description: "Manage prayer times" },
|
|
30
|
+
{ name: "status", description: "Full system state (vault, agents, version)" },
|
|
31
|
+
{ name: "help", description: "Interactive guide to Mover OS" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function parseCliUsage() {
|
|
35
|
+
// False-zero discipline (T261): distinguish "config absent / never tracked"
|
|
36
|
+
// from "tracked, genuinely zero runs". A present cfg.cli_usage map — even
|
|
37
|
+
// all-zero — is real telemetry and stays available:true. Only true ABSENCE
|
|
38
|
+
// (no config file, or no cli_usage key at all) flips available:false so the
|
|
39
|
+
// consumer renders "—"/"calibrating" instead of a fabricated 0-usage table.
|
|
40
|
+
const cfg = safeReadJson(moverConfigPath());
|
|
41
|
+
if (cfg == null) {
|
|
42
|
+
return {
|
|
43
|
+
available: false,
|
|
44
|
+
unavailableReason: "file_missing",
|
|
45
|
+
commands: [],
|
|
46
|
+
totalUsage: null,
|
|
47
|
+
everUsedCount: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (cfg.cli_usage == null || typeof cfg.cli_usage !== "object") {
|
|
51
|
+
return {
|
|
52
|
+
available: false,
|
|
53
|
+
unavailableReason: "no_data",
|
|
54
|
+
commands: [],
|
|
55
|
+
totalUsage: null,
|
|
56
|
+
everUsedCount: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const usage = cfg.cli_usage;
|
|
60
|
+
|
|
61
|
+
const commands = COMMANDS.map(c => {
|
|
62
|
+
const u = usage[c.name] || {};
|
|
63
|
+
const count = typeof u.count === "number" ? u.count : 0;
|
|
64
|
+
const last = u.last || null;
|
|
65
|
+
let ageDays = null;
|
|
66
|
+
if (last) {
|
|
67
|
+
try {
|
|
68
|
+
const t = new Date(last).getTime();
|
|
69
|
+
if (!isNaN(t)) ageDays = Math.floor((Date.now() - t) / 86400000);
|
|
70
|
+
} catch (_) { /* skip */ }
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
name: c.name,
|
|
74
|
+
description: c.description,
|
|
75
|
+
count,
|
|
76
|
+
lastRun: last,
|
|
77
|
+
ageDays,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const totalUsage = commands.reduce((sum, c) => sum + c.count, 0);
|
|
82
|
+
const everUsed = commands.filter(c => c.count > 0).length;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
available: true,
|
|
86
|
+
commands,
|
|
87
|
+
totalUsage,
|
|
88
|
+
everUsedCount: everUsed,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { parseCliUsage };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
// config-parser.js — Read/write ~/.mover/config.json. Used by Settings mode +
|
|
8
|
+
// agent-detector (preferredAgent) + first-run UX (vaultPath resolution).
|
|
9
|
+
|
|
10
|
+
const CONFIG_PATH = path.join(os.homedir(), ".mover", "config.json");
|
|
11
|
+
|
|
12
|
+
const DEFAULTS = Object.freeze({
|
|
13
|
+
vaultPath: null,
|
|
14
|
+
preferredAgent: null,
|
|
15
|
+
// Onboarding basic-info (captured on first run, editable in Configure):
|
|
16
|
+
userName: null, // what to call the user
|
|
17
|
+
vaultName: null, // friendly label for their vault/system
|
|
18
|
+
osName: null, // dashboard + desktop-shortcut display name (shortcut.js reads os_name/osName)
|
|
19
|
+
lens: "everyday", // worldview "lingo": everyday | founder | muslim | secular | custom
|
|
20
|
+
lensCustom: { bet: "", seal: "", receipts: "" }, // the custom-lens term overrides
|
|
21
|
+
reviewDay: "Sunday",
|
|
22
|
+
eveningZoneStart: 17,
|
|
23
|
+
midnightCutoff: 5,
|
|
24
|
+
deepWorkHours: [9, 12],
|
|
25
|
+
sleepTarget: 7,
|
|
26
|
+
frictionSensitivity: "normal",
|
|
27
|
+
// #112: per-provider run tuning. null model/effort = the provider's own
|
|
28
|
+
// default (flag omitted). These are "what the Run picker starts on", not a
|
|
29
|
+
// hidden default — the picker writes back the user's last choice.
|
|
30
|
+
providerPrefs: {
|
|
31
|
+
"claude-code": { model: null, effort: null },
|
|
32
|
+
codex: { model: null, effort: null },
|
|
33
|
+
gemini: { model: null, approvalMode: "yolo" },
|
|
34
|
+
},
|
|
35
|
+
redaction: {
|
|
36
|
+
names: true,
|
|
37
|
+
faith: true,
|
|
38
|
+
weight: true,
|
|
39
|
+
money: true,
|
|
40
|
+
projectNames: true,
|
|
41
|
+
exactDates: false,
|
|
42
|
+
fileNames: true,
|
|
43
|
+
},
|
|
44
|
+
studio: {
|
|
45
|
+
density: "comfortable",
|
|
46
|
+
mono: false,
|
|
47
|
+
accent: "#d97a3c",
|
|
48
|
+
showAgentsInTopStrip: true,
|
|
49
|
+
autoOpenBrowser: true,
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// two-level merge: per-provider objects merge key-by-key so a stored
|
|
54
|
+
// { codex: { effort } } keeps the default gemini approvalMode intact.
|
|
55
|
+
function mergeProviderPrefs(base, raw) {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const k of Object.keys(base)) out[k] = { ...base[k] };
|
|
58
|
+
for (const k of Object.keys(raw || {})) {
|
|
59
|
+
const next = raw[k];
|
|
60
|
+
out[k] = next && typeof next === "object" ? { ...(out[k] || {}), ...next } : next;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function read() {
|
|
66
|
+
try {
|
|
67
|
+
if (!fs.existsSync(CONFIG_PATH)) return { ...DEFAULTS, exists: false, path: CONFIG_PATH };
|
|
68
|
+
const text = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
69
|
+
const raw = JSON.parse(text);
|
|
70
|
+
return {
|
|
71
|
+
...DEFAULTS,
|
|
72
|
+
...raw,
|
|
73
|
+
// Deep-merge for nested objects
|
|
74
|
+
redaction: { ...DEFAULTS.redaction, ...(raw.redaction || {}) },
|
|
75
|
+
studio: { ...DEFAULTS.studio, ...(raw.studio || {}) },
|
|
76
|
+
lensCustom: { ...DEFAULTS.lensCustom, ...(raw.lensCustom || {}) },
|
|
77
|
+
providerPrefs: mergeProviderPrefs(DEFAULTS.providerPrefs, raw.providerPrefs),
|
|
78
|
+
exists: true,
|
|
79
|
+
path: CONFIG_PATH,
|
|
80
|
+
};
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return { ...DEFAULTS, exists: false, path: CONFIG_PATH, parseError: e.message };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function write(updates) {
|
|
87
|
+
// Merge with existing — never overwrite full file
|
|
88
|
+
const current = read();
|
|
89
|
+
delete current.exists;
|
|
90
|
+
delete current.path;
|
|
91
|
+
delete current.parseError;
|
|
92
|
+
const next = {
|
|
93
|
+
...current,
|
|
94
|
+
...updates,
|
|
95
|
+
redaction: { ...current.redaction, ...(updates.redaction || {}) },
|
|
96
|
+
studio: { ...current.studio, ...(updates.studio || {}) },
|
|
97
|
+
lensCustom: { ...current.lensCustom, ...(updates.lensCustom || {}) },
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
const dir = path.dirname(CONFIG_PATH);
|
|
101
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
102
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
103
|
+
return { ok: true, written: next };
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return { ok: false, error: e.message };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { read, write, DEFAULTS, CONFIG_PATH };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* connect-recommender.js — from a setup-interview profile, build a personalized,
|
|
5
|
+
* domain-grouped "what to feed/connect me" plan. This is the fix for the cold-start gap
|
|
6
|
+
* ("if you don't feed it your day-to-day data it takes a while to pick up"): on day one we
|
|
7
|
+
* tell the user EXACTLY what to feed Mover OS, grouped by domain, in their own context —
|
|
8
|
+
* instead of a generic "upload your notes."
|
|
9
|
+
*
|
|
10
|
+
* Deterministic + zero-dep (mirrors skill-recommender.js). HONESTY CONTRACT: a source is
|
|
11
|
+
* `status:'ready'` ONLY if we can act on it right now — i.e. a file UPLOAD, wired to the real
|
|
12
|
+
* /api/session/ingest. Live OAuth connectors we have NOT built (Slack/Drive/calendar) are
|
|
13
|
+
* `status:'planned'` — an honest recommendation, never a fake "Connect" button. There is always
|
|
14
|
+
* ≥1 ready action so the user can do something real immediately.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} profile free-text setup answers ({work, tools, vitality, faith, goals, ...})
|
|
17
|
+
* @returns {{ diagnosis: string,
|
|
18
|
+
* groups: Array<{ key:string, label:string,
|
|
19
|
+
* sources: Array<{ id, label, why, action:'upload'|'connect', status:'ready'|'planned' }> }> }}
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// action 'upload' → we can ingest a file today (status ready). 'connect' → live OAuth we have
|
|
23
|
+
// NOT built (status planned). `universal:true` is always offered (the baseline cold-start fix).
|
|
24
|
+
const WORK_SOURCES = [
|
|
25
|
+
{ id: "notes-docs", action: "upload", universal: true,
|
|
26
|
+
label: "Upload your key docs & notes",
|
|
27
|
+
why: "the docs, notes and trackers you already have, so Mover reads them instead of asking you to re-explain." },
|
|
28
|
+
{ id: "clients-leads", action: "upload",
|
|
29
|
+
signals: ["client", "clients", "lead", "leads", "sales", "crm", "customer", "customers", "deal", "pipeline", "prospect"],
|
|
30
|
+
label: "Upload your leads / client list",
|
|
31
|
+
why: "your pipeline, so Mover tracks who's waiting on what." },
|
|
32
|
+
{ id: "whatsapp", action: "upload", signals: ["whatsapp"],
|
|
33
|
+
label: "Upload a WhatsApp export",
|
|
34
|
+
why: "export a key chat. Mover pulls the decisions and commitments out of it." },
|
|
35
|
+
{ id: "slack", action: "connect", signals: ["slack", "team", "teammate", "coworker", "co-worker", "channel"],
|
|
36
|
+
label: "Connect Slack",
|
|
37
|
+
why: "your team's back-and-forth, so Mover sees what's actually in flight." },
|
|
38
|
+
{ id: "gdrive", action: "connect", signals: ["drive", "google doc", "gdoc", "docs", "document", "sheet", "spreadsheet"],
|
|
39
|
+
label: "Connect Google Drive",
|
|
40
|
+
why: "your working docs in one place, instead of scattered." },
|
|
41
|
+
{ id: "calendar", action: "connect", signals: ["calendar", "meeting", "meetings", "schedule", "gcal", "appointment"],
|
|
42
|
+
label: "Connect your calendar",
|
|
43
|
+
why: "so /plan-tomorrow plans around your real day, not a blank one." },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const VITALITY_SOURCES = [
|
|
47
|
+
{ id: "health-export", action: "upload",
|
|
48
|
+
signals: ["gym", "fitness", "sleep", "health", "fitbit", "whoop", "oura", "workout", "training", "run", "running", "diet", "calorie", "calories", "steps", "exercise"],
|
|
49
|
+
label: "Upload a health export",
|
|
50
|
+
why: "your sleep/training data, so the battery and floors are real, not guessed." },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const FAITH_SOURCES = [
|
|
54
|
+
{ id: "faith-notes", action: "upload",
|
|
55
|
+
signals: ["faith", "prayer", "pray", "quran", "qur'an", "salah", "salat", "spiritual", "deen", "ibadah", "worship", "scripture", "church", "mosque", "masjid"],
|
|
56
|
+
label: "Add your practice / reflection notes",
|
|
57
|
+
why: "your reflections, so the faith domain isn't a placeholder." },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const statusFor = (action) => (action === "upload" ? "ready" : "planned");
|
|
61
|
+
|
|
62
|
+
function profileBlob(profile) {
|
|
63
|
+
const parts = [];
|
|
64
|
+
const walk = (v) => {
|
|
65
|
+
if (v == null) return;
|
|
66
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
67
|
+
parts.push(String(v));
|
|
68
|
+
} else if (Array.isArray(v)) {
|
|
69
|
+
v.forEach(walk);
|
|
70
|
+
} else if (typeof v === "object") {
|
|
71
|
+
Object.values(v).forEach(walk);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
for (const v of Object.values(profile || {})) {
|
|
75
|
+
walk(v);
|
|
76
|
+
}
|
|
77
|
+
return parts.join(" ").toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const matches = (text, signals) => (signals || []).some((s) => text.includes(s));
|
|
81
|
+
|
|
82
|
+
function pickSources(catalogue, text, cap) {
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const src of catalogue) {
|
|
85
|
+
if (src.universal || matches(text, src.signals)) {
|
|
86
|
+
out.push({ id: src.id, label: src.label, why: src.why, action: src.action, status: statusFor(src.action) });
|
|
87
|
+
}
|
|
88
|
+
if (out.length >= cap) break;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function diagnose(profile) {
|
|
94
|
+
let work = String((profile && profile.work) || "").replace(/```[\s\S]*?```/g, "").trim();
|
|
95
|
+
// Strip a first-person lead-in ("I run …", "I'm building …", "my …") so the line reads
|
|
96
|
+
// cleanly after "You're running …" instead of echoing "You're running I run a marketing agency."
|
|
97
|
+
work = work.replace(
|
|
98
|
+
/^(i['’]?m\s+building\s+|i\s+am\s+building\s+|i['’]?m\s+running\s+|i\s+am\s+running\s+|i\s+run\s+|i\s+build\s+|i\s+do\s+|i\s+lead\s+|i\s+manage\s+|i\s+operate\s+|i\s+own\s+|i\s+work\s+on\s+|i['’]?m\s+a\s+|i\s+am\s+a\s+|i['’]?m\s+|i\s+am\s+|my\s+|we\s+|currently\s+)/i,
|
|
99
|
+
""
|
|
100
|
+
).trim();
|
|
101
|
+
if (work) {
|
|
102
|
+
const words = work.split(/\s+/);
|
|
103
|
+
const summary = words.slice(0, 14).join(" ") + (words.length > 14 ? "…" : "");
|
|
104
|
+
return `You're running ${summary}. Mover OS is sharpest once it can see your real work. ` +
|
|
105
|
+
`here's what to feed it first, so day one isn't a blank slate.`;
|
|
106
|
+
}
|
|
107
|
+
return "Mover OS gets sharper the more of your real work it can see. " +
|
|
108
|
+
"Here's what to feed it first so day one isn't a blank slate.";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function connectPlan(profile = {}) {
|
|
112
|
+
const text = profileBlob(profile);
|
|
113
|
+
const groups = [{ key: "work", label: "Work", sources: pickSources(WORK_SOURCES, text, 6) }];
|
|
114
|
+
|
|
115
|
+
const vitality = pickSources(VITALITY_SOURCES, text, 3);
|
|
116
|
+
if (vitality.length) groups.push({ key: "vitality", label: "Vitality", sources: vitality });
|
|
117
|
+
|
|
118
|
+
const faith = pickSources(FAITH_SOURCES, text, 3);
|
|
119
|
+
if (faith.length) groups.push({ key: "faith", label: "Faith", sources: faith });
|
|
120
|
+
|
|
121
|
+
// Total cap — overwhelm is the enemy. Trim from the LAST group inward; never drop a group's
|
|
122
|
+
// first source (Work's first is the universal upload, the one always-real action).
|
|
123
|
+
let total = groups.reduce((n, g) => n + g.sources.length, 0);
|
|
124
|
+
for (let i = groups.length - 1; i >= 0 && total > 8; i--) {
|
|
125
|
+
while (groups[i].sources.length > 1 && total > 8) { groups[i].sources.pop(); total--; }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { diagnosis: diagnose(profile), groups };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { connectPlan, WORK_SOURCES, VITALITY_SOURCES, FAITH_SOURCES };
|