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.
Files changed (119) hide show
  1. package/README.md +34 -24
  2. package/install.js +2868 -251
  3. package/package.json +15 -3
  4. package/src/dashboard/build.js +1541 -0
  5. package/src/dashboard/dashboard.js +276 -0
  6. package/src/dashboard/index.js +319 -0
  7. package/src/dashboard/lib/activation-log.js +297 -0
  8. package/src/dashboard/lib/active-context-parser.js +189 -0
  9. package/src/dashboard/lib/agent-command.js +93 -0
  10. package/src/dashboard/lib/agent-detect.js +255 -0
  11. package/src/dashboard/lib/agent-detector.js +92 -0
  12. package/src/dashboard/lib/agent-session.js +483 -0
  13. package/src/dashboard/lib/anti-identity-detector.js +116 -0
  14. package/src/dashboard/lib/approval-registry.js +170 -0
  15. package/src/dashboard/lib/auto-learnings-parser.js +183 -0
  16. package/src/dashboard/lib/cli-usage-parser.js +92 -0
  17. package/src/dashboard/lib/config-parser.js +109 -0
  18. package/src/dashboard/lib/connect-recommender.js +131 -0
  19. package/src/dashboard/lib/correlations-parser.js +231 -0
  20. package/src/dashboard/lib/daily-note-resolver.js +228 -0
  21. package/src/dashboard/lib/date-utils.js +43 -0
  22. package/src/dashboard/lib/distribution-parser.js +137 -0
  23. package/src/dashboard/lib/dossier-parser.js +64 -0
  24. package/src/dashboard/lib/drift-history.js +88 -0
  25. package/src/dashboard/lib/drift-score.js +119 -0
  26. package/src/dashboard/lib/engine-default-fingerprints.json +53 -0
  27. package/src/dashboard/lib/engine-health.js +173 -0
  28. package/src/dashboard/lib/engine-writer.js +1831 -0
  29. package/src/dashboard/lib/execution-plan.js +125 -0
  30. package/src/dashboard/lib/experiments-parser.js +429 -0
  31. package/src/dashboard/lib/feed-parser.js +294 -0
  32. package/src/dashboard/lib/forked-future.js +60 -0
  33. package/src/dashboard/lib/goal-forecast.js +427 -0
  34. package/src/dashboard/lib/goals-parser.js +67 -0
  35. package/src/dashboard/lib/health-protocols-parser.js +133 -0
  36. package/src/dashboard/lib/hook-activity.js +48 -0
  37. package/src/dashboard/lib/hook-indexer.js +169 -0
  38. package/src/dashboard/lib/hourly-activity-parser.js +143 -0
  39. package/src/dashboard/lib/identity-parser.js +85 -0
  40. package/src/dashboard/lib/ingestion.js +418 -0
  41. package/src/dashboard/lib/library-indexer-v2.js +226 -0
  42. package/src/dashboard/lib/library-indexer.js +105 -0
  43. package/src/dashboard/lib/library-search.js +290 -0
  44. package/src/dashboard/lib/log-activation.sh +61 -0
  45. package/src/dashboard/lib/memory-curator.js +97 -0
  46. package/src/dashboard/lib/memory-gardener.js +177 -0
  47. package/src/dashboard/lib/memory-gepa.js +102 -0
  48. package/src/dashboard/lib/memory-index.js +519 -0
  49. package/src/dashboard/lib/memory-rerank.js +72 -0
  50. package/src/dashboard/lib/memory-text.js +136 -0
  51. package/src/dashboard/lib/metrics-log-parser.js +76 -0
  52. package/src/dashboard/lib/moves-usage-parser.js +184 -0
  53. package/src/dashboard/lib/onboarding-forge.js +70 -0
  54. package/src/dashboard/lib/override-outcome-parser.js +68 -0
  55. package/src/dashboard/lib/override-summary.js +73 -0
  56. package/src/dashboard/lib/paths.js +192 -0
  57. package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
  58. package/src/dashboard/lib/phantom-strategy.js +129 -0
  59. package/src/dashboard/lib/pid-markers.js +80 -0
  60. package/src/dashboard/lib/project-scanner.js +121 -0
  61. package/src/dashboard/lib/promise-wall.js +88 -0
  62. package/src/dashboard/lib/record-score.js +173 -0
  63. package/src/dashboard/lib/redaction.js +140 -0
  64. package/src/dashboard/lib/refusal-parser.js +44 -0
  65. package/src/dashboard/lib/regenerate-manifest.js +206 -0
  66. package/src/dashboard/lib/rewind-snapshots.js +689 -0
  67. package/src/dashboard/lib/roast-wall-parser.js +200 -0
  68. package/src/dashboard/lib/run-registry.js +286 -0
  69. package/src/dashboard/lib/safe-write.js +63 -0
  70. package/src/dashboard/lib/session-log-parser.js +145 -0
  71. package/src/dashboard/lib/session-time-parser.js +158 -0
  72. package/src/dashboard/lib/skill-index.js +171 -0
  73. package/src/dashboard/lib/skill-indexer.js +118 -0
  74. package/src/dashboard/lib/skill-recommender.js +689 -0
  75. package/src/dashboard/lib/state-core/backfill.js +298 -0
  76. package/src/dashboard/lib/state-core/dryrun.js +188 -0
  77. package/src/dashboard/lib/state-core/event-log.js +615 -0
  78. package/src/dashboard/lib/state-core/events.js +265 -0
  79. package/src/dashboard/lib/state-core/projections.js +376 -0
  80. package/src/dashboard/lib/state-core/start-close.js +162 -0
  81. package/src/dashboard/lib/state-core/trial.js +96 -0
  82. package/src/dashboard/lib/strategy-parser.js +248 -0
  83. package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
  84. package/src/dashboard/lib/streak-parser.js +95 -0
  85. package/src/dashboard/lib/suggested-now.js +254 -0
  86. package/src/dashboard/lib/tool-awareness.js +125 -0
  87. package/src/dashboard/lib/transcript-parser.js +371 -0
  88. package/src/dashboard/lib/vault-graph-parser.js +205 -0
  89. package/src/dashboard/lib/view-generator.js +163 -0
  90. package/src/dashboard/lib/voice-dna-parser.js +57 -0
  91. package/src/dashboard/lib/walkthrough-script.js +140 -0
  92. package/src/dashboard/lib/workflow-graph-parser.js +170 -0
  93. package/src/dashboard/lib/workflow-library-parser.js +146 -0
  94. package/src/dashboard/server.js +2402 -0
  95. package/src/dashboard/shortcut.js +284 -0
  96. package/src/dashboard/static/setup-poc.html +306 -0
  97. package/src/dashboard/static/walkthrough-poc.html +580 -0
  98. package/src/dashboard/styles.css +1201 -0
  99. package/src/dashboard/templates/index.html +278 -0
  100. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
  101. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
  102. package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
  103. package/src/dashboard/ui/dist/assets/hero.png +0 -0
  104. package/src/dashboard/ui/dist/assets/index-ByVKPvLf.js +34 -0
  105. package/src/dashboard/ui/dist/assets/index-CCoKjUcC.js +161 -0
  106. package/src/dashboard/ui/dist/assets/index-CZWNQDt5.css +1 -0
  107. package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  108. package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  109. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  110. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  111. package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  112. package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
  113. package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
  114. package/src/dashboard/ui/dist/icon.svg +4 -0
  115. package/src/dashboard/ui/dist/index.html +18 -0
  116. package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
  117. package/src/dashboard/ui/dist/registerSW.js +1 -0
  118. package/src/dashboard/ui/dist/sw.js +1 -0
  119. 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 };