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,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// record-score.js — the journal-calibration gap. What the RECORD says your
|
|
4
|
+
// day was (derived purely from logged signal), vs the self-score you'd give
|
|
5
|
+
// it. The dashboard surfaces the delta so a "felt like a 7" can meet a
|
|
6
|
+
// "logged like a 3" — or the reverse.
|
|
7
|
+
//
|
|
8
|
+
// computeRecordScore({ daily, distribution, strategyProtocol }) reads two
|
|
9
|
+
// honest signals and nothing else:
|
|
10
|
+
// 1. plan-done ratio — daily.today.tasksDone / daily.today.tasksTotal
|
|
11
|
+
// 2. ship/distribution today — distribution.todayCount
|
|
12
|
+
//
|
|
13
|
+
// FALSE-ZERO (binding, T261 discipline): "no tasks logged AND no
|
|
14
|
+
// distribution today" is INSUFFICIENT DATA, not a bad day. It returns
|
|
15
|
+
// { score: null, why: null } — never a fabricated 4. A real 0/N plan with
|
|
16
|
+
// 0 ships is a measured low day and DOES score (low), because the tasks
|
|
17
|
+
// were logged. The distinction is absence-of-record vs record-of-nothing.
|
|
18
|
+
//
|
|
19
|
+
// strategyProtocol is accepted for signature parity with the other parsers
|
|
20
|
+
// build.js wires (and to let the basis string note whether a protocol
|
|
21
|
+
// exists), but it is NOT load-bearing for the number — the score must stay
|
|
22
|
+
// defensible from the two hard signals above.
|
|
23
|
+
|
|
24
|
+
// ── The 1-10 mapping ──────────────────────────────────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// Two components, then a blend that lets either one carry a day.
|
|
27
|
+
//
|
|
28
|
+
// PLAN component (0..10), only when tasksTotal > 0:
|
|
29
|
+
// ratio = tasksDone / tasksTotal, clamped to [0,1].
|
|
30
|
+
// plan = round(ratio * 9) + 1 → 0% maps to 1 (a logged dead day is a 1,
|
|
31
|
+
// not a 0; 0 is reserved for "no record"),
|
|
32
|
+
// 100% maps to 10. Linear between.
|
|
33
|
+
//
|
|
34
|
+
// SHIP component (0..10), from distribution.todayCount (a ship/outreach
|
|
35
|
+
// count — see distribution-parser.js KEYWORDS):
|
|
36
|
+
// 0 ships → 0 (no lift; it cannot, alone, drag a completed plan down —
|
|
37
|
+
// see blend)
|
|
38
|
+
// 1 ship → 6 (you put something into the world; floor of a "real" day)
|
|
39
|
+
// 2 ships → 8
|
|
40
|
+
// 3+ ships→ 10 (capped)
|
|
41
|
+
// This is deliberately generous: distribution is the scarce, high-leverage
|
|
42
|
+
// act in this system, so each unit of it is worth a lot.
|
|
43
|
+
//
|
|
44
|
+
// BLEND:
|
|
45
|
+
// - Both signals present → max(plan, ship). The day is as good as its best
|
|
46
|
+
// real axis; shipping rescues a thin plan, a full plan rescues a quiet
|
|
47
|
+
// ship-day. Taking the max (not the mean) avoids punishing a 10/10 plan
|
|
48
|
+
// for a no-outreach day, and avoids punishing a 3-ship day for a sparse
|
|
49
|
+
// task list.
|
|
50
|
+
// - Plan only (ships unknown/absent) → plan.
|
|
51
|
+
// - Ships only (no tasks logged, but ships happened) → ship, floored at 1
|
|
52
|
+
// so a logged ship is never a "1-or-below" by rounding.
|
|
53
|
+
// - Neither → null (false-zero).
|
|
54
|
+
//
|
|
55
|
+
// Final score is clamped to [1,10] and rounded to a whole number — a journal
|
|
56
|
+
// score is a gut integer, not 6.4.
|
|
57
|
+
|
|
58
|
+
function computeRecordScore({ daily, distribution, strategyProtocol } = {}) {
|
|
59
|
+
// ── Read the two hard signals, distinguishing absent from zero ──────────
|
|
60
|
+
|
|
61
|
+
// Plan: only "logged" when today's note exists AND has a task list.
|
|
62
|
+
// daily / daily.today may be null (build.js daily mapper, lines ~681-704).
|
|
63
|
+
const today = daily && daily.today ? daily.today : null;
|
|
64
|
+
const tasksTotal = today && Number.isFinite(today.tasksTotal) ? today.tasksTotal : 0;
|
|
65
|
+
const tasksDone = today && Number.isFinite(today.tasksDone) ? today.tasksDone : 0;
|
|
66
|
+
// A plan only counts as "logged" when there are tasks to grade against.
|
|
67
|
+
// 0/0 is an empty note — no record — not a 0% day.
|
|
68
|
+
const planLogged = tasksTotal > 0;
|
|
69
|
+
|
|
70
|
+
// Ships: distribution.todayCount is number|null. null = parser couldn't
|
|
71
|
+
// measure (no Dailies dir / unavailable), which is NOT zero. Only a real
|
|
72
|
+
// finite number is a measurement. [false-zero — mirrors goal-forecast.js:38]
|
|
73
|
+
const distAvailable = !!(distribution && distribution.available !== false);
|
|
74
|
+
const shipsToday = distAvailable && Number.isFinite(distribution.todayCount)
|
|
75
|
+
? distribution.todayCount
|
|
76
|
+
: null;
|
|
77
|
+
const shipsKnown = shipsToday != null;
|
|
78
|
+
|
|
79
|
+
// ── FALSE-ZERO GATE (binding) ───────────────────────────────────────────
|
|
80
|
+
// No tasks logged AND no distribution measured today → not enough to score.
|
|
81
|
+
// Never a fabricated middle-of-the-road 4.
|
|
82
|
+
if (!planLogged && (!shipsKnown || shipsToday === 0)) {
|
|
83
|
+
// One more split: if ships were *measured* as 0 we still bail (no plan +
|
|
84
|
+
// 0 ships = nothing recorded happened). If ships are simply unknown, same
|
|
85
|
+
// outcome. Both are "nothing to grade."
|
|
86
|
+
return {
|
|
87
|
+
score: null,
|
|
88
|
+
why: null,
|
|
89
|
+
basis: "not enough logged to score today",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Component scores ────────────────────────────────────────────────────
|
|
94
|
+
let planScore = null;
|
|
95
|
+
if (planLogged) {
|
|
96
|
+
const ratio = clamp(tasksDone / tasksTotal, 0, 1);
|
|
97
|
+
planScore = Math.round(ratio * 9) + 1; // 0%→1, 100%→10
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let shipScore = null;
|
|
101
|
+
if (shipsKnown) {
|
|
102
|
+
shipScore = shipsToday <= 0 ? 0
|
|
103
|
+
: shipsToday === 1 ? 6
|
|
104
|
+
: shipsToday === 2 ? 8
|
|
105
|
+
: 10; // 3+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Blend ───────────────────────────────────────────────────────────────
|
|
109
|
+
let raw;
|
|
110
|
+
if (planScore != null && shipScore != null) {
|
|
111
|
+
raw = Math.max(planScore, shipScore);
|
|
112
|
+
} else if (planScore != null) {
|
|
113
|
+
raw = planScore;
|
|
114
|
+
} else {
|
|
115
|
+
// ships-only path: a logged ship with no task list. Floor at 1 so a
|
|
116
|
+
// single ship can't round below the "1 = a real but minimal day" baseline
|
|
117
|
+
// (shipScore for 1 ship is already 6, so this floor only matters
|
|
118
|
+
// defensively).
|
|
119
|
+
raw = Math.max(1, shipScore);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const score = clamp(Math.round(raw), 1, 10);
|
|
123
|
+
|
|
124
|
+
// ── Explain the number in plain language ────────────────────────────────
|
|
125
|
+
const why = buildWhy({ planLogged, tasksDone, tasksTotal, shipsKnown, shipsToday, planScore, shipScore, score });
|
|
126
|
+
|
|
127
|
+
// basis = the provenance string, names which signals were actually used
|
|
128
|
+
// (and whether a protocol was available for context — not used in math).
|
|
129
|
+
const parts = [];
|
|
130
|
+
if (planLogged) parts.push("plan-done ratio");
|
|
131
|
+
if (shipsKnown) parts.push("distribution today");
|
|
132
|
+
const protoNote = strategyProtocol && strategyProtocol.available ? " (protocol present)" : "";
|
|
133
|
+
const basis = parts.length
|
|
134
|
+
? `derived from ${parts.join(" + ")}${protoNote}`
|
|
135
|
+
: "derived from logged signal";
|
|
136
|
+
|
|
137
|
+
return { score, why, basis };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function clamp(n, min, max) {
|
|
143
|
+
if (!Number.isFinite(n)) return min;
|
|
144
|
+
return Math.min(max, Math.max(min, n));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildWhy({ planLogged, tasksDone, tasksTotal, shipsKnown, shipsToday, planScore, shipScore, score }) {
|
|
148
|
+
const bits = [];
|
|
149
|
+
if (planLogged) {
|
|
150
|
+
const pct = Math.round((clamp(tasksDone / tasksTotal, 0, 1)) * 100);
|
|
151
|
+
bits.push(`${tasksDone}/${tasksTotal} plan tasks done (${pct}%)`);
|
|
152
|
+
}
|
|
153
|
+
if (shipsKnown) {
|
|
154
|
+
const n = shipsToday;
|
|
155
|
+
bits.push(n === 0 ? "no distribution logged today" : `${n} ship${n === 1 ? "" : "s"} logged today`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Name the carrier when blending, so the user sees WHY it landed where it did.
|
|
159
|
+
// Only claim something "carried" the day when it actually beat the other axis
|
|
160
|
+
// by enough to matter; ties (incl. a low/low dead day) just say they agree.
|
|
161
|
+
let carrier = "";
|
|
162
|
+
if (planScore != null && shipScore != null) {
|
|
163
|
+
if (shipScore > planScore) carrier = ". Shipping carried the day over a thin plan";
|
|
164
|
+
else if (planScore > shipScore) carrier = ". Plan completion carried it over a quiet ship-day";
|
|
165
|
+
else carrier = ". Plan and shipping agree";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// "an 8/10" — article matches the score's spoken vowel sound (8/11/18), else "a".
|
|
169
|
+
const art = /^(8|11|18)/.test(String(score)) ? "an" : "a";
|
|
170
|
+
return `Record scores today ${art} ${score}/10: ${bits.join(", ")}${carrier}. Compare against how the day felt.`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { computeRecordScore };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// redaction.js — Privacy-first scrubber for Public Proof share cards.
|
|
4
|
+
// Allowlist-based: anything NOT explicitly allowed is redacted.
|
|
5
|
+
// Default redaction set covers personal goals, faith, weight, money,
|
|
6
|
+
// named people, project names, dates. Per ui-ux-audit: "redaction must
|
|
7
|
+
// feel trustworthy, not paranoid." Pure function — takes content +
|
|
8
|
+
// settings, returns redacted content + per-field provenance.
|
|
9
|
+
|
|
10
|
+
const DEFAULT_REDACTIONS = {
|
|
11
|
+
names: true,
|
|
12
|
+
faith: true,
|
|
13
|
+
weight: true,
|
|
14
|
+
money: true,
|
|
15
|
+
projectNames: true,
|
|
16
|
+
exactDates: false,
|
|
17
|
+
fileNames: true,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Patterns that match commonly sensitive content. Each one returns a
|
|
21
|
+
// redacted replacement keeping the structure of the original visible.
|
|
22
|
+
const PATTERNS = {
|
|
23
|
+
// Personal names — capitalized two-word sequences. Best-effort heuristic.
|
|
24
|
+
names: {
|
|
25
|
+
re: /\b([A-Z][a-z]+) ([A-Z][a-z]+)\b/g,
|
|
26
|
+
replace: () => "[NAME]",
|
|
27
|
+
},
|
|
28
|
+
// Common faith markers
|
|
29
|
+
faith: {
|
|
30
|
+
re: /\b(Fajr|Dhuhr|Asr|Maghrib|Isha|Tahajjud|Quran|Hadith|Allah|Iman|Salah|Wudu)\b/gi,
|
|
31
|
+
replace: () => "[FAITH]",
|
|
32
|
+
},
|
|
33
|
+
// Weight references (kg/lb with numbers)
|
|
34
|
+
weight: {
|
|
35
|
+
re: /\b(\d{2,3})\s*(kg|kilos?|lbs?|pounds?)\b/gi,
|
|
36
|
+
replace: () => "[WEIGHT]",
|
|
37
|
+
},
|
|
38
|
+
// Money — $, £, €, ¥ with numbers, or "X dollars/pounds"
|
|
39
|
+
money: {
|
|
40
|
+
re: /(?:[$£€¥]\s?\d[\d,.]*\b|\b\d[\d,.]*\s*(?:dollars?|pounds?|euros?|GBP|USD|EUR)\b)/gi,
|
|
41
|
+
replace: () => "[MONEY]",
|
|
42
|
+
},
|
|
43
|
+
// Project names — anything in `01_Projects/Foo/`
|
|
44
|
+
projectNames: {
|
|
45
|
+
re: /01_Projects\/([^/\s)]+)/g,
|
|
46
|
+
replace: () => "01_Projects/[PROJECT]",
|
|
47
|
+
},
|
|
48
|
+
// Exact dates YYYY-MM-DD
|
|
49
|
+
exactDates: {
|
|
50
|
+
re: /\b20\d{2}-[01]\d-[0-3]\d\b/g,
|
|
51
|
+
replace: () => "[DATE]",
|
|
52
|
+
},
|
|
53
|
+
// File names with extensions
|
|
54
|
+
fileNames: {
|
|
55
|
+
re: /\b([A-Za-z0-9_-]{2,40})\.(md|txt|js|ts|tsx|jsx|json|yaml|toml|sh)\b/g,
|
|
56
|
+
replace: (_m, base) => `[FILE].${_m.split('.').pop()}`,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Apply redactions to a string. Returns { text, replacements: { kind: count } }.
|
|
61
|
+
function redactText(text, settings = {}) {
|
|
62
|
+
const cfg = Object.assign({}, DEFAULT_REDACTIONS, settings);
|
|
63
|
+
if (!text || typeof text !== "string") return { text: text || "", replacements: {} };
|
|
64
|
+
let out = text;
|
|
65
|
+
const replacements = {};
|
|
66
|
+
for (const [key, pat] of Object.entries(PATTERNS)) {
|
|
67
|
+
if (!cfg[key]) continue;
|
|
68
|
+
let count = 0;
|
|
69
|
+
out = out.replace(pat.re, (...args) => {
|
|
70
|
+
count++;
|
|
71
|
+
return pat.replace(...args);
|
|
72
|
+
});
|
|
73
|
+
if (count > 0) replacements[key] = count;
|
|
74
|
+
}
|
|
75
|
+
return { text: out, replacements };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Redact a Phantom share card payload — strips names from stated/demonstrated
|
|
79
|
+
// while preserving severity + source counts.
|
|
80
|
+
function redactPhantomCard(card, settings = {}) {
|
|
81
|
+
const stated = redactText(card.stated || "", settings);
|
|
82
|
+
const demonstrated = redactText(card.demonstrated || "", settings);
|
|
83
|
+
return {
|
|
84
|
+
...card,
|
|
85
|
+
stated: stated.text,
|
|
86
|
+
demonstrated: demonstrated.text,
|
|
87
|
+
evidence: card.evidence
|
|
88
|
+
? {
|
|
89
|
+
...card.evidence,
|
|
90
|
+
sources: (card.evidence.sources || []).map((s) => ({
|
|
91
|
+
...s,
|
|
92
|
+
file: redactText(s.file || "", settings).text,
|
|
93
|
+
quote: redactText(s.quote || "", settings).text,
|
|
94
|
+
})),
|
|
95
|
+
}
|
|
96
|
+
: undefined,
|
|
97
|
+
redactionReplacements: {
|
|
98
|
+
stated: stated.replacements,
|
|
99
|
+
demonstrated: demonstrated.replacements,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Redact a full Wrapped/share data payload for export.
|
|
105
|
+
function redactWrappedPayload(payload, settings = {}) {
|
|
106
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
107
|
+
const out = JSON.parse(JSON.stringify(payload));
|
|
108
|
+
walk(out, settings);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function walk(node, settings) {
|
|
113
|
+
if (!node) return;
|
|
114
|
+
if (Array.isArray(node)) {
|
|
115
|
+
for (let i = 0; i < node.length; i++) {
|
|
116
|
+
if (typeof node[i] === "string") {
|
|
117
|
+
node[i] = redactText(node[i], settings).text;
|
|
118
|
+
} else {
|
|
119
|
+
walk(node[i], settings);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (typeof node === "object") {
|
|
125
|
+
for (const k of Object.keys(node)) {
|
|
126
|
+
if (typeof node[k] === "string") {
|
|
127
|
+
node[k] = redactText(node[k], settings).text;
|
|
128
|
+
} else {
|
|
129
|
+
walk(node[k], settings);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
DEFAULT_REDACTIONS,
|
|
137
|
+
redactText,
|
|
138
|
+
redactPhantomCard,
|
|
139
|
+
redactWrappedPayload,
|
|
140
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
// refusal-parser.js — F38. Reads ~/.mover/refusals.jsonl appended by the
|
|
8
|
+
// friction-enforcer skill. Each line: { ts, message, level, accepted }.
|
|
9
|
+
// Used for the "AI refused me N times this week" share artifact + StatsStrip tile.
|
|
10
|
+
|
|
11
|
+
function compute({ daysLookback = 30 } = {}) {
|
|
12
|
+
const p = path.join(os.homedir(), ".mover", "refusals.jsonl");
|
|
13
|
+
if (!fs.existsSync(p)) return { available: false, count: 0, count7d: 0, recent: [] };
|
|
14
|
+
let lines;
|
|
15
|
+
try {
|
|
16
|
+
lines = fs.readFileSync(p, "utf8").split("\n").filter(Boolean);
|
|
17
|
+
} catch (_) {
|
|
18
|
+
return { available: false, count: 0, count7d: 0, recent: [] };
|
|
19
|
+
}
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const dayMs = 24 * 3600 * 1000;
|
|
22
|
+
const entries = [];
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
try {
|
|
25
|
+
const j = JSON.parse(line);
|
|
26
|
+
const ts = j.ts ? Date.parse(j.ts) : null;
|
|
27
|
+
if (ts && Number.isFinite(ts)) {
|
|
28
|
+
const ageDays = (now - ts) / dayMs;
|
|
29
|
+
if (ageDays <= daysLookback) entries.push({ ts: j.ts, level: j.level || 1, message: j.message || '', accepted: !!j.accepted, ageDays });
|
|
30
|
+
}
|
|
31
|
+
} catch (_) { /* skip malformed lines */ }
|
|
32
|
+
}
|
|
33
|
+
const count7d = entries.filter(e => e.ageDays <= 7).length;
|
|
34
|
+
const count30d = entries.length;
|
|
35
|
+
return {
|
|
36
|
+
available: true,
|
|
37
|
+
count: count30d,
|
|
38
|
+
count7d,
|
|
39
|
+
delta7d: count7d - entries.filter(e => e.ageDays > 7 && e.ageDays <= 14).length,
|
|
40
|
+
recent: entries.slice(-10).reverse()
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { compute };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// regenerate-manifest.js — V5 Phase 41.0.8.
|
|
5
|
+
//
|
|
6
|
+
// Auto-regenerates ~/.mover/pattern-manifest.json after /pivot-strategy
|
|
7
|
+
// writes a new Strategy.md. Without this step, the session-start hook
|
|
8
|
+
// + dashboard hero continue to inject the V3 cached strings until the
|
|
9
|
+
// next manual `moveros doctor --rebuild-manifest` (which doesn't exist).
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node regenerate-manifest.js [vaultPath]
|
|
13
|
+
//
|
|
14
|
+
// Vault path defaults to:
|
|
15
|
+
// - first argv
|
|
16
|
+
// - ~/.mover/config.json vaultPath
|
|
17
|
+
// - walks up from cwd looking for 02_Areas/Engine/
|
|
18
|
+
//
|
|
19
|
+
// Preserves these fields from the existing manifest (append-only):
|
|
20
|
+
// override_history, recent_wins, active_patterns (if newer than file mtime),
|
|
21
|
+
// zombie_tasks (the workflow updates these via a different path)
|
|
22
|
+
//
|
|
23
|
+
// Re-derived from Strategy.md:
|
|
24
|
+
// single_test, single_test_status, single_test_deadline, sacrifice,
|
|
25
|
+
// sacrifice_status, friction_level, friction_level_reason
|
|
26
|
+
//
|
|
27
|
+
// Safe to call from /pivot-strategy workflow Bash step. Best-effort —
|
|
28
|
+
// failures print to stderr but exit 1 only on truly fatal errors
|
|
29
|
+
// (no Strategy.md found, no vault path resolvable).
|
|
30
|
+
|
|
31
|
+
const fs = require("fs");
|
|
32
|
+
const path = require("path");
|
|
33
|
+
const os = require("os");
|
|
34
|
+
|
|
35
|
+
function resolveVaultPath(argv) {
|
|
36
|
+
if (argv && argv.length > 2) {
|
|
37
|
+
const p = argv[2];
|
|
38
|
+
if (fs.existsSync(path.join(p, "02_Areas", "Engine"))) return p;
|
|
39
|
+
}
|
|
40
|
+
const cfgPath = path.join(os.homedir(), ".mover", "config.json");
|
|
41
|
+
if (fs.existsSync(cfgPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
44
|
+
if (cfg.vaultPath && fs.existsSync(path.join(cfg.vaultPath, "02_Areas", "Engine"))) {
|
|
45
|
+
return cfg.vaultPath;
|
|
46
|
+
}
|
|
47
|
+
} catch (_) { /* fall through */ }
|
|
48
|
+
}
|
|
49
|
+
let cur = process.cwd();
|
|
50
|
+
for (let i = 0; i < 6; i++) {
|
|
51
|
+
if (fs.existsSync(path.join(cur, "02_Areas", "Engine"))) return cur;
|
|
52
|
+
const next = path.dirname(cur);
|
|
53
|
+
if (next === cur) break;
|
|
54
|
+
cur = next;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readSection(text, sectionRegex) {
|
|
60
|
+
// Extract from "## N. <heading>" until next "## " or end of file.
|
|
61
|
+
const startMatch = text.match(sectionRegex);
|
|
62
|
+
if (!startMatch) return null;
|
|
63
|
+
const startIdx = startMatch.index + startMatch[0].length;
|
|
64
|
+
const tail = text.slice(startIdx);
|
|
65
|
+
const endMatch = tail.match(/\n##\s/);
|
|
66
|
+
return endMatch ? tail.slice(0, endMatch.index) : tail;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractField(section, labelRegex) {
|
|
70
|
+
if (!section) return null;
|
|
71
|
+
// Match "**Label:** value" lines OR "* **Label:** value" bullets.
|
|
72
|
+
const m = section.match(new RegExp(`\\*?\\*?${labelRegex}:?\\*?\\*?\\s+(.+?)(?=\\n\\*?\\*?\\w+:|\\n\\n|$)`, "is"));
|
|
73
|
+
if (m) return m[1].trim().replace(/\s+/g, " ");
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractSingleTest(strategy) {
|
|
78
|
+
// Pass 4 BLOCKER fix: parser was looking for "**The Bet:**" / "**Single Test:**"
|
|
79
|
+
// headers that V4 Strategy.md doesn't use. Actual V4 format:
|
|
80
|
+
// **Mid-term Milestone:** <N activated installs + M referrals by date>...
|
|
81
|
+
// **The Positioning (V4):**
|
|
82
|
+
// - **Hero:** "..."
|
|
83
|
+
// Prefer Mid-term Milestone (machine-stable measurable target), fall back
|
|
84
|
+
// to legacy headers if found.
|
|
85
|
+
const milestoneMatch = strategy.match(/\*\*Mid-term Milestone:\*\*\s+(.+?)(?=\n\*\*|\n##|$)/is);
|
|
86
|
+
if (milestoneMatch) return milestoneMatch[1].trim().replace(/\s+/g, " ");
|
|
87
|
+
|
|
88
|
+
// Legacy header support (older Strategy.md versions still parse).
|
|
89
|
+
const sec = readSection(strategy, /##\s*1\.\s*(BUSINESS\s+)?HYPOTHESIS/i) ||
|
|
90
|
+
readSection(strategy, /##\s*1\./);
|
|
91
|
+
if (sec) {
|
|
92
|
+
const betMatch = sec.match(/\*\*(?:The Bet|Single Test):\*\*\s+(.+?)(?=\n\*\*\w|\n\n##|\n>\s|$)/is);
|
|
93
|
+
if (betMatch) return betMatch[1].trim().replace(/\s+/g, " ");
|
|
94
|
+
const hypoMatch = sec.match(/\*\*Hypothesis:\*\*\s+(.+?)(?=\n\*\*\w|\n\n##|$)/is);
|
|
95
|
+
if (hypoMatch) return hypoMatch[1].trim().replace(/\s+/g, " ");
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractSacrifice(strategy) {
|
|
101
|
+
// Pass 4 BLOCKER fix: V4 uses "**The Sacrifice (V4 — hardened past V3.0):**"
|
|
102
|
+
// which has a parenthetical that the old regex didn't match. The header
|
|
103
|
+
// can also be followed by a bulleted list rather than inline text.
|
|
104
|
+
// Strategy: find the header anywhere (Section 1 or 2), then capture the
|
|
105
|
+
// first bullet point OR inline text until the next bold header / next section.
|
|
106
|
+
//
|
|
107
|
+
// First try: full bullet list under the header (V4 actual structure).
|
|
108
|
+
const sacrHeaderMatch = strategy.match(/\*\*The Sacrifice[^*]*\*\*\s*\n([\s\S]+?)(?=\n\*\*\w[^*]*\*\*|\n##\s|$)/i);
|
|
109
|
+
if (sacrHeaderMatch) {
|
|
110
|
+
// Collapse the bullet list into a single human-readable string.
|
|
111
|
+
// Take first 2-3 bullets, strip markdown bullet markers.
|
|
112
|
+
const bullets = sacrHeaderMatch[1]
|
|
113
|
+
.split(/\n/)
|
|
114
|
+
.map(l => l.replace(/^\s*[*-]\s+/, "").trim())
|
|
115
|
+
.filter(l => l && !l.startsWith("**"))
|
|
116
|
+
.slice(0, 4);
|
|
117
|
+
if (bullets.length) return bullets.join(" · ").replace(/\s+/g, " ").slice(0, 400);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fall back: legacy inline format.
|
|
121
|
+
const sec = readSection(strategy, /##\s*2\./);
|
|
122
|
+
if (sec) {
|
|
123
|
+
return extractField(sec, "(?:The Sacrifice|Sacrifice)") ||
|
|
124
|
+
extractField(sec, "What I am NOT doing");
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractFrictionLevel(strategy) {
|
|
130
|
+
// Pass 4 BLOCKER fix: V4 uses prose like "L3 friction floor active from Day 1"
|
|
131
|
+
// — not a labeled "Friction Level: L3" line. Accept both formats.
|
|
132
|
+
// First try labeled form, then prose "L<N> friction" / "friction L<N>".
|
|
133
|
+
const labeled = strategy.match(/Friction\s+Level\s*[:=]\s*L?(\d)/i);
|
|
134
|
+
if (labeled) return parseInt(labeled[1], 10);
|
|
135
|
+
const proseFloor = strategy.match(/\bL([0-5])\s+friction\s+(?:floor|level|active)/i);
|
|
136
|
+
if (proseFloor) return parseInt(proseFloor[1], 10);
|
|
137
|
+
const proseLevel = strategy.match(/\bfriction\s+(?:level|floor)\s+L?([0-5])/i);
|
|
138
|
+
if (proseLevel) return parseInt(proseLevel[1], 10);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractStrategyVersion(strategy) {
|
|
143
|
+
const m = strategy.match(/^\*\*Version:\*\*\s*(.+)$/m);
|
|
144
|
+
return m ? m[1].trim() : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function regenerate(vaultPath) {
|
|
148
|
+
const strategyPath = path.join(vaultPath, "02_Areas", "Engine", "Strategy.md");
|
|
149
|
+
if (!fs.existsSync(strategyPath)) {
|
|
150
|
+
console.error(`[regenerate-manifest] Strategy.md not found at ${strategyPath}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const strategy = fs.readFileSync(strategyPath, "utf8");
|
|
154
|
+
const manifestPath = path.join(os.homedir(), ".mover", "pattern-manifest.json");
|
|
155
|
+
|
|
156
|
+
let existing = {};
|
|
157
|
+
if (fs.existsSync(manifestPath)) {
|
|
158
|
+
try { existing = JSON.parse(fs.readFileSync(manifestPath, "utf8")); }
|
|
159
|
+
catch (_) { existing = {}; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const singleTest = extractSingleTest(strategy);
|
|
163
|
+
const sacrifice = extractSacrifice(strategy);
|
|
164
|
+
const frictionLevel = extractFrictionLevel(strategy);
|
|
165
|
+
const strategyVersion = extractStrategyVersion(strategy);
|
|
166
|
+
|
|
167
|
+
if (!singleTest && !sacrifice) {
|
|
168
|
+
console.error(`[regenerate-manifest] could not extract Single Test or Sacrifice from Strategy.md — manifest unchanged`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const next = {
|
|
173
|
+
...existing,
|
|
174
|
+
version: 1,
|
|
175
|
+
last_updated: new Date().toISOString().slice(0, 16),
|
|
176
|
+
updated_by: "regenerate-manifest.js",
|
|
177
|
+
strategy_version: strategyVersion || existing.strategy_version || null,
|
|
178
|
+
// Re-derived fields
|
|
179
|
+
...(singleTest ? { single_test: singleTest } : {}),
|
|
180
|
+
...(sacrifice ? { sacrifice: sacrifice } : {}),
|
|
181
|
+
...(frictionLevel != null ? { friction_level: frictionLevel } : {}),
|
|
182
|
+
// Preserved fields (append-only)
|
|
183
|
+
override_history: Array.isArray(existing.override_history) ? existing.override_history : [],
|
|
184
|
+
recent_wins: Array.isArray(existing.recent_wins) ? existing.recent_wins : [],
|
|
185
|
+
active_patterns: Array.isArray(existing.active_patterns) ? existing.active_patterns : [],
|
|
186
|
+
zombie_tasks: Array.isArray(existing.zombie_tasks) ? existing.zombie_tasks : [],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Atomic tmp+rename
|
|
190
|
+
const tmp = `${manifestPath}.tmp-${process.pid}-${Date.now()}`;
|
|
191
|
+
fs.writeFileSync(tmp, JSON.stringify(next, null, 2), "utf8");
|
|
192
|
+
fs.renameSync(tmp, manifestPath);
|
|
193
|
+
|
|
194
|
+
console.log(`Pattern manifest regenerated: single_test="${(next.single_test || "—").slice(0, 60)}…", sacrifice="${(next.sacrifice || "—").slice(0, 60)}…", friction_level=L${next.friction_level || 0}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (require.main === module) {
|
|
198
|
+
const vault = resolveVaultPath(process.argv);
|
|
199
|
+
if (!vault) {
|
|
200
|
+
console.error(`[regenerate-manifest] could not resolve vault path. Pass as first arg or set vaultPath in ~/.mover/config.json`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
regenerate(vault);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { regenerate, extractSingleTest, extractSacrifice, extractFrictionLevel };
|