diff --git a/plugins/linkedin-studio/.claude-plugin/plugin.json b/plugins/linkedin-studio/.claude-plugin/plugin.json index eb4308e..94ccf66 100644 --- a/plugins/linkedin-studio/.claude-plugin/plugin.json +++ b/plugins/linkedin-studio/.claude-plugin/plugin.json @@ -1,11 +1,11 @@ { "name": "linkedin-studio", "version": "3.1.0", - "description": "LinkedIn Studio — full-spectrum LinkedIn content engine: feed posts, carousels, video scripts, and long-form newsletter editions, with the January 2026 360Brew algorithm baked in. v3.1.0 (Endring 9) adds a cold adversarial review package to `/linkedin:newsletter` — Step 6.5 + the standalone `/linkedin:headless-review` command run three new headless archetypes (content-reviewer, language-reviewer, fact-reviewer) plus the persona reviewer with NO drafting-session context — a `/linkedin:pivot` command that re-opens cleared gates after a late change, and per-artifact personas (one or more readers configurable per edition). v3.0.0 renamed the plugin (was `linkedin-thought-leadership`): slug, agent namespace, and state-file path are `linkedin-studio`; the `/linkedin:*` commands are unchanged.", + "description": "LinkedIn Studio — full-spectrum LinkedIn content engine: feed posts, carousels, video scripts, and long-form newsletter editions, with the 2026 relevance-ranking model baked in. v3.1.0 (Endring 9) adds a cold adversarial review package to `/linkedin:newsletter` — Step 6.5 + the standalone `/linkedin:headless-review` command run three new headless archetypes (content-reviewer, language-reviewer, fact-reviewer) plus the persona reviewer with NO drafting-session context — a `/linkedin:pivot` command that re-opens cleared gates after a late change, and per-artifact personas (one or more readers configurable per edition). v3.0.0 renamed the plugin (was `linkedin-thought-leadership`): slug, agent namespace, and state-file path are `linkedin-studio`; the `/linkedin:*` commands are unchanged.", "author": { "name": "Kjell Tore Guttormsen" }, "license": "MIT", "repository": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace", - "keywords": ["linkedin", "content-creation", "newsletter", "analytics", "360brew"] + "keywords": ["linkedin", "content-creation", "newsletter", "analytics", "relevance-ranking"] } diff --git a/plugins/linkedin-studio/.gitignore b/plugins/linkedin-studio/.gitignore index efe2074..5f99885 100644 --- a/plugins/linkedin-studio/.gitignore +++ b/plugins/linkedin-studio/.gitignore @@ -5,6 +5,10 @@ # Local configuration *.local.md +# Real voice profile is personal data — adopters keep theirs local; the tracked +# authentic-voice-samples.md ships as a sentinel placeholder. (Already matched by +# *.local.md above; listed explicitly so the intent is unmissable.) +assets/voice-samples/authentic-voice-samples.local.md # Session state (personal activity, auto-initialized from template) REMEMBER.md diff --git a/plugins/linkedin-studio/assets/voice-samples/authentic-voice-samples.md b/plugins/linkedin-studio/assets/voice-samples/authentic-voice-samples.md index 7f45d07..0d0c418 100644 --- a/plugins/linkedin-studio/assets/voice-samples/authentic-voice-samples.md +++ b/plugins/linkedin-studio/assets/voice-samples/authentic-voice-samples.md @@ -1,227 +1,70 @@ -# Authentic Voice Samples - Kjell Tore Guttormsen + + -Kjell Tore does not have traditional writing samples to share. Instead, his voice is defined by the following characteristics which Claude should internalize and apply consistently. +# Authentic Voice Samples — Placeholder (neutral defaults) ---- +These are neutral, widely-applicable defaults so the plugin produces reasonable +content before you personalize. Replace them with your own voice via +`/linkedin:setup`. Until you do, the voice category scores 0. ## Core Voice Characteristics -### 1. Solution-Oriented Mindset -- Every problem is presented as an opportunity -- Never complains without offering a path forward -- Focuses on "what can be done" rather than "what went wrong" -- Sees challenges as interesting puzzles to solve +1. **Solution-oriented** — frame problems with a path forward, not just complaints. +2. **Factually grounded** — base claims on evidence; acknowledge uncertainty openly. +3. **Non-judgmental** — explain without criticizing people, companies, or decisions. +4. **Curious and open** — treat "I don't know" as a starting point, not a weakness. +5. **Story-led** — open with a concrete example before the abstract point. +6. **Actionable** — end with something the reader can do, or a clear takeaway. -### 2. Factual Grounding -- Statements are based on facts, not assumptions -- If uncertain, acknowledges uncertainty openly -- Prefers data and evidence over opinions -- Avoids speculation presented as fact +## Do's -### 3. Non-Judgmental Tone -- Observes and explains without criticizing others -- Builds up, never tears down -- Avoids negative commentary about people, companies, or decisions -- When discussing alternatives, frames as "different approaches" not "better/worse" +- ✅ Open with a story or concrete example before the concept. +- ✅ Use clear, accessible language even for technical topics. +- ✅ Explain jargon on first use — assume intelligence, not prior knowledge. +- ✅ Show rather than tell. +- ✅ End with a specific, actionable takeaway. +- ✅ Keep standard posts concise (≈800–1500 characters). -### 4. Curiosity and Openness -- Genuinely interested in learning new things -- Open to new ideas and approaches -- Asks questions to understand, not to challenge -- Embraces "I don't know" as a starting point for exploration +## Don'ts -### 5. Storytelling Approach -- Uses narrative techniques to make points memorable -- Varies storytelling patterns based on content: - - Hero's journey (transformation stories) - - Problem-solution (practical content) - - Before-after (showing change/improvement) - - Discovery narrative (learning something new) - - Day-in-the-life (practical application) -- Shows rather than tells - -### 6. Actionable Conclusions -- Ends with something the reader can do -- The more actionable, the better -- If no clear action, provides a clear summary/takeaway -- Never ends on a vague note - ---- - -## Cross-Sample Analysis - -### Do's (Things that sound like Kjell Tore) - -- ✅ Start with stories or concrete examples before explaining concepts -- ✅ Use clear, accessible language even for technical topics -- ✅ Explain technical concepts thoroughly - assume intelligence, not knowledge -- ✅ Show rather than tell - demonstrate with examples -- ✅ End with actionable takeaways - what can the reader do NOW? -- ✅ Vary storytelling techniques based on the content -- ✅ Be genuinely helpful and supportive -- ✅ Acknowledge complexity before simplifying -- ✅ Use transitions like "What I've learned is..." to share insights -- ✅ Frame discoveries as shared learning, not lecturing -- ✅ Keep posts concise - short to medium length (800-1500 characters) - -### Don'ts (Things Kjell Tore would NEVER say) - -- ❌ Don't use buzzwords: "game-changer", "leverage", "synergy", "disrupt", "revolutionize" -- ❌ Don't criticize people, companies, or decisions -- ❌ Don't use self-deprecating humor -- ❌ Don't make assumptions without facts -- ❌ Don't write overly long posts (stay under 1500 characters for posts) -- ❌ Don't use more than 1-2 emojis per post -- ❌ Don't discuss politics, religion, or personal matters -- ❌ Don't use em dashes (—) - use hyphens or alternatives instead -- ❌ Don't start with "Let's dive deep into..." -- ❌ Don't use excessive exclamation marks!!! -- ❌ Don't use generic motivational phrases -- ❌ Don't be preachy or lecture the reader -- ❌ Don't use "we" when you mean "I" (be direct about personal experience) - ---- +- ❌ Corporate buzzwords ("game-changer", "leverage", "synergy", "disrupt"). +- ❌ Criticizing people, companies, or decisions. +- ❌ Claims without evidence. +- ❌ More than 1–2 emojis per post. +- ❌ Generic motivational filler or preachy lecturing. ## Signature Phrases -Use these naturally when appropriate - don't force them: - -- "Let me show you..." -- "What I've learned is..." -- "Here is the secret to..." - -These phrases signal a transition to insight or demonstration. Use them to introduce key points or revelations. - ---- +_(Add the phrases you naturally use once you personalize this profile.)_ ## Vocabulary Preferences -### Technical Terms - How to Handle - -- **RAG (Retrieval-Augmented Generation):** Always explain on first use -- **MCP (Model Context Protocol):** Explain what it enables, not just the acronym -- **Copilot Studio:** Can assume some familiarity with Microsoft ecosystem -- **Skills (Claude):** Explain as "reusable instruction sets" or similar -- **Low-code:** Generally understood, but clarify scope if needed - -**Principle:** Assume intelligence, not knowledge. Explain jargon without being condescending. - -### Words/Phrases to AVOID - -- "Game-changer" -- "Revolutionary" -- "Disruption" / "Disruptive" -- "Leverage" (as a verb) -- "Synergy" -- "Deep dive" / "Let's dive deep" -- "Unpack" (as in "let me unpack this") -- "At the end of the day" -- "It is what it is" -- "Touch base" -- "Circle back" -- "Low-hanging fruit" - ---- - -## Humor and Personality - -- **Humor style:** Mostly absent in professional content. If humor appears, it's observational and gentle - never at anyone's expense -- **Self-deprecation:** Never. Don't undermine your own credibility. -- **Cultural references:** Avoid pop culture references. Stick to professional/work context. -- **Analogies:** Use when helpful for explanation. Prefer technical or universal analogies over sports/culture-specific ones. - ---- - -## Transitions and Flow - -### How to Move Between Ideas - -- Use questions: "So what does this mean for..." -- Use signposting: "Three things matter here..." -- Use revelation: "Here's what I discovered..." -- Use contrast: "The common approach is X. But what actually works is Y." - -### How to Conclude - -- Always tie back to practical implications -- End with a specific action the reader can take -- If no action possible, summarize the key insight clearly -- Occasionally invite discussion, but don't overuse "What do you think?" as a crutch - ---- - -## Technical Depth Adaptation - -Match technical depth to the target audience: - -### For Leaders -- High-level concepts -- Business implications -- Strategic decisions -- ROI and outcomes -- Avoid implementation details - -### For Low-Code Developers -- Practical tips and patterns -- Step-by-step guidance -- Tool-specific insights -- Common pitfalls and solutions -- Can include some technical detail - -### For AI Architects -- Technical depth welcome -- Architecture patterns -- Integration approaches -- Trade-offs and decisions -- Code snippets when relevant - -### For Power Users -- Productivity gains -- Workflow improvements -- Tool comparisons -- Time-saving techniques -- Practical shortcuts - -**Key principle:** Always ensure technical content is well-explained and followable, regardless of depth. If you go technical, go all the way - don't half-explain. - ---- +_(List the terms you always explain, and the words/phrases you avoid.)_ ## Language Guidelines -- **Always English** for all LinkedIn content -- Clear, international English accessible to non-native speakers -- Avoid idioms that don't translate well internationally -- Prefer simple sentence structures for complex ideas -- Never use em dashes (—) - use hyphens, commas, or separate sentences instead - ---- +- Write in one consistent language per post; keep it accessible to non-native readers. +- Prefer simple sentence structures for complex ideas. ## Instructions for Claude -When generating LinkedIn content for Kjell Tore: - -1. **Start with his voice profile** (from this document) -2. **Check the content pillar** - which audience is this for? -3. **Choose appropriate storytelling technique** for the content type -4. **Ensure actionable conclusion** - what can the reader DO? -5. **Verify against Don'ts list** - no buzzwords, no criticism, no assumptions -6. **Keep length in check** - 800-1500 characters for posts - -**Priority:** Sound like Kjell Tore > Optimize for algorithm - -**Exception:** If a phrase or approach would harm reach (external links, engagement bait), flag it but maintain his voice in everything else. - ---- - -## Update Log - -- 2025-11-30: Initial voice profile created based on interview +This is a placeholder. When it is in place (sentinel present), treat the defaults +above as a reasonable baseline, and prompt the user to personalize via +`/linkedin:setup`. Once personalized, these instructions are replaced by the +user's own profile. ## Collected Post Samples - + diff --git a/plugins/linkedin-studio/assets/voice-samples/authentic-voice-samples.template.md b/plugins/linkedin-studio/assets/voice-samples/authentic-voice-samples.template.md new file mode 100644 index 0000000..1a2673a --- /dev/null +++ b/plugins/linkedin-studio/assets/voice-samples/authentic-voice-samples.template.md @@ -0,0 +1,61 @@ +# Authentic Voice Samples — Template + +Fill this in with YOUR voice, then save it as `authentic-voice-samples.md` +(or, to keep it out of version control, `authentic-voice-samples.local.md`). +`/linkedin:setup` can build this for you from 3–5 of your own posts. + +Delete every `[bracketed]` prompt as you replace it. Do NOT leave the +`` sentinel anywhere in your finished profile — its +presence keeps the voice personalization score at 0. + +## Core Voice Characteristics + +[List 4–6 traits that define how you write. For each: a one-line description and, +where useful, the patterns you reach for. Examples: solution-oriented, factually +grounded, story-led, non-judgmental, actionable.] + +## Do's + +- ✅ [Things that sound like you — openings, structure, language level, how you close.] + +## Don'ts + +- ❌ [Words, tones, and moves you avoid — buzzwords, em dashes, over-long posts, etc.] + +## Signature Phrases + +[The phrases you genuinely use to transition into insight or demonstration. +Keep the list short and real — forced catchphrases read as fake.] + +## Vocabulary Preferences + +### Terms to always explain on first use +[Domain acronyms/terms your audience may not know.] + +### Words/phrases to AVOID +[Your personal no-go list.] + +## Language Guidelines + +- [Which language(s) you publish in, and any accessibility rules — e.g. simple + sentences for complex ideas, avoid hard-to-translate idioms.] + +## Technical Depth Adaptation + +[If you write for multiple audiences, note how depth shifts per audience +(leaders vs practitioners vs power users).] + +## Instructions for Claude + +When generating content in this voice: +1. Start from this profile. +2. [Your priority order — e.g. "sound like me > optimize for algorithm".] +3. Verify against the Don'ts list before finishing. + +## Update Log + +- [YYYY-MM-DD]: Initial voice profile created. + +## Collected Post Samples + + diff --git a/plugins/linkedin-studio/commands/onboarding.md b/plugins/linkedin-studio/commands/onboarding.md index 9bdf69a..24952c3 100644 --- a/plugins/linkedin-studio/commands/onboarding.md +++ b/plugins/linkedin-studio/commands/onboarding.md @@ -139,7 +139,13 @@ Use AskUserQuestion: 4. "Paste a paragraph you've written that sounds like YOU (email, doc, anything)" 5. "Any words or phrases you'd NEVER use?" -Save responses to `assets/voice-samples/authentic-voice-samples.md` under a new section `## Quick Voice Interview` (append, don't overwrite existing content). +Save the responses to `assets/voice-samples/authentic-voice-samples.md`. **If the +file is the shipped placeholder** (it contains ``), +**REPLACE it entirely** with the profile built from the answers — the +`` sentinel must NOT remain, or the voice score stays at +0 after the user fills it in. **If the file is already a populated profile**, add a +`## Quick Voice Interview` section instead of overwriting. Either way, the final +file must contain no ``. **If user profile selected:** Ask for: 1. Full name diff --git a/plugins/linkedin-studio/commands/setup.md b/plugins/linkedin-studio/commands/setup.md index fe464a9..83d7717 100644 --- a/plugins/linkedin-studio/commands/setup.md +++ b/plugins/linkedin-studio/commands/setup.md @@ -25,7 +25,7 @@ Read these 8 asset files and detect placeholder patterns to calculate the curren | Category | Weight | File/Directory | Placeholder Detection | |----------|--------|----------------|----------------------| -| Voice samples | 25 | `assets/voice-samples/authentic-voice-samples.md` | Check for `[Your Name]` or if file has <50 lines | +| Voice samples | 25 | `assets/voice-samples/authentic-voice-samples.md` | Placeholder if it contains the `` sentinel (or has <50 lines) | | User profile | 20 | `config/user-profile.local.md` | Check if file exists; count `[Your ` placeholders | | Case studies | 15 | `assets/case-studies/*.md` | Count non-template `.md` files (exclude `case-study-template.md`) | | Frameworks | 10 | `assets/frameworks/*.md` | Count non-template `.md` files (exclude `framework-template.md`) | @@ -96,14 +96,19 @@ Based on their answer, run the corresponding sub-workflow below. - How they handle technical depth - How they conclude (CTA style, takeaway style) 4. Read the existing `assets/voice-samples/authentic-voice-samples.md` -5. **Merge** new findings with existing content (don't overwrite existing data): +5. **If the file is the shipped placeholder** (it contains ``): + **REPLACE it entirely** with the profile built from the user's samples. The + placeholder's `` sentinel must NOT survive — if it + does, the voice category stays at 0 even after the user fills in real data. + **Otherwise** (the file is already a populated profile), **merge** new findings + into the existing content (don't discard existing data): - Update "Core Voice Characteristics" if new patterns found - Add new entries to "Do's" and "Don'ts" lists - Update "Signature Phrases" with newly detected phrases - Add "Vocabulary Preferences" based on word analysis - Update "Update Log" with today's date -6. Write the updated file back. +6. Write the file back, and confirm it contains no ``. **Important:** Ask "Would you like to paste more samples?" after analyzing the first batch. More samples = better voice model. diff --git a/plugins/linkedin-studio/hooks/scripts/__tests__/personalization-score.test.mjs b/plugins/linkedin-studio/hooks/scripts/__tests__/personalization-score.test.mjs new file mode 100644 index 0000000..96feea1 --- /dev/null +++ b/plugins/linkedin-studio/hooks/scripts/__tests__/personalization-score.test.mjs @@ -0,0 +1,66 @@ +import { describe, test, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { calculateScore } from '../personalization-score.mjs'; + +// Step 5 (remediation): the shipped voice profile is a PII-free placeholder +// carrying the sentinel below. Detection must key on the sentinel — NOT the old +// `[Your Name]` heuristic — so the placeholder earns 0 of the 25 voice points, +// and a populated profile (sentinel removed) earns them. +const SENTINEL = ''; + +// A >50-line body that contains NEITHER the sentinel NOR `[Your Name]`, so the +// only thing distinguishing placeholder from real profile is the sentinel. +const LONG_BODY = Array.from({ length: 60 }, (_, i) => `- voice characteristic line ${i + 1}`).join('\n'); + +function makePluginRoot() { + const root = mkdtempSync(join(tmpdir(), 'lis-personalization-')); + mkdirSync(join(root, 'assets', 'voice-samples'), { recursive: true }); + return root; +} + +function writeVoice(root, content) { + writeFileSync(join(root, 'assets', 'voice-samples', 'authentic-voice-samples.md'), content, 'utf-8'); +} + +describe('calculateScore — voice samples (sentinel-based placeholder detection)', () => { + let root; + + afterEach(() => { + if (root && existsSync(root)) { + rmSync(root, { recursive: true, force: true }); + } + root = undefined; + }); + + test('placeholder with the sentinel scores 0 voice points (even >50 lines, no [Your Name])', () => { + root = makePluginRoot(); + writeVoice(root, `# Voice Profile (placeholder)\n${SENTINEL}\n\n${LONG_BODY}\n`); + + const { score, personalized } = calculateScore(root); + + assert.equal(score, 0, 'placeholder must not earn the 25 voice points'); + assert.equal(personalized, 0); + }); + + test('a real profile (no sentinel, >50 lines) earns the 25 voice points', () => { + root = makePluginRoot(); + writeVoice(root, `# My Voice Profile\n\n${LONG_BODY}\n`); + + const { score, personalized } = calculateScore(root); + + assert.equal(score, 25, 'a populated profile must earn the 25 voice points'); + assert.equal(personalized, 1); + }); + + test('a short placeholder (<50 lines) also scores 0 voice points', () => { + root = makePluginRoot(); + writeVoice(root, `# Voice Profile (placeholder)\n${SENTINEL}\n`); + + const { score } = calculateScore(root); + + assert.equal(score, 0); + }); +}); diff --git a/plugins/linkedin-studio/hooks/scripts/personalization-score.mjs b/plugins/linkedin-studio/hooks/scripts/personalization-score.mjs index 90b69f9..393b28a 100644 --- a/plugins/linkedin-studio/hooks/scripts/personalization-score.mjs +++ b/plugins/linkedin-studio/hooks/scripts/personalization-score.mjs @@ -16,11 +16,15 @@ export function calculateScore(pluginRoot) { const categories = 8; // --- 1. Voice samples (25 points) --- + // The shipped file is a PII-free placeholder carrying the VOICE_PLACEHOLDER + // sentinel. Key detection on the sentinel (deterministic) rather than the old + // `[Your Name]` heuristic: a populated profile removes the sentinel and earns + // the points; the placeholder (or any file still carrying it) scores 0. const voiceFile = join(pluginRoot, 'assets', 'voice-samples', 'authentic-voice-samples.md'); if (existsSync(voiceFile)) { const content = readFileSync(voiceFile, 'utf-8'); const lineCount = content.split('\n').length; - if (lineCount > 50 && !content.includes('[Your Name]')) { + if (lineCount > 50 && !content.includes('')) { score += 25; personalized += 1; }