mover-os 4.7.6 → 4.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/install.js +763 -70
  2. package/package.json +15 -3
  3. package/src/dashboard/build.js +1503 -0
  4. package/src/dashboard/dashboard.js +276 -0
  5. package/src/dashboard/index.js +319 -0
  6. package/src/dashboard/lib/activation-log.js +297 -0
  7. package/src/dashboard/lib/active-context-parser.js +177 -0
  8. package/src/dashboard/lib/agent-command.js +93 -0
  9. package/src/dashboard/lib/agent-detect.js +255 -0
  10. package/src/dashboard/lib/agent-detector.js +92 -0
  11. package/src/dashboard/lib/agent-session.js +462 -0
  12. package/src/dashboard/lib/anti-identity-detector.js +116 -0
  13. package/src/dashboard/lib/auto-learnings-parser.js +183 -0
  14. package/src/dashboard/lib/cli-usage-parser.js +92 -0
  15. package/src/dashboard/lib/config-parser.js +109 -0
  16. package/src/dashboard/lib/connect-recommender.js +131 -0
  17. package/src/dashboard/lib/correlations-parser.js +231 -0
  18. package/src/dashboard/lib/daily-note-resolver.js +211 -0
  19. package/src/dashboard/lib/date-utils.js +35 -0
  20. package/src/dashboard/lib/distribution-parser.js +78 -0
  21. package/src/dashboard/lib/dossier-parser.js +64 -0
  22. package/src/dashboard/lib/drift-history.js +83 -0
  23. package/src/dashboard/lib/drift-score.js +119 -0
  24. package/src/dashboard/lib/engine-health.js +170 -0
  25. package/src/dashboard/lib/engine-writer.js +1685 -0
  26. package/src/dashboard/lib/execution-plan.js +125 -0
  27. package/src/dashboard/lib/experiments-parser.js +429 -0
  28. package/src/dashboard/lib/feed-parser.js +294 -0
  29. package/src/dashboard/lib/forked-future.js +60 -0
  30. package/src/dashboard/lib/goal-forecast.js +304 -0
  31. package/src/dashboard/lib/goals-parser.js +67 -0
  32. package/src/dashboard/lib/health-protocols-parser.js +133 -0
  33. package/src/dashboard/lib/hook-activity.js +48 -0
  34. package/src/dashboard/lib/hook-indexer.js +169 -0
  35. package/src/dashboard/lib/hourly-activity-parser.js +143 -0
  36. package/src/dashboard/lib/identity-parser.js +85 -0
  37. package/src/dashboard/lib/ingestion.js +418 -0
  38. package/src/dashboard/lib/library-indexer-v2.js +212 -0
  39. package/src/dashboard/lib/library-indexer.js +105 -0
  40. package/src/dashboard/lib/log-activation.sh +61 -0
  41. package/src/dashboard/lib/memory-curator.js +97 -0
  42. package/src/dashboard/lib/memory-gardener.js +177 -0
  43. package/src/dashboard/lib/memory-gepa.js +102 -0
  44. package/src/dashboard/lib/memory-index.js +470 -0
  45. package/src/dashboard/lib/memory-rerank.js +72 -0
  46. package/src/dashboard/lib/memory-text.js +136 -0
  47. package/src/dashboard/lib/metrics-log-parser.js +76 -0
  48. package/src/dashboard/lib/moves-usage-parser.js +184 -0
  49. package/src/dashboard/lib/onboarding-forge.js +70 -0
  50. package/src/dashboard/lib/override-outcome-parser.js +68 -0
  51. package/src/dashboard/lib/override-summary.js +73 -0
  52. package/src/dashboard/lib/paths.js +192 -0
  53. package/src/dashboard/lib/pattern-manifest-loader.js +29 -0
  54. package/src/dashboard/lib/phantom-strategy.js +129 -0
  55. package/src/dashboard/lib/project-scanner.js +121 -0
  56. package/src/dashboard/lib/promise-wall.js +88 -0
  57. package/src/dashboard/lib/record-score.js +173 -0
  58. package/src/dashboard/lib/redaction.js +140 -0
  59. package/src/dashboard/lib/refusal-parser.js +44 -0
  60. package/src/dashboard/lib/regenerate-manifest.js +206 -0
  61. package/src/dashboard/lib/rewind-snapshots.js +689 -0
  62. package/src/dashboard/lib/roast-wall-parser.js +200 -0
  63. package/src/dashboard/lib/run-registry.js +226 -0
  64. package/src/dashboard/lib/safe-write.js +63 -0
  65. package/src/dashboard/lib/session-log-parser.js +145 -0
  66. package/src/dashboard/lib/session-time-parser.js +158 -0
  67. package/src/dashboard/lib/skill-index.js +171 -0
  68. package/src/dashboard/lib/skill-indexer.js +118 -0
  69. package/src/dashboard/lib/skill-recommender.js +689 -0
  70. package/src/dashboard/lib/strategy-parser.js +245 -0
  71. package/src/dashboard/lib/strategy-protocol-parser.js +135 -0
  72. package/src/dashboard/lib/streak-parser.js +95 -0
  73. package/src/dashboard/lib/suggested-now.js +254 -0
  74. package/src/dashboard/lib/tool-awareness.js +125 -0
  75. package/src/dashboard/lib/transcript-parser.js +331 -0
  76. package/src/dashboard/lib/vault-graph-parser.js +205 -0
  77. package/src/dashboard/lib/view-generator.js +163 -0
  78. package/src/dashboard/lib/voice-dna-parser.js +57 -0
  79. package/src/dashboard/lib/walkthrough-script.js +140 -0
  80. package/src/dashboard/lib/workflow-graph-parser.js +170 -0
  81. package/src/dashboard/lib/workflow-library-parser.js +146 -0
  82. package/src/dashboard/server.js +2024 -0
  83. package/src/dashboard/shortcut.js +0 -0
  84. package/src/dashboard/static/setup-poc.html +306 -0
  85. package/src/dashboard/static/walkthrough-poc.html +580 -0
  86. package/src/dashboard/styles.css +1201 -0
  87. package/src/dashboard/templates/index.html +278 -0
  88. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-ext-wght-normal-Dg-wlmqe.woff2 +0 -0
  89. package/src/dashboard/ui/dist/assets/hanken-grotesk-latin-wght-normal-CaVRRdDk.woff2 +0 -0
  90. package/src/dashboard/ui/dist/assets/hanken-grotesk-vietnamese-wght-normal-CHiFlh_0.woff2 +0 -0
  91. package/src/dashboard/ui/dist/assets/hero.png +0 -0
  92. package/src/dashboard/ui/dist/assets/index-598CSGOZ.js +157 -0
  93. package/src/dashboard/ui/dist/assets/index-BP--M69H.css +1 -0
  94. package/src/dashboard/ui/dist/assets/index-CidzmwSW.js +34 -0
  95. package/src/dashboard/ui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  96. package/src/dashboard/ui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  97. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  98. package/src/dashboard/ui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  99. package/src/dashboard/ui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  100. package/src/dashboard/ui/dist/assets/mover-system.css +62 -0
  101. package/src/dashboard/ui/dist/assets/xterm-CqkleIqs.js +1 -0
  102. package/src/dashboard/ui/dist/icon.svg +4 -0
  103. package/src/dashboard/ui/dist/index.html +18 -0
  104. package/src/dashboard/ui/dist/manifest.webmanifest +1 -0
  105. package/src/dashboard/ui/dist/registerSW.js +1 -0
  106. package/src/dashboard/ui/dist/sw.js +1 -0
  107. package/src/dashboard/ui/dist/workbox-39fa566e.js +1 -0
@@ -0,0 +1,689 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * skill-recommender.js — D4 of the Mover OS onboarding engine.
5
+ *
6
+ * Given a user profile extracted from setup answers, returns a ranked
7
+ * recommendation of skills + connectors to install. Pure keyword/domain
8
+ * matching — no ML, no external calls, zero maintenance.
9
+ *
10
+ * API:
11
+ * recommend(profile) → { recommended: [{skill, label, reason, confidence}],
12
+ * connectors: [{skill, label, reason, confidence}],
13
+ * installAll: bool }
14
+ *
15
+ * profile shape: { work?: string, domain?: string[], tools?: string[], goals?: string[] }
16
+ * All fields optional — degrades gracefully to a sensible default set.
17
+ *
18
+ * Skill catalogue reflects src/skills/ on disk as of 2026-06-18.
19
+ * Always-apply skills (friction-enforcer, mover-os-context, etc.) are ambient —
20
+ * they auto-load regardless and are NOT surfaced here (they're not additive).
21
+ *
22
+ * onboarding-engine-SPEC.md §5 D4.
23
+ */
24
+
25
+ // ─── catalogue ────────────────────────────────────────────────────────────────
26
+ // Each entry:
27
+ // skill — slug matching src/skills/<slug>/ on disk
28
+ // label — human-readable display name
29
+ // category — internal bucket for grouping in the UI
30
+ // keywords — lowercase tokens; profile text is matched against these
31
+ // domains — abstract domain tags (indie-saas, engineer, marketer, …)
32
+ // reason — template for the recommendation card ("because you …").
33
+ // %WORK% is substituted with profile.work when present.
34
+ // tier — 1 (core domain skill) | 2 (supporting) | 3 (niche)
35
+
36
+ const CATALOGUE = [
37
+ // ── Engineering / Building ──────────────────────────────────────────────────
38
+ {
39
+ skill: "systematic-debugging",
40
+ label: "Systematic Debugging",
41
+ category: "engineering",
42
+ keywords: ["code", "debug", "bug", "error", "fix", "crash", "logs", "test", "developer", "engineer", "build", "backend", "api", "typescript", "javascript", "python"],
43
+ domains: ["engineer", "indie-saas"],
44
+ reason: "surfaces evidence-first debugging, no more guessing, straight to root cause",
45
+ tier: 1,
46
+ },
47
+ {
48
+ skill: "tdd-enforcer",
49
+ label: "TDD Enforcer",
50
+ category: "engineering",
51
+ keywords: ["test", "tdd", "backend", "money", "auth", "payment", "stripe", "database", "reliability", "production"],
52
+ domains: ["engineer", "indie-saas"],
53
+ reason: "locks in test-first discipline on the code paths that can't break",
54
+ tier: 1,
55
+ },
56
+ {
57
+ skill: "error-handling-patterns",
58
+ label: "Error Handling Patterns",
59
+ category: "engineering",
60
+ keywords: ["api", "service", "backend", "production", "resilient", "retry", "circuit", "fallback", "outage"],
61
+ domains: ["engineer", "indie-saas"],
62
+ reason: "circuit-breaker and graceful-degradation patterns for production code",
63
+ tier: 2,
64
+ },
65
+ {
66
+ skill: "webhook-handler-patterns",
67
+ label: "Webhook Patterns",
68
+ category: "engineering",
69
+ keywords: ["webhook", "stripe", "event", "idempotent", "payment", "integration", "livemode"],
70
+ domains: ["engineer", "indie-saas"],
71
+ reason: "verify-first, idempotent webhook handlers that don't double-charge users",
72
+ tier: 2,
73
+ },
74
+ {
75
+ skill: "anti-slop-frontend",
76
+ label: "Anti-Slop Frontend",
77
+ category: "engineering",
78
+ keywords: ["frontend", "ui", "design", "react", "component", "dashboard", "interface", "css", "tailwind"],
79
+ domains: ["engineer", "indie-saas"],
80
+ reason: "prevents the AI-generated look that tanks credibility on first impression",
81
+ tier: 1,
82
+ },
83
+ {
84
+ skill: "frontend-design",
85
+ label: "Frontend Design",
86
+ category: "engineering",
87
+ keywords: ["frontend", "design", "ui", "component", "dashboard", "landing", "page", "interface"],
88
+ domains: ["engineer", "indie-saas"],
89
+ reason: "production-grade UI that earns trust before a word is read",
90
+ tier: 2,
91
+ },
92
+ {
93
+ skill: "ui-ux-pro-max",
94
+ label: "UI/UX Pro Max",
95
+ category: "engineering",
96
+ keywords: ["design", "ux", "ui", "figma", "layout", "component", "mobile", "web", "app", "react", "tailwind", "shadcn", "swiftui"],
97
+ domains: ["engineer", "indie-saas"],
98
+ reason: "layout, component, and design-review intelligence across React, Tailwind, shadcn",
99
+ tier: 2,
100
+ },
101
+ {
102
+ skill: "refactoring",
103
+ label: "Refactoring",
104
+ category: "engineering",
105
+ keywords: ["refactor", "clean", "technical debt", "legacy", "rewrite", "simplify", "code quality"],
106
+ domains: ["engineer"],
107
+ reason: "structured refactor assessment once the tests are green",
108
+ tier: 2,
109
+ },
110
+ {
111
+ skill: "find-bugs",
112
+ label: "Find Bugs",
113
+ category: "engineering",
114
+ keywords: ["security", "vulnerability", "audit", "review", "bug", "pen test", "safety"],
115
+ domains: ["engineer", "indie-saas"],
116
+ reason: "branch-level security and quality scan before you ship",
117
+ tier: 2,
118
+ },
119
+ {
120
+ skill: "code-review-lifecycle",
121
+ label: "Code Review Lifecycle",
122
+ category: "engineering",
123
+ keywords: ["review", "pr", "pull request", "team", "feedback", "merge"],
124
+ domains: ["engineer"],
125
+ reason: "PR lifecycle from request to merge, no dropped threads",
126
+ tier: 3,
127
+ },
128
+ {
129
+ skill: "agent-code-reviewer",
130
+ label: "Code Reviewer Agent",
131
+ category: "engineering",
132
+ keywords: ["architecture", "deep review", "multi-file", "complex", "codebase"],
133
+ domains: ["engineer"],
134
+ reason: "spawns a focused code/architecture review agent in a clean context",
135
+ tier: 3,
136
+ },
137
+
138
+ // ── Strategy / Business ─────────────────────────────────────────────────────
139
+ {
140
+ skill: "agent-strategy-analyst",
141
+ label: "Strategy Analyst Agent",
142
+ category: "strategy",
143
+ keywords: ["strategy", "hypothesis", "pivot", "pre-mortem", "investor", "decision", "bet", "direction"],
144
+ domains: ["founder-operator", "indie-saas"],
145
+ reason: "stress-tests your strategy assumptions before you commit resources",
146
+ tier: 1,
147
+ },
148
+ {
149
+ skill: "self-transcendence",
150
+ label: "Self-Transcendence",
151
+ category: "strategy",
152
+ keywords: ["resistance", "stuck", "motivation", "identity", "psychology", "block", "avoidance", "procrastination", "purpose"],
153
+ domains: ["founder-operator"],
154
+ reason: "identity and resistance frameworks for the /debug-resistance workflow",
155
+ tier: 2,
156
+ },
157
+ {
158
+ skill: "experiment-loop",
159
+ label: "Experiment Loop",
160
+ category: "strategy",
161
+ keywords: ["test", "experiment", "ab test", "optimize", "iterate", "hypothesis", "measure", "improve", "convert"],
162
+ domains: ["founder-operator", "marketer", "indie-saas"],
163
+ reason: "Darwinian test-measure-iterate cycle, for decisions that need data, not opinion",
164
+ tier: 1,
165
+ },
166
+
167
+ // ── Marketing / Growth ──────────────────────────────────────────────────────
168
+ {
169
+ skill: "copywriting",
170
+ label: "Copywriting",
171
+ category: "marketing",
172
+ keywords: ["copy", "headline", "cta", "landing page", "homepage", "messaging", "convert", "sales", "persuade"],
173
+ domains: ["marketer", "indie-saas", "freelancer", "content-creator"],
174
+ reason: "headlines, CTAs, and page copy that convert without sounding like a template",
175
+ tier: 1,
176
+ },
177
+ {
178
+ skill: "launch-strategy",
179
+ label: "Launch Strategy",
180
+ category: "marketing",
181
+ keywords: ["launch", "product hunt", "release", "go-to-market", "gtm", "beta", "announce", "ship"],
182
+ domains: ["indie-saas", "founder-operator"],
183
+ reason: "launch sequencing and momentum, from beta to Product Hunt",
184
+ tier: 1,
185
+ },
186
+ {
187
+ skill: "marketing-psychology",
188
+ label: "Marketing Psychology",
189
+ category: "marketing",
190
+ keywords: ["psychology", "persuasion", "behavior", "bias", "social proof", "scarcity", "urgency", "trust"],
191
+ domains: ["marketer", "indie-saas", "content-creator"],
192
+ reason: "70+ behavioral science models applied to copy, pricing, and UX",
193
+ tier: 1,
194
+ },
195
+ {
196
+ skill: "onboarding-cro",
197
+ label: "Onboarding CRO",
198
+ category: "marketing",
199
+ keywords: ["onboarding", "activation", "first run", "aha moment", "time to value", "new user", "churn"],
200
+ domains: ["indie-saas", "marketer"],
201
+ reason: "turns signups into activated users, the leaky bucket under every SaaS",
202
+ tier: 1,
203
+ },
204
+ {
205
+ skill: "signup-flow-cro",
206
+ label: "Signup Flow CRO",
207
+ category: "marketing",
208
+ keywords: ["signup", "registration", "form", "friction", "convert", "trial", "account"],
209
+ domains: ["indie-saas", "marketer"],
210
+ reason: "removes the friction that kills signups before the product gets a chance",
211
+ tier: 2,
212
+ },
213
+ {
214
+ skill: "page-cro",
215
+ label: "Page CRO",
216
+ category: "marketing",
217
+ keywords: ["landing page", "homepage", "pricing", "conversion", "cro", "optimize", "above fold"],
218
+ domains: ["marketer", "indie-saas"],
219
+ reason: "conversion-rate optimization for any marketing page",
220
+ tier: 2,
221
+ },
222
+ {
223
+ skill: "pricing-strategy",
224
+ label: "Pricing Strategy",
225
+ category: "marketing",
226
+ keywords: ["pricing", "price", "tier", "freemium", "subscription", "charge", "monetize", "revenue", "willingness to pay"],
227
+ domains: ["indie-saas", "founder-operator", "freelancer"],
228
+ reason: "packaging and pricing that doesn't leave money on the table",
229
+ tier: 1,
230
+ },
231
+ {
232
+ skill: "email-sequence",
233
+ label: "Email Sequences",
234
+ category: "marketing",
235
+ keywords: ["email", "drip", "nurture", "welcome", "newsletter", "sequence", "lifecycle", "list"],
236
+ domains: ["marketer", "indie-saas", "content-creator"],
237
+ reason: "welcome, nurture, and re-engagement sequences that compound over time",
238
+ tier: 2,
239
+ },
240
+ {
241
+ skill: "social-content",
242
+ label: "Social Content",
243
+ category: "marketing",
244
+ keywords: ["twitter", "linkedin", "instagram", "tiktok", "post", "content", "social", "thread", "tweet", "audience", "grow"],
245
+ domains: ["content-creator", "marketer"],
246
+ reason: "platform-specific content that builds an audience before you need one",
247
+ tier: 1,
248
+ },
249
+ {
250
+ skill: "paid-ads",
251
+ label: "Paid Ads",
252
+ category: "marketing",
253
+ keywords: ["ads", "google ads", "meta", "facebook", "paid", "roas", "campaign", "targeting", "spend"],
254
+ domains: ["marketer", "indie-saas"],
255
+ reason: "campaign strategy and ad copy that pays back more than it costs",
256
+ tier: 2,
257
+ },
258
+ {
259
+ skill: "ab-test-setup",
260
+ label: "A/B Test Setup",
261
+ category: "marketing",
262
+ keywords: ["ab test", "split test", "experiment", "statistical", "multivariate", "hypothesis", "test"],
263
+ domains: ["marketer", "indie-saas"],
264
+ reason: "statistically sound A/B tests, no more hunches dressed as data",
265
+ tier: 2,
266
+ },
267
+ {
268
+ skill: "analytics-tracking",
269
+ label: "Analytics Tracking",
270
+ category: "marketing",
271
+ keywords: ["analytics", "ga4", "google analytics", "posthog", "tracking", "utm", "gtm", "event", "funnel", "conversion"],
272
+ domains: ["marketer", "indie-saas"],
273
+ reason: "GA4/GTM/PostHog setup so decisions are based on what actually happened",
274
+ tier: 2,
275
+ },
276
+ {
277
+ skill: "referral-program",
278
+ label: "Referral Program",
279
+ category: "marketing",
280
+ keywords: ["referral", "affiliate", "viral", "word of mouth", "invite", "refer"],
281
+ domains: ["indie-saas", "marketer"],
282
+ reason: "referral and affiliate loops that make customers your best salespeople",
283
+ tier: 3,
284
+ },
285
+ {
286
+ skill: "seo-audit",
287
+ label: "SEO Audit",
288
+ category: "marketing",
289
+ keywords: ["seo", "search", "organic", "rank", "google", "traffic", "technical seo"],
290
+ domains: ["marketer", "indie-saas"],
291
+ reason: "technical and on-page SEO audit, find and fix what's killing your rank",
292
+ tier: 2,
293
+ },
294
+ {
295
+ skill: "programmatic-seo",
296
+ label: "Programmatic SEO",
297
+ category: "marketing",
298
+ keywords: ["programmatic seo", "directory", "location pages", "integration pages", "scale", "template"],
299
+ domains: ["marketer", "indie-saas"],
300
+ reason: "SEO at scale with templates and data, for products with natural keyword surfaces",
301
+ tier: 3,
302
+ },
303
+ {
304
+ skill: "seo-content-writer",
305
+ label: "SEO Content Writer",
306
+ category: "marketing",
307
+ keywords: ["seo content", "blog", "article", "keyword", "content marketing", "organic traffic"],
308
+ domains: ["marketer", "content-creator"],
309
+ reason: "keyword-targeted content that earns rankings and trust simultaneously",
310
+ tier: 2,
311
+ },
312
+ {
313
+ skill: "marketing-ideas",
314
+ label: "Marketing Ideas",
315
+ category: "marketing",
316
+ keywords: ["marketing", "growth", "ideas", "channel", "traction", "acquisition", "strategy"],
317
+ domains: ["marketer", "indie-saas", "founder-operator"],
318
+ reason: "140 proven growth approaches organised by stage and budget",
319
+ tier: 2,
320
+ },
321
+ {
322
+ skill: "free-tool-strategy",
323
+ label: "Free Tool Strategy",
324
+ category: "marketing",
325
+ keywords: ["free tool", "calculator", "generator", "widget", "acquisition", "lead magnet"],
326
+ domains: ["marketer", "indie-saas"],
327
+ reason: "free tools as compounding acquisition levers, done right they generate leads on autopilot",
328
+ tier: 3,
329
+ },
330
+
331
+ // ── Research / Intelligence ─────────────────────────────────────────────────
332
+ {
333
+ skill: "agent-research-analyst",
334
+ label: "Research Analyst Agent",
335
+ category: "research",
336
+ keywords: ["research", "market", "competitive", "competitor", "analysis", "intelligence", "landscape"],
337
+ domains: ["marketer", "founder-operator", "indie-saas"],
338
+ reason: "deep market + competitive research in a focused subagent context",
339
+ tier: 2,
340
+ },
341
+ {
342
+ skill: "agent-data-analyst",
343
+ label: "Data Analyst Agent",
344
+ category: "research",
345
+ keywords: ["data", "analytics", "metrics", "quantitative", "numbers", "analysis", "sql", "spreadsheet"],
346
+ domains: ["marketer", "indie-saas", "engineer"],
347
+ reason: "quantitative analysis subagent, surfaces the story in your numbers",
348
+ tier: 3,
349
+ },
350
+
351
+ // ── Writing / Content ───────────────────────────────────────────────────────
352
+ {
353
+ skill: "human-writer",
354
+ label: "Human Writer",
355
+ category: "writing",
356
+ keywords: ["write", "article", "post", "tweet", "thread", "newsletter", "email", "content", "publish", "copy", "reddit", "linkedin"],
357
+ domains: ["content-creator", "marketer", "freelancer"],
358
+ reason: "anti-AI detection, voice calibration, natural writing, for anything published under your name",
359
+ tier: 1,
360
+ },
361
+ {
362
+ skill: "agent-prompt-engineer",
363
+ label: "Prompt Engineer Agent",
364
+ category: "writing",
365
+ keywords: ["prompt", "ai", "llm", "gpt", "claude", "system prompt", "instruction", "context"],
366
+ domains: ["engineer", "indie-saas"],
367
+ reason: "prompt design and optimization subagent for LLM-powered features",
368
+ tier: 3,
369
+ },
370
+
371
+ // ── Obsidian / Knowledge ────────────────────────────────────────────────────
372
+ {
373
+ skill: "obsidian-markdown",
374
+ label: "Obsidian Markdown",
375
+ category: "obsidian",
376
+ keywords: ["obsidian", "note", "markdown", "vault", "wikilink", "callout", "frontmatter", "pkm", "knowledge"],
377
+ domains: ["knowledge-worker"],
378
+ reason: "wikilinks, callouts, and Obsidian-specific syntax, so your vault is machine-readable",
379
+ tier: 1,
380
+ },
381
+ {
382
+ skill: "obsidian-cli",
383
+ label: "Obsidian CLI",
384
+ category: "obsidian",
385
+ keywords: ["obsidian", "vault", "note", "search", "plugin", "theme", "cli"],
386
+ domains: ["knowledge-worker"],
387
+ reason: "read, create, and search vault notes directly from the terminal",
388
+ tier: 2,
389
+ },
390
+ {
391
+ skill: "obsidian-bases",
392
+ label: "Obsidian Bases",
393
+ category: "obsidian",
394
+ keywords: ["obsidian bases", "database", "table view", "filter", "formula", "card view"],
395
+ domains: ["knowledge-worker"],
396
+ reason: "database-like views over your notes, turns the vault into a queryable system",
397
+ tier: 2,
398
+ },
399
+ {
400
+ skill: "defuddle",
401
+ label: "Defuddle",
402
+ category: "obsidian",
403
+ keywords: ["web", "url", "article", "page", "read", "extract", "research", "browser"],
404
+ domains: ["knowledge-worker", "research"],
405
+ reason: "clean markdown from any web page, saves tokens and removes navigation clutter",
406
+ tier: 2,
407
+ },
408
+
409
+ // ── System / Tooling ────────────────────────────────────────────────────────
410
+ {
411
+ skill: "parallel-agents",
412
+ label: "Parallel Agents",
413
+ category: "system",
414
+ keywords: ["parallel", "agent", "concurrent", "speed", "batch", "multiple tasks"],
415
+ domains: ["engineer", "founder-operator"],
416
+ reason: "dispatch independent tasks to parallel subagents, halves wall-clock time",
417
+ tier: 2,
418
+ },
419
+ {
420
+ skill: "mover-skill-creator",
421
+ label: "Skill Creator",
422
+ category: "system",
423
+ keywords: ["skill", "create", "extend", "custom", "build", "new capability"],
424
+ domains: ["engineer", "founder-operator"],
425
+ reason: "create and evaluate new skills, extend the system to your exact domain",
426
+ tier: 3,
427
+ },
428
+ ];
429
+
430
+ // ─── domain detector ──────────────────────────────────────────────────────────
431
+ // Maps free-text profile fields to abstract domain buckets.
432
+ // Returns { [domain]: score } where score = overlap count (not normalised).
433
+
434
+ const DOMAIN_SIGNALS = {
435
+ "indie-saas": ["saas", "product", "app", "startup", "launch", "indie", "subscription", "stripe", "software", "build", "ship", "solo"],
436
+ "content-creator": ["content", "youtube", "twitter", "tiktok", "newsletter", "audience", "grow", "creator", "post", "publish", "followers", "views"],
437
+ "freelancer": ["freelance", "client", "agency", "consulting", "proposal", "retainer", "contract", "invoice", "services"],
438
+ "engineer": ["code", "developer", "engineer", "build", "backend", "frontend", "api", "typescript", "javascript", "python", "rust", "swift"],
439
+ "marketer": ["marketing", "seo", "ads", "funnel", "conversion", "cro", "traffic", "growth", "campaign", "leads"],
440
+ "knowledge-worker": ["note", "obsidian", "pkm", "research", "writing", "vault", "library", "knowledge", "second brain"],
441
+ "founder-operator": ["founder", "ceo", "strategy", "hire", "team", "investor", "pitch", "ops", "operations", "decision"],
442
+ };
443
+
444
+ function detectDomains(profile) {
445
+ const text = [
446
+ profile.work || "",
447
+ (profile.domain || []).join(" "),
448
+ (profile.tools || []).join(" "),
449
+ (profile.goals || []).join(" "),
450
+ ].join(" ").toLowerCase();
451
+
452
+ const tokens = text.split(/[\s,;.!?]+/).filter(Boolean);
453
+ const scores = {};
454
+
455
+ for (const [domain, signals] of Object.entries(DOMAIN_SIGNALS)) {
456
+ let score = 0;
457
+ for (const sig of signals) {
458
+ // multi-word signals: substring match; single-word: exact token match
459
+ if (sig.includes(" ")) {
460
+ if (text.includes(sig)) score += 2; // phrase match = stronger signal
461
+ } else {
462
+ if (tokens.includes(sig)) score += 1;
463
+ }
464
+ }
465
+ if (score > 0) scores[domain] = score;
466
+ }
467
+
468
+ return scores;
469
+ }
470
+
471
+ // ─── keyword scorer ───────────────────────────────────────────────────────────
472
+ // Returns a 0–1 confidence for a single catalogue entry against the profile.
473
+
474
+ function scoreEntry(entry, profileText, domainScores) {
475
+ const tokens = profileText.toLowerCase().split(/[\s,;.!?]+/).filter(Boolean);
476
+ let hits = 0;
477
+
478
+ for (const kw of entry.keywords) {
479
+ if (kw.includes(" ")) {
480
+ if (profileText.toLowerCase().includes(kw)) hits += 2;
481
+ } else {
482
+ if (tokens.includes(kw)) hits += 1;
483
+ }
484
+ }
485
+
486
+ // Domain bonus: each matching domain for this entry adds to the score
487
+ let domainBonus = 0;
488
+ for (const d of entry.domains) {
489
+ if (domainScores[d]) domainBonus += Math.min(domainScores[d], 3); // cap per-domain
490
+ }
491
+
492
+ const rawScore = hits + domainBonus;
493
+ const maxPossible = entry.keywords.length + entry.domains.length * 3;
494
+ const normalised = Math.min(rawScore / Math.max(maxPossible, 1), 1);
495
+
496
+ // Tier penalty: tier-3 skills need a higher raw score to surface
497
+ const tierPenalty = entry.tier === 3 ? 0.15 : entry.tier === 2 ? 0.05 : 0;
498
+ return Math.max(0, normalised - tierPenalty);
499
+ }
500
+
501
+ // ─── public API ───────────────────────────────────────────────────────────────
502
+
503
+ /**
504
+ * recommend(profile) — the main entrypoint.
505
+ *
506
+ * @param {object} profile
507
+ * work? {string} — free-text "what do you work on?" answer
508
+ * domain? {string[]} — optional domain tags from structured Q (e.g. ["saas","marketing"])
509
+ * tools? {string[]} — tools/stack the user mentioned
510
+ * goals? {string[]} — goals/outcomes mentioned
511
+ *
512
+ * @returns {{
513
+ * recommended: Array<{skill:string, label:string, reason:string, confidence:number}>,
514
+ * connectors: Array<{skill:string, label:string, reason:string, confidence:number}>,
515
+ * installAll: boolean
516
+ * }}
517
+ */
518
+ function recommend(profile = {}) {
519
+ // Robust to string OR array for the tag fields: a real setup interview yields
520
+ // free text, not pre-tagged arrays. Coerce each to an array before use.
521
+ const toArr = (v) => Array.isArray(v) ? v : (v == null ? [] : [String(v)]);
522
+ profile = {
523
+ work: String(profile.work || ""),
524
+ domain: toArr(profile.domain),
525
+ tools: toArr(profile.tools),
526
+ goals: toArr(profile.goals),
527
+ };
528
+ const profileText = [
529
+ profile.work,
530
+ profile.domain.join(" "),
531
+ profile.tools.join(" "),
532
+ profile.goals.join(" "),
533
+ ].join(" ");
534
+
535
+ const isEmpty = profileText.trim().length < 3;
536
+
537
+ // Empty profile → suggest install-all (better a full system than a partial one)
538
+ if (isEmpty) {
539
+ return {
540
+ recommended: [],
541
+ connectors: [],
542
+ installAll: true,
543
+ };
544
+ }
545
+
546
+ const domainScores = detectDomains(profile);
547
+ const dominantDomains = Object.entries(domainScores)
548
+ .sort((a, b) => b[1] - a[1])
549
+ .slice(0, 3)
550
+ .map(([d]) => d);
551
+
552
+ // Very broad profile (all 4 top domains score ≥ 2) → install-all is cleaner
553
+ const highDomains = Object.values(domainScores).filter((s) => s >= 2).length;
554
+ const installAll = highDomains >= 4 && Object.keys(domainScores).length >= 4;
555
+
556
+ if (installAll) {
557
+ return { recommended: [], connectors: [], installAll: true };
558
+ }
559
+
560
+ // Score every catalogue entry
561
+ const scored = CATALOGUE.map((entry) => ({
562
+ ...entry,
563
+ confidence: scoreEntry(entry, profileText, domainScores),
564
+ })).filter((e) => e.confidence > 0.05); // drop noise-floor entries
565
+
566
+ // Sort: confidence desc, then tier asc (tier 1 first when tied)
567
+ scored.sort((a, b) => {
568
+ const dc = b.confidence - a.confidence;
569
+ if (Math.abs(dc) > 0.01) return dc;
570
+ return a.tier - b.tier;
571
+ });
572
+
573
+ // Substitute %WORK% in reason templates
574
+ const work = (profile.work || "").split(/\s+/).slice(0, 5).join(" ") || "your work";
575
+ const withReasons = scored.map((e) => ({
576
+ skill: e.skill,
577
+ label: e.label,
578
+ reason: e.reason.replace(/%WORK%/g, work),
579
+ confidence: Math.round(e.confidence * 100) / 100,
580
+ category: e.category,
581
+ }));
582
+
583
+ // Split: connectors (marketing sub-packs) vs core domain skills
584
+ const MARKETING_SKILLS = new Set([
585
+ "copywriting", "launch-strategy", "marketing-psychology", "onboarding-cro",
586
+ "signup-flow-cro", "page-cro", "pricing-strategy", "email-sequence",
587
+ "social-content", "paid-ads", "ab-test-setup", "analytics-tracking",
588
+ "referral-program", "seo-audit", "programmatic-seo", "seo-content-writer",
589
+ "marketing-ideas", "free-tool-strategy", "schema-markup", "competitor-alternatives",
590
+ "form-cro", "popup-cro", "paywall-upgrade-cro",
591
+ ]);
592
+
593
+ const recommended = withReasons
594
+ .filter((e) => !MARKETING_SKILLS.has(e.skill))
595
+ .slice(0, 5); // cap at 5 — overwhelm is the enemy (SPEC §0 North Star)
596
+
597
+ const connectors = withReasons
598
+ .filter((e) => MARKETING_SKILLS.has(e.skill))
599
+ .slice(0, 5); // same cap for marketing connectors
600
+
601
+ return { recommended, connectors, installAll: false };
602
+ }
603
+
604
+ // ─── exports ──────────────────────────────────────────────────────────────────
605
+ module.exports = { recommend, detectDomains, CATALOGUE };
606
+
607
+ // ─── self-test (node skill-recommender.js) ────────────────────────────────────
608
+ if (require.main === module) {
609
+ const assert = (condition, msg) => {
610
+ if (!condition) { console.error("FAIL:", msg); process.exit(1); }
611
+ console.log("PASS:", msg);
612
+ };
613
+
614
+ // ── Profile 1: Indie SaaS founder, building a product, needs to launch ──────
615
+ const p1 = {
616
+ work: "I'm building a SaaS product for freelancers, subscription billing, stripe, backend api",
617
+ domain: ["saas", "indie"],
618
+ tools: ["typescript", "react", "stripe"],
619
+ goals: ["launch on Product Hunt", "get first 100 paying customers", "pricing strategy"],
620
+ };
621
+ const r1 = recommend(p1);
622
+ assert(!r1.installAll, "p1: should NOT be install-all");
623
+ assert(r1.recommended.length > 0, "p1: should have recommended skills");
624
+ assert(r1.recommended.some((s) => ["systematic-debugging","tdd-enforcer","anti-slop-frontend"].includes(s.skill)),
625
+ "p1: engineering skill in recommended");
626
+ assert(r1.connectors.some((s) => ["launch-strategy","pricing-strategy","onboarding-cro"].includes(s.skill)),
627
+ "p1: launch/pricing connector present");
628
+ assert(r1.recommended.every((s) => s.confidence > 0.05), "p1: all recommended above noise floor");
629
+ console.log(" p1 recommended:", r1.recommended.map((s) => `${s.skill}(${s.confidence})`).join(", "));
630
+ console.log(" p1 connectors:", r1.connectors.map((s) => s.skill).join(", "));
631
+
632
+ // ── Profile 2: Content creator building an audience on Twitter/LinkedIn ──────
633
+ const p2 = {
634
+ work: "I write a newsletter and post threads on Twitter and LinkedIn. Growing my audience.",
635
+ domain: ["content"],
636
+ tools: ["twitter", "linkedin", "substack"],
637
+ goals: ["grow audience", "publish consistently", "monetize newsletter"],
638
+ };
639
+ const r2 = recommend(p2);
640
+ assert(!r2.installAll, "p2: should NOT be install-all");
641
+ assert(r2.recommended.some((s) => s.skill === "human-writer"), "p2: human-writer in recommended");
642
+ assert(r2.connectors.some((s) => ["social-content","email-sequence"].includes(s.skill)),
643
+ "p2: social/email connector present");
644
+ console.log(" p2 recommended:", r2.recommended.map((s) => s.skill).join(", "));
645
+ console.log(" p2 connectors:", r2.connectors.map((s) => s.skill).join(", "));
646
+
647
+ // ── Profile 3: Empty / vague profile → install-all ───────────────────────────
648
+ const r3 = recommend({});
649
+ assert(r3.installAll === true, "p3: empty profile → installAll");
650
+ assert(r3.recommended.length === 0, "p3: no recommended on install-all");
651
+ console.log(" p3 installAll:", r3.installAll);
652
+
653
+ // ── Profile 4: Obsidian/PKM knowledge worker ─────────────────────────────────
654
+ const p4 = {
655
+ work: "I use Obsidian for knowledge management, research, and writing. Second brain setup.",
656
+ domain: ["knowledge", "pkm"],
657
+ tools: ["obsidian"],
658
+ goals: ["better note system", "extract knowledge from research"],
659
+ };
660
+ const r4 = recommend(p4);
661
+ assert(!r4.installAll, "p4: knowledge worker not install-all");
662
+ assert(r4.recommended.some((s) => s.skill === "obsidian-markdown"), "p4: obsidian-markdown surfaced");
663
+ console.log(" p4 recommended:", r4.recommended.map((s) => s.skill).join(", "));
664
+
665
+ // ── Profile 5: confidence ordering — when confidence is tied, tier 1 before tier 3 ──
666
+ // (higher confidence always wins across tiers; tier is a tiebreaker only)
667
+ const p5 = {
668
+ work: "I build frontend apps in react and need to ship clean ui without it looking ai-generated",
669
+ domain: ["engineer", "frontend"],
670
+ tools: ["react", "tailwind", "typescript"],
671
+ goals: ["ship clean ui", "improve code quality"],
672
+ };
673
+ const r5 = recommend(p5);
674
+ assert(!r5.installAll, "p5: not install-all");
675
+ assert(r5.recommended.length > 0, "p5: has recommended skills");
676
+ // When two skills have the same confidence (within 0.01), lower tier number comes first
677
+ for (let i = 0; i < r5.recommended.length - 1; i++) {
678
+ const a = r5.recommended[i];
679
+ const b = r5.recommended[i + 1];
680
+ const ea = CATALOGUE.find((c) => c.skill === a.skill) || { tier: 2 };
681
+ const eb = CATALOGUE.find((c) => c.skill === b.skill) || { tier: 2 };
682
+ if (Math.abs(a.confidence - b.confidence) <= 0.01) {
683
+ assert(ea.tier <= eb.tier, `p5: tied-confidence pair ${a.skill}(t${ea.tier}) before ${b.skill}(t${eb.tier})`);
684
+ }
685
+ }
686
+ console.log(" p5 recommended:", r5.recommended.map((s) => `${s.skill}(t${(CATALOGUE.find(c=>c.skill===s.skill)||{tier:0}).tier},${s.confidence})`).join(", "));
687
+
688
+ console.log("\nAll self-tests passed.");
689
+ }