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,163 @@
1
+ "use strict";
2
+
3
+ // View Generator MVP — wishlist #64, the categorical differentiator.
4
+ //
5
+ // "Mover OS reads your vault, sessions, strategy, health notes, and agent
6
+ // history. Then it generates the view you need for the job you are doing."
7
+ //
8
+ // V1 design: rather than a full LLM-driven layout system, this MVP:
9
+ // 1. Accepts a free-text prompt ("make me a content tracker from current goals")
10
+ // 2. Bundles relevant vault context (Goals + Strategy + Active_Context + plan files)
11
+ // 3. Returns a STRUCTURED VIEW DEFINITION — JSON with sections, queries,
12
+ // and render hints — that the dashboard can interpret to build a
13
+ // bespoke layout from existing parsers.
14
+ // 4. Persists view definitions to ~/.mover/views/<slug>.json so users
15
+ // can return to them.
16
+ //
17
+ // V2 (multi-week): wire to claude-code / codex CLI to actually generate
18
+ // the view definition via LLM. For now: a templating engine that maps
19
+ // keywords in the prompt to known view shapes ('content', 'metric',
20
+ // 'project', 'pattern', 'distribution'). Honest MVP.
21
+ //
22
+ // The radical idea is that the dashboard layout is NOT FIXED. Same
23
+ // underlying parsers, different views per job-to-be-done.
24
+
25
+ const fs = require("fs");
26
+ const path = require("path");
27
+ const os = require("os");
28
+
29
+ const VIEWS_DIR = path.join(os.homedir(), ".mover", "views");
30
+
31
+ // Keyword → view template mapping. Each template names which state.json
32
+ // fields to render and in what order. Extend as more shapes emerge.
33
+ const VIEW_TEMPLATES = {
34
+ content: {
35
+ sections: [
36
+ { kind: "text", source: "strategy.singleTest", label: "Current bet" },
37
+ { kind: "list", source: "projects", filter: "tasks", label: "Active projects with tasks" },
38
+ { kind: "list", source: "library.inputs", limit: 10, label: "Recent Inputs (research)" },
39
+ ],
40
+ },
41
+ metric: {
42
+ sections: [
43
+ { kind: "number", source: "driftScore.value", label: "Drift score" },
44
+ { kind: "sparkline", source: "driftScore.history", label: "30-day trend" },
45
+ { kind: "list", source: "metrics.rolling30", label: "Rolling 30-day metrics" },
46
+ ],
47
+ },
48
+ project: {
49
+ sections: [
50
+ { kind: "list", source: "projects", filter: "all", label: "All projects" },
51
+ { kind: "list", source: "activeContext.commitments", label: "Outstanding commitments" },
52
+ ],
53
+ },
54
+ pattern: {
55
+ sections: [
56
+ { kind: "list", source: "activeContext.activePatterns", label: "Active patterns" },
57
+ { kind: "list", source: "roastWall.roasts", limit: 10, label: "Recent verdicts" },
58
+ { kind: "table", source: "correlations.correlations", label: "Black Box correlations" },
59
+ ],
60
+ },
61
+ distribution: {
62
+ sections: [
63
+ { kind: "text", source: "strategy.singleTest", label: "Sale gate" },
64
+ { kind: "list", source: "activeContext.commitments", filter: "distribution", label: "Distribution commitments" },
65
+ { kind: "number", source: "activation.installDays", label: "Days since install" },
66
+ ],
67
+ },
68
+ habit: {
69
+ sections: [
70
+ { kind: "heatmap", source: "streak.days", label: "Daily streak" },
71
+ { kind: "number", source: "streak.currentStreak", label: "Current streak" },
72
+ { kind: "table", source: "correlations.correlations", label: "What helps / hurts" },
73
+ ],
74
+ },
75
+ };
76
+
77
+ const KEYWORD_TO_TEMPLATE = [
78
+ { keywords: ["content", "post", "article", "tweet", "reel", "video"], template: "content" },
79
+ { keywords: ["metric", "kpi", "drift", "score", "trend"], template: "metric" },
80
+ { keywords: ["project", "deliverable", "milestone", "feature"], template: "project" },
81
+ { keywords: ["pattern", "behavior", "habit pattern", "avoidance", "leak"], template: "pattern" },
82
+ { keywords: ["distribution", "marketing", "sales", "dms", "reach"], template: "distribution" },
83
+ { keywords: ["habit", "routine", "streak", "vitality", "sleep"], template: "habit" },
84
+ ];
85
+
86
+ function chooseTemplate(prompt) {
87
+ const lower = String(prompt || "").toLowerCase();
88
+ for (const { keywords, template } of KEYWORD_TO_TEMPLATE) {
89
+ if (keywords.some(k => lower.includes(k))) return template;
90
+ }
91
+ // Default: pattern view (most generally useful)
92
+ return "pattern";
93
+ }
94
+
95
+ function slugify(text) {
96
+ return String(text || "view")
97
+ .toLowerCase()
98
+ .replace(/[^a-z0-9]+/g, "-")
99
+ .replace(/^-+|-+$/g, "")
100
+ .slice(0, 60) || "view";
101
+ }
102
+
103
+ function generate({ prompt, save = true } = {}) {
104
+ if (!prompt) {
105
+ return { ok: false, error: "prompt required" };
106
+ }
107
+ const templateKey = chooseTemplate(prompt);
108
+ const template = VIEW_TEMPLATES[templateKey];
109
+ const slug = slugify(prompt);
110
+ const view = {
111
+ version: 1,
112
+ prompt,
113
+ template: templateKey,
114
+ slug,
115
+ createdAt: new Date().toISOString(),
116
+ sections: template.sections,
117
+ };
118
+ if (save) {
119
+ try {
120
+ fs.mkdirSync(VIEWS_DIR, { recursive: true });
121
+ const dest = path.join(VIEWS_DIR, `${slug}.json`);
122
+ fs.writeFileSync(dest, JSON.stringify(view, null, 2), "utf8");
123
+ view.path = dest;
124
+ } catch (e) {
125
+ view.saveError = e.message;
126
+ }
127
+ }
128
+ return { ok: true, view };
129
+ }
130
+
131
+ function list() {
132
+ try {
133
+ if (!fs.existsSync(VIEWS_DIR)) return [];
134
+ return fs.readdirSync(VIEWS_DIR)
135
+ .filter(f => f.endsWith(".json"))
136
+ .map(f => {
137
+ try {
138
+ return JSON.parse(fs.readFileSync(path.join(VIEWS_DIR, f), "utf8"));
139
+ } catch { return null; }
140
+ })
141
+ .filter(Boolean);
142
+ } catch {
143
+ return [];
144
+ }
145
+ }
146
+
147
+ function get(slug) {
148
+ try {
149
+ const file = path.join(VIEWS_DIR, `${slug}.json`);
150
+ if (!fs.existsSync(file)) return null;
151
+ return JSON.parse(fs.readFileSync(file, "utf8"));
152
+ } catch { return null; }
153
+ }
154
+
155
+ function remove(slug) {
156
+ try {
157
+ const file = path.join(VIEWS_DIR, `${slug}.json`);
158
+ if (fs.existsSync(file)) fs.unlinkSync(file);
159
+ return true;
160
+ } catch { return false; }
161
+ }
162
+
163
+ module.exports = { generate, list, get, remove, VIEW_TEMPLATES, chooseTemplate };
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ const { voiceDnaPath, safeRead, safeStat } = require("./paths");
4
+
5
+ // Parse Voice_DNA.md — extract writing samples, banned phrases, tone scores, sentence patterns.
6
+
7
+ function parseVoiceDna(vault) {
8
+ const filePath = voiceDnaPath(vault);
9
+ const text = safeRead(filePath);
10
+ const stat = safeStat(filePath);
11
+ if (!text) return { available: false };
12
+
13
+ const out = {
14
+ available: true,
15
+ filePath,
16
+ lastUpdated: stat ? stat.mtime : null,
17
+ raw: text,
18
+ samples: [],
19
+ fields: {}
20
+ };
21
+
22
+ // Bold field extraction
23
+ const re = /\*\*([^*]+):\*\*\s*([^\n]+)/g;
24
+ let m;
25
+ while ((m = re.exec(text)) !== null) {
26
+ const k = m[1].trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_|_$/g, "");
27
+ out.fields[k] = m[2].trim();
28
+ }
29
+
30
+ // Extract writing samples — code blocks ``` ... ```
31
+ const sampleRe = /```\s*([\s\S]*?)\s*```/g;
32
+ while ((m = sampleRe.exec(text)) !== null) {
33
+ out.samples.push(m[1].trim());
34
+ }
35
+
36
+ // Common patterns
37
+ out.lowercaseI = /lowercase\s*[\"']?i[\"']?/i.test(text);
38
+ out.commaSpliced = /comma[-\s]*spliced/i.test(text);
39
+ out.usesBrother = /\bbrother\b/i.test(text);
40
+ out.urlInline = /url[-\s]inline|inline\s*url/i.test(text);
41
+
42
+ // Banned phrases section
43
+ const bannedSection = text.match(/##+\s*Banned[^\n]*\n([\s\S]*?)(?=\n##+|\n---|\n$)/i);
44
+ if (bannedSection) {
45
+ out.bannedPhrases = bannedSection[1].split("\n")
46
+ .map(l => l.match(/^[-*]\s+(.+)$/))
47
+ .filter(Boolean).map(m => m[1].trim()).slice(0, 12);
48
+ } else out.bannedPhrases = [];
49
+
50
+ out.totalSamples = out.samples.length;
51
+ out.shortestSample = out.samples.reduce((min, s) => Math.min(min, s.length), Infinity);
52
+ out.longestSample = out.samples.reduce((max, s) => Math.max(max, s.length), 0);
53
+
54
+ return out;
55
+ }
56
+
57
+ module.exports = { parseVoiceDna };
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * walkthrough-script.js — structured walkthrough data for the Mover OS
5
+ * onboarding engine (onboarding-engine-SPEC.md §5 D6).
6
+ *
7
+ * The walkthrough runs AFTER setup. Its job is not to explain the system —
8
+ * the user just sat through that. Its job is to teach the user to OPERATE
9
+ * the agent: the gaps that cause 90% of day-3 drop-off:
10
+ *
11
+ * 1. Drive the LLM (don't just chat at it — run workflows)
12
+ * 2. Stay in ONE chat (session continuity is the product)
13
+ * 3. Compact when full (don't lose context — /compact before it degrades)
14
+ * 4. Verify file creation (trust the files, not the chat)
15
+ * 5. Discover capabilities (the system tells you what's next)
16
+ * 6. Strategy → Daily loop (the one rhythm that makes it self-sustaining)
17
+ * 7. Run a workflow end-to-end (/log)
18
+ * 8. Land on a first plan (/plan-tomorrow — the done-gate)
19
+ *
20
+ * Design principles (per grill findings + Mobbin "fast first-win"):
21
+ * - SHORT. 8 steps, 8 minutes. Not a lecture.
22
+ * - DOING, not explaining. Each step has a concrete `doThis` action.
23
+ * - `checkpoint` is HOW the agent verifies the user actually did it.
24
+ * - No recap of Engine files — setup covered that. This is ops, not orientation.
25
+ * - SENTINEL: when all 8 steps are confirmed, agent emits [[WALKTHROUGH_COMPLETE]]
26
+ * so the GUI can fire the done banner (mirrors setup-poc.html's [[SETUP_COMPLETE]]).
27
+ */
28
+
29
+ // ── System prompt for the walkthrough agent persona ──────────────────────────
30
+ // Different from DEFAULT_SETUP_SYSTEM: the walkthrough agent is a GUIDE,
31
+ // not an interviewer. It gives instructions, waits, confirms, and moves on.
32
+ // It NEVER explains setup material again. It is brief and operational.
33
+ const WALKTHROUGH_SYSTEM = [
34
+ "You are the Mover OS walkthrough guide.",
35
+ "Your ONLY job: teach the user to operate the system in 8 steps, taking no more than 8 minutes total.",
36
+ "Do NOT re-explain what Mover OS is, what Engine files are, or what setup covered.",
37
+ "Each turn: acknowledge the user's last action in ONE sentence, then give the next step's instruction clearly.",
38
+ "Keep every response under 4 sentences. No exceptions.",
39
+ "Do NOT use numbered lists, bullet points, or headings in your responses. Prose only.",
40
+ "Do NOT run any tools and do NOT read files — you have no tools in this session.",
41
+ "You may reference file names (like Active_Context.md or plan.md) but never their contents.",
42
+ "Tone: warm but fast. A co-founder showing someone the system in a short video call, not a teacher at a whiteboard.",
43
+ "When the user confirms they completed step 8 (running /plan-tomorrow and having a plan for tomorrow), emit the exact token [[WALKTHROUGH_COMPLETE]] on its own line, then write one sentence that says they are ready to operate the system.",
44
+ "CRITICAL: Only emit [[WALKTHROUGH_COMPLETE]] after the user has confirmed step 8. Not before.",
45
+ ].join(" ");
46
+
47
+ // ── Walkthrough steps ─────────────────────────────────────────────────────────
48
+ // Each step is self-contained: title + body for the card UI, doThis for the
49
+ // concrete action the user takes, checkpoint for how the guide verifies it.
50
+ //
51
+ // The guide reads these steps and drives the conversation through them in order.
52
+ // The GUI renders each step as a card BEFORE the agent turn so the user can
53
+ // read the instruction while the agent is composing its confirmation.
54
+
55
+ const STEPS = [
56
+ {
57
+ id: "welcome",
58
+ step: 1,
59
+ title: "You're set up. Now let's operate.",
60
+ body: "Setup extracted who you are and built your Engine. This walkthrough teaches you how to RUN it. Eight steps, eight minutes. No reading — only doing.",
61
+ doThis: "Type \"ready\" to begin.",
62
+ checkpoint: "User says they are ready or asks a question. Either counts — they are engaged.",
63
+ },
64
+
65
+ {
66
+ id: "first-workflow",
67
+ step: 2,
68
+ title: "Drive the system with workflows.",
69
+ body: "This is not a chatbot. You run commands. The core command is /morning — it loads your Engine files, checks your energy, and sets your Single Test for the session. Without /morning, you are flying blind.",
70
+ doThis: "Open a Claude Code session (a terminal, type `claude`). Run: /morning",
71
+ checkpoint: "User confirms they ran /morning, or asks where Claude Code is. If they ask, tell them: open a terminal, type `claude`, press Enter, then type /morning.",
72
+ },
73
+
74
+ {
75
+ id: "one-chat",
76
+ step: 3,
77
+ title: "One session. Not ten tabs.",
78
+ body: "Every new Claude Code window starts from zero — no memory of the last one. Your Engine files ARE the persistent memory. Opening a new window mid-task means re-loading all that context, which costs tokens and minutes. Continuity lives in the files, not the window.",
79
+ doThis: "For today: stay in the Claude Code session where you ran /morning. Do not open a new window unless you run /morning first.",
80
+ checkpoint: "User acknowledges the habit. No file action needed — they are committing to a behavior, not running a command.",
81
+ },
82
+
83
+ {
84
+ id: "compact",
85
+ step: 4,
86
+ title: "Compact before context breaks down.",
87
+ body: "After a long session, responses get shallower — the model is losing the thread. /compact compresses the conversation while re-loading your Engine context. It is not 'start over' — it is 'keep going with a clear head'. Use it when responses feel off or surface-level.",
88
+ doThis: "In your Claude Code session: type /compact. If the session is short, that is fine — run it anyway so you know what it does.",
89
+ checkpoint: "User ran /compact, or confirms they understand when to use it. If their session is short, acknowledge that is the correct read — they understand the trigger condition.",
90
+ },
91
+
92
+ {
93
+ id: "verify-files",
94
+ step: 5,
95
+ title: "The files are the truth. Not the chat.",
96
+ body: "The chat transcript is temporary — it disappears when you close the session. Every workflow writes its output to a file. Your Daily Note, Active_Context.md, plan.md — those are permanent. If a workflow ran and nothing was written, something went wrong. Always verify: did that actually land in a file?",
97
+ doThis: "Open Obsidian. Navigate to 02_Areas/Engine/. Confirm Active_Context.md exists and has content from setup. That is your proof the system is working.",
98
+ checkpoint: "User confirms they saw Active_Context.md with content. If they cannot find it, ask for their vault path and guide them to 02_Areas/Engine/.",
99
+ },
100
+
101
+ {
102
+ id: "discover",
103
+ step: 6,
104
+ title: "Every workflow tells you what comes next.",
105
+ body: "You do not need to memorize 25 commands. Every workflow ends with a handoff — a specific suggestion for what to run next. Follow the handoffs. Today that chain is: /morning → work → /log → /analyse-day → /plan-tomorrow. You already ran /morning. /log is next.",
106
+ doThis: "In your Claude Code session: type /log — the end-of-session capture. Read its output. Note what it suggests you run after it.",
107
+ checkpoint: "User ran /log and reports what it suggested. Any answer is correct — they have now seen the handoff pattern live.",
108
+ },
109
+
110
+ {
111
+ id: "daily-loop",
112
+ step: 7,
113
+ title: "The loop that keeps the system alive.",
114
+ body: "Run this every day and the system compounds. Skip it and it decays. The loop: /morning to start → do your work → /log to capture → /analyse-day to audit → /plan-tomorrow to set tomorrow. This is not optional ceremony. This is the product.",
115
+ doThis: "Say the loop back from memory — just the five steps.",
116
+ checkpoint: "User gives some version of: morning, work, log, analyse-day, plan-tomorrow. Exact words do not matter. If they miss a step, name it once, do not lecture. Then advance.",
117
+ },
118
+
119
+ {
120
+ id: "first-plan",
121
+ step: 8,
122
+ title: "Close the loop before you leave.",
123
+ body: "Do not walk away from setup and walkthrough without a plan for tomorrow. /plan-tomorrow pulls from your strategy, your backlogs, and what happened today. It gives you a clear first task for tomorrow morning. This is the bridge from 'I installed this' to 'I am actually using this'.",
124
+ doThis: "In your Claude Code session: type /plan-tomorrow. Confirm you have a plan for tomorrow when it finishes.",
125
+ checkpoint: "User confirms they ran /plan-tomorrow and have a plan. This is the done-gate. When confirmed, emit [[WALKTHROUGH_COMPLETE]] and the closing line.",
126
+ },
127
+ ];
128
+
129
+ // ── Exports ───────────────────────────────────────────────────────────────────
130
+
131
+ module.exports = {
132
+ WALKTHROUGH_SYSTEM,
133
+ STEPS,
134
+ /** Total number of steps (convenience for progress indicators). */
135
+ STEP_COUNT: STEPS.length,
136
+ /** Look up a step by id (returns undefined if not found). */
137
+ getStep: (id) => STEPS.find((s) => s.id === id),
138
+ /** Get step by 1-indexed position. */
139
+ getStepByIndex: (n) => STEPS[n - 1] || null,
140
+ };
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+
3
+ // Phase 41.7 — Workflow read/write graph parser.
4
+ //
5
+ // Scans every src/workflows/*.md for references to Engine files (or other
6
+ // workflow files). Builds a directed graph: workflows are nodes, edges
7
+ // represent "reads" or "writes" relationships.
8
+ //
9
+ // Heuristic source detection: workflow content is markdown, so we look for:
10
+ // - "READS:" / "WRITES:" / "READ:" / "WRITE:" labels (preferred format)
11
+ // - Bare references to known Engine files in code-fenced or quoted contexts
12
+ // - YAML frontmatter `reads:` / `writes:` arrays
13
+ //
14
+ // The graph powers Library → Workflow Graph tab, helps users see which
15
+ // workflow updates which Engine file at a glance, and surfaces orphaned
16
+ // workflows (no reads/writes).
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+
21
+ const ENGINE_FILES = [
22
+ "Strategy.md",
23
+ "Identity_Prime.md",
24
+ "Active_Context.md",
25
+ "Architect_Dossier.md", // live filename (renamed from Mover_Dossier.md)
26
+ "Mover_Dossier.md", // legacy alias — kept so older vaults still match
27
+ "Auto_Learnings.md",
28
+ "Goals.md",
29
+ "Metrics_Log.md",
30
+ "Someday_Maybe.md",
31
+ "Daily Note",
32
+ "Daily_Template.md",
33
+ "Voice_DNA.md",
34
+ ];
35
+
36
+ function workflowsDir(bundleRoot) {
37
+ return path.join(bundleRoot, "src", "workflows");
38
+ }
39
+
40
+ function parseFrontmatter(text) {
41
+ const m = text.match(/^---\n([\s\S]*?)\n---\n?/);
42
+ if (!m) return { fm: {}, body: text };
43
+ const fmText = m[1];
44
+ const body = text.slice(m[0].length);
45
+ const fm = {};
46
+ const lines = fmText.split("\n");
47
+ let currentKey = null;
48
+ let listBuf = null;
49
+ for (const line of lines) {
50
+ if (!line.trim()) continue;
51
+ const topMatch = line.match(/^([a-z_][a-z0-9_]*)\s*:\s*(.*)$/i);
52
+ if (topMatch && !/^\s/.test(line)) {
53
+ if (currentKey && listBuf) fm[currentKey] = listBuf;
54
+ currentKey = topMatch[1];
55
+ listBuf = null;
56
+ const v = topMatch[2].trim();
57
+ if (v) {
58
+ fm[currentKey] = v.replace(/^["']|["']$/g, "");
59
+ currentKey = null;
60
+ } else {
61
+ listBuf = [];
62
+ }
63
+ } else if (currentKey && listBuf) {
64
+ const itemMatch = line.match(/^\s*-\s+(.+)$/);
65
+ if (itemMatch) listBuf.push(itemMatch[1].trim().replace(/^["']|["']$/g, ""));
66
+ }
67
+ }
68
+ if (currentKey && listBuf) fm[currentKey] = listBuf;
69
+ return { fm, body };
70
+ }
71
+
72
+ function extractFromFrontmatter(fm, key) {
73
+ const v = fm[key];
74
+ if (!v) return [];
75
+ if (Array.isArray(v)) return v;
76
+ return String(v).split(/[,;]/).map(s => s.trim()).filter(Boolean);
77
+ }
78
+
79
+ function extractFromBody(body) {
80
+ // Pattern: "READS: file1.md, file2.md" / "WRITES: file3.md"
81
+ const reads = new Set();
82
+ const writes = new Set();
83
+ for (const line of body.split("\n")) {
84
+ const r = line.match(/READS?:\s+(.+)/i);
85
+ if (r) for (const f of r[1].split(/[,;]/)) {
86
+ const trimmed = f.trim().replace(/[`*_]/g, "");
87
+ if (trimmed) reads.add(trimmed);
88
+ }
89
+ const w = line.match(/WRITES?:\s+(.+)/i);
90
+ if (w) for (const f of w[1].split(/[,;]/)) {
91
+ const trimmed = f.trim().replace(/[`*_]/g, "");
92
+ if (trimmed) writes.add(trimmed);
93
+ }
94
+ }
95
+ // Heuristic backup: scan for engine file mentions in headed contexts.
96
+ // Lower confidence — only count if file is named with explicit ".md".
97
+ for (const ef of ENGINE_FILES) {
98
+ if (ef === "Daily Note") continue;
99
+ const re = new RegExp(`\\b${ef.replace(/\./g, "\\.")}\\b`, "g");
100
+ const matches = body.match(re);
101
+ if (matches) {
102
+ // Default heuristic: if "write" appears within 80 chars before the
103
+ // file ref, count as write; otherwise read.
104
+ reads.add(ef);
105
+ }
106
+ }
107
+ return {
108
+ reads: Array.from(reads),
109
+ writes: Array.from(writes),
110
+ };
111
+ }
112
+
113
+ function parseWorkflow(filePath) {
114
+ const name = path.basename(filePath, ".md");
115
+ const text = fs.readFileSync(filePath, "utf8").replace(/\r\n/g, "\n");
116
+ const { fm, body } = parseFrontmatter(text);
117
+ const fmReads = extractFromFrontmatter(fm, "reads");
118
+ const fmWrites = extractFromFrontmatter(fm, "writes");
119
+ const bodyExtracted = extractFromBody(body);
120
+ // Frontmatter overrides body heuristics when present.
121
+ const reads = fmReads.length ? fmReads : bodyExtracted.reads;
122
+ const writes = fmWrites.length ? fmWrites : bodyExtracted.writes;
123
+ return {
124
+ name,
125
+ file: filePath,
126
+ description: fm.description || "",
127
+ reads,
128
+ writes,
129
+ };
130
+ }
131
+
132
+ function scan({ bundleRoot } = {}) {
133
+ if (!bundleRoot) return { available: false, workflows: [], edges: [] };
134
+ const dir = workflowsDir(bundleRoot);
135
+ if (!fs.existsSync(dir)) return { available: false, workflows: [], edges: [] };
136
+ const files = fs.readdirSync(dir).filter(f => f.endsWith(".md"));
137
+ const workflows = [];
138
+ const edges = [];
139
+ for (const f of files) {
140
+ try {
141
+ const parsed = parseWorkflow(path.join(dir, f));
142
+ workflows.push({
143
+ name: parsed.name,
144
+ description: parsed.description,
145
+ readCount: parsed.reads.length,
146
+ writeCount: parsed.writes.length,
147
+ });
148
+ for (const r of parsed.reads) edges.push({ from: parsed.name, to: r, kind: "reads" });
149
+ for (const w of parsed.writes) edges.push({ from: parsed.name, to: w, kind: "writes" });
150
+ } catch (e) {
151
+ // Skip unparseable
152
+ }
153
+ }
154
+ // Aggregate per-file: which workflows touch each engine file.
155
+ const fileMap = {};
156
+ for (const e of edges) {
157
+ if (!fileMap[e.to]) fileMap[e.to] = { file: e.to, readers: [], writers: [] };
158
+ if (e.kind === "reads") fileMap[e.to].readers.push(e.from);
159
+ if (e.kind === "writes") fileMap[e.to].writers.push(e.from);
160
+ }
161
+ return {
162
+ available: true,
163
+ workflowCount: workflows.length,
164
+ workflows,
165
+ edges,
166
+ files: Object.values(fileMap),
167
+ };
168
+ }
169
+
170
+ module.exports = { scan, parseWorkflow };
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ // workflow-library-parser.js — Read all .md workflow files from
7
+ // src/workflows/. Extract description, reads, writes, last-run timestamp.
8
+ // Powers the Workflows tab in Studio (Phase 6.7).
9
+
10
+ function indexWorkflows(bundleRoot) {
11
+ const workflowsDir = path.join(bundleRoot, "src", "workflows");
12
+ if (!fs.existsSync(workflowsDir)) return { available: false, workflows: [] };
13
+
14
+ const files = fs.readdirSync(workflowsDir).filter((f) => f.endsWith(".md"));
15
+ const workflows = files.map((file) => {
16
+ const filePath = path.join(workflowsDir, file);
17
+ const text = safeRead(filePath);
18
+ return parseWorkflow(file, filePath, text);
19
+ });
20
+
21
+ return {
22
+ available: true,
23
+ count: workflows.length,
24
+ workflows: workflows.sort((a, b) => a.name.localeCompare(b.name))
25
+ };
26
+ }
27
+
28
+ function parseWorkflow(file, filePath, text) {
29
+ const name = "/" + file.replace(/\.md$/, "");
30
+ const out = {
31
+ name,
32
+ filename: file,
33
+ filePath,
34
+ description: "",
35
+ risk: "LOW",
36
+ target: "",
37
+ role: "",
38
+ reads: [],
39
+ writes: [],
40
+ handsOffTo: [],
41
+ workflowLength: text ? text.length : 0
42
+ };
43
+
44
+ // Frontmatter (description from YAML frontmatter if present)
45
+ const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
46
+ if (fmMatch) {
47
+ const fm = fmMatch[1];
48
+ const descLine = fm.match(/description:\s*(.+)$/im);
49
+ if (descLine) out.description = stripQuotes(descLine[1].trim());
50
+
51
+ // v6 SURFACE 1 — the Say-it card copy + the structured reads/writes
52
+ // contract. Additive: workflows without these keys parse exactly as before
53
+ // (does/long/needsYou undefined, io null). The card degrades honestly.
54
+ const doesLine = fm.match(/^does:\s*(.+)$/im);
55
+ if (doesLine) out.does = stripQuotes(doesLine[1].trim());
56
+ const longLine = fm.match(/^long:\s*(.+)$/im);
57
+ if (longLine) out.long = stripQuotes(longLine[1].trim());
58
+ const ny = fm.match(/^needs_you:\s*(true|false)\s*$/im);
59
+ if (ny) out.needsYou = ny[1].toLowerCase() === "true";
60
+ out.io = parseIoBlock(fm);
61
+ }
62
+
63
+ // Standard sections from Mover OS workflow template
64
+ const triggerMatch = text.match(/\*\*Trigger:\*\*\s+(.+)$/m);
65
+ const targetMatch = text.match(/\*\*Target:\*\*\s+(.+)$/m);
66
+ const roleMatch = text.match(/\*\*Role:\*\*\s+(.+)$/m);
67
+ const taskMatch = text.match(/\*\*Task:\*\*\s+(.+)$/m);
68
+ const riskMatch = text.match(/\*\*Risk Tier:\*\*\s+(\w+)/);
69
+
70
+ if (triggerMatch) out.trigger = triggerMatch[1].trim();
71
+ if (targetMatch) out.target = targetMatch[1].trim();
72
+ if (roleMatch) out.role = roleMatch[1].trim();
73
+ if (taskMatch && !out.description) out.description = taskMatch[1].trim();
74
+ if (riskMatch) out.risk = riskMatch[1].toUpperCase();
75
+
76
+ // Find what the workflow READS. The old approach matched a single
77
+ // LOAD/"Read these files"/Memory-Hierarchy section and only scanned the 800
78
+ // chars after it for backtick-wrapped files — it caught just 1 of 25 real
79
+ // workflows, so nearly every Run card rendered "reads —" while "changes" was
80
+ // populated, despite the workflows clearly reading Engine files. Scan the whole
81
+ // body with a broad, verb-anchored pattern that mirrors the writes scan below
82
+ // (basename only, deduped, capped). Lifts coverage 1/25 → 13/25; the rest
83
+ // genuinely declare no explicit .md read and honestly stay "—".
84
+ const readPattern = /(?:\bRe-?reads?\b|\bReads?\b|\bLOAD\b|\bReview\b)\s+(?:the\s+|your\s+|current\s+|today'?s\s+|active\s+)*[`*]?([A-Za-z_][A-Za-z0-9_/-]*\.md)[`*]?/gi;
85
+ const reads = [...text.matchAll(readPattern)].map((m) => m[1].replace(/^.*\//, ""));
86
+ out.reads = [...new Set(reads)].slice(0, 10);
87
+
88
+ // Find what the workflow WRITES (look for "Target:", "Write to", "Append to")
89
+ const writePattern = /(?:Target:|Write(?: to)?|Append to|Update)\s+[`*]?([A-Za-z_/]+\.md)[`*]?/gi;
90
+ const writes = [...text.matchAll(writePattern)].map((m) => m[1]);
91
+ out.writes = [...new Set(writes)].slice(0, 8);
92
+
93
+ // Handoff
94
+ const handoffMatch = text.match(/(?:HANDOFF|Next[: ]+).{0,200}?\/([a-z-]+)/im);
95
+ if (handoffMatch) out.handsOffTo.push("/" + handoffMatch[1]);
96
+
97
+ return out;
98
+ }
99
+
100
+ function safeRead(filePath) {
101
+ try { return fs.readFileSync(filePath, "utf8"); } catch (_) { return ""; }
102
+ }
103
+
104
+ function stripQuotes(s) {
105
+ return s.replace(/^['"]|['"]$/g, "");
106
+ }
107
+
108
+ // Parse the `io:` frontmatter block into { reads:[{f,src,note?}], writes:[…] }.
109
+ // Line-based + indentation-aware on purpose (robust against blank lines and
110
+ // `io:` appearing anywhere in the frontmatter; stops at the next top-level key).
111
+ // Returns null when there is no io block — the honest "not declared yet" state.
112
+ // io:
113
+ // reads:
114
+ // - f: "Daily/{today}.md"
115
+ // src: dailyNote
116
+ // writes:
117
+ // - f: Metrics_Log.md
118
+ // src: metrics
119
+ // note: with a changelog entry
120
+ function parseIoBlock(fm) {
121
+ const lines = fm.split("\n");
122
+ const start = lines.findIndex((l) => /^io:\s*$/.test(l));
123
+ if (start === -1) return null;
124
+ const io = { reads: [], writes: [] };
125
+ let bucket = null, cur = null;
126
+ for (let i = start + 1; i < lines.length; i++) {
127
+ const line = lines[i];
128
+ if (/^\S/.test(line)) break; // next top-level key → io block ended
129
+ if (line.trim() === "") continue;
130
+ const key = line.match(/^\s{2}(reads|writes):\s*$/);
131
+ if (key) { bucket = io[key[1]]; cur = null; continue; }
132
+ if (!bucket) continue;
133
+ const item = line.match(/^\s{4}-\s*f:\s*(.+)$/);
134
+ if (item) { cur = { f: stripQuotes(item[1].trim()), src: null }; bucket.push(cur); continue; }
135
+ const sub = line.match(/^\s{6}(src|note):\s*(.+)$/);
136
+ if (sub && cur) {
137
+ const val = stripQuotes(sub[2].trim());
138
+ // YAML null on `src` → real null (a plain, non-drawer chip), not the
139
+ // string "null". Authors can also just omit the src line.
140
+ cur[sub[1]] = (sub[1] === "src" && (val === "null" || val === "~")) ? null : val;
141
+ }
142
+ }
143
+ return io;
144
+ }
145
+
146
+ module.exports = { indexWorkflows };