feat(linkedin-studio): short-form de-AI gate via differentiation-checker + voice-guardian
Wire the orphan differentiation-checker (#10) into the five short-form creation commands (post/quick/react/carousel/video) as a De-AI / Differentiation Gate at each command's quality-check step: confirm the LinkedIn-named substance signals (personal substance, original thinking, concrete specifics, genuine voice) + a soft engagement-bait check, and delegate an originality pass to linkedin-studio:differentiation-checker when the angle risks commodity content. Add Task to allowed-tools in quick/react/carousel (post/video already had it from Step 13). Extend (not duplicate) hooks/prompts/voice-guardian.md's AI-pattern section with the same named signals from research/01 D8 + research/03 D4. Runtime-loaded prompt — no compile-hooks.py, no hooks.json change (verified: compile-hooks --check reports no drift). Test: new hooks/scripts/__tests__/linkedin-content-filter.test.mjs pins the content/non-content boundary the gate is scoped by (14 tests). Full hook suite 76/76, structure lint 61/61. Plan Step 14 (Wave 4 S2). Counts unchanged (26 commands / 19 agents). [skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9f65daa288
commit
e2ed3eb0aa
7 changed files with 131 additions and 0 deletions
|
|
@ -12,6 +12,7 @@ allowed-tools:
|
|||
- Bash
|
||||
- AskUserQuestion
|
||||
- mcp__mcp-image__generate_image
|
||||
- Task
|
||||
---
|
||||
<!-- MCP_IMAGE_TEXT_OVERLAY: VERIFIED -->
|
||||
<!-- MERMAID_CHART_RESOLUTION: UNTESTED -->
|
||||
|
|
@ -117,6 +118,12 @@ Run against the Carousel Quality Checklist from `carousel-templates.md`:
|
|||
|
||||
If any item fails, fix before presenting.
|
||||
|
||||
### De-AI / Differentiation Gate
|
||||
|
||||
The caption is the feed text, and it rides the same low-substance down-rank LinkedIn confirmed. Confirm the caption and cover slide carry the signals LinkedIn named — **personal substance, original thinking, concrete specifics, genuine voice** — and use no mechanical-response engagement bait ("Comment YES", "Like for Part 2"); a genuine question is fine. (The voice-guardian hook scores the caption on save.)
|
||||
|
||||
If the deck's premise is a list the audience has seen many times — commodity content — delegate an originality pass to the `differentiation-checker` agent: invoke it via `Task` with `subagent_type: linkedin-studio:differentiation-checker` (foreground, from this command layer), then sharpen the angle before generating slides.
|
||||
|
||||
## Step 5.5: Generate Slide Images
|
||||
|
||||
Generate a visual for each slide using mcp-image (Nano Banana Pro). If mcp-image is unavailable or fails, skip this step — the command degrades gracefully to text-only output with a manual design guide.
|
||||
|
|
|
|||
|
|
@ -134,6 +134,12 @@ Before presenting, verify against `assets/checklists/quality-scorecard.md`:
|
|||
- [ ] Topic aligns with user's 5 core expertise areas
|
||||
- [ ] Passes thought leadership test (helps someone decide or think differently)
|
||||
|
||||
### De-AI / Differentiation Gate
|
||||
|
||||
LinkedIn reach-suppresses low-substance AI content (officially confirmed — down to first-degree connections, not deleted). Confirm the draft carries the signals LinkedIn named — **personal substance, original thinking, concrete specifics, genuine voice** — and uses no mechanical-response engagement bait ("Comment YES", "Like for Part 2"); a genuine question is fine. (The voice-guardian hook scores this automatically on save.)
|
||||
|
||||
If the angle risks being commodity content — a take the audience has seen many times — delegate an originality pass to the `differentiation-checker` agent: invoke it via `Task` with `subagent_type: linkedin-studio:differentiation-checker` (foreground, from this command layer), then apply its angle suggestions before presenting.
|
||||
|
||||
## Step 6: Present Draft
|
||||
|
||||
Present ONE draft with:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ allowed-tools:
|
|||
- Read
|
||||
- Bash
|
||||
- AskUserQuestion
|
||||
- Task
|
||||
---
|
||||
|
||||
# Quick LinkedIn Post (5-Minute Workflow)
|
||||
|
|
@ -144,6 +145,10 @@ Create the post, then verify:
|
|||
|
||||
**All 6 = Yes? -> Ready to post.**
|
||||
|
||||
### De-AI / Differentiation Gate (fast)
|
||||
|
||||
Even quick posts ride the low-substance down-rank LinkedIn confirmed. Confirm one concrete specific plus a genuine point of view (not generic advice), and no mechanical-response bait ("Comment YES", "Like for Part 2") — a real question is fine. (The voice-guardian hook scores this on save.) Only when the take feels like commodity content does an originality pass earn its time: delegate to the `differentiation-checker` agent via `Task` with `subagent_type: linkedin-studio:differentiation-checker` — otherwise keep the 5-minute promise and skip it.
|
||||
|
||||
## Step 6: Present Draft
|
||||
|
||||
Show the post with:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ allowed-tools:
|
|||
- WebFetch
|
||||
- Bash
|
||||
- AskUserQuestion
|
||||
- Task
|
||||
---
|
||||
|
||||
# React to External Content — URL-to-Post Pipeline
|
||||
|
|
@ -126,6 +127,12 @@ Verify against quality rules:
|
|||
- [ ] Topic aligns with expertise areas
|
||||
- [ ] CTA invites discussion, not just "What do you think?"
|
||||
|
||||
### De-AI / Differentiation Gate
|
||||
|
||||
A reaction still has to add something only you can. Confirm the draft carries the signals LinkedIn named — **personal substance, original thinking, concrete specifics, genuine voice** — and uses no mechanical-response engagement bait ("Comment YES", "Like for Part 2"); a genuine question is fine. (The voice-guardian hook scores this on save.)
|
||||
|
||||
If your take echoes the source instead of extending it — commodity reaction — delegate an originality pass to the `differentiation-checker` agent: invoke it via `Task` with `subagent_type: linkedin-studio:differentiation-checker` (foreground, from this command layer), then sharpen the angle before presenting.
|
||||
|
||||
## Step 7: Present Draft
|
||||
|
||||
Show:
|
||||
|
|
|
|||
|
|
@ -121,6 +121,12 @@ Before presenting, verify the script passes the video quality gate:
|
|||
- [ ] Topic aligns with expertise pillars
|
||||
- [ ] No external links in post caption
|
||||
|
||||
### De-AI / Differentiation Gate
|
||||
|
||||
The post caption rides the same low-substance down-rank LinkedIn confirmed for text. Confirm the script's core idea and caption carry the signals LinkedIn named — **personal substance, original thinking, concrete specifics, genuine voice** — and use no mechanical-response engagement bait ("Comment YES", "Like for Part 2"); a genuine question is fine. (The voice-guardian hook scores the caption on save.)
|
||||
|
||||
If the idea is a take the audience has seen many times — commodity content — delegate an originality pass to the `differentiation-checker` agent: invoke it via `Task` with `subagent_type: linkedin-studio:differentiation-checker` (foreground, from this command layer), then sharpen the angle before presenting.
|
||||
|
||||
## Step 6: Present the Script
|
||||
|
||||
Present using the standardized output format:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,19 @@ Scan for these common AI writing patterns:
|
|||
|
||||
If 3+ AI patterns detected, flag: 'Voice Guardian Alert: This content scores below authenticity threshold. AI patterns found: [list specific patterns]. Suggested fixes: [specific rewrites using natural language].'
|
||||
|
||||
### LinkedIn-named substance signals (official de-AI down-rank)
|
||||
|
||||
LinkedIn confirmed (VP Laura Lorenzetti, 2026-05-19) an active program that **reach-suppresses** generic AI posts/comments and attention-bait — down to first-degree connections, not deletion — using ML trained on human-annotated "original thinking" vs "lacking substance." Beyond the generic tells above, check the draft for the four signals LinkedIn *named*. This is the differentiation surface, not an unverified SEO tell-list:
|
||||
|
||||
- **Personal substance** — a lived detail, stake, or first-hand observation only this author has. Generic advice anyone could have written is the failure mode.
|
||||
- **Original thinking** — a take or synthesis, not a restatement of the consensus.
|
||||
- **Concrete specifics** — named tools, real numbers, a dated example — not abstract nouns.
|
||||
- **Genuine voice** — reads as the author, not a model-default cadence.
|
||||
|
||||
If two or more of these are missing, flag it alongside the AI-pattern alert: the post risks the low-substance down-rank, not merely sounding generic.
|
||||
|
||||
**Soft engagement-bait check:** block mechanical-response CTAs — "Comment YES", "Like for Part 2", "DM me 'X'", "Repost if you agree" — which trigger a post-level throttle. A *genuine* open question is not penalized; the line is a real answer vs a reflexive token.
|
||||
|
||||
## 2. Six-Dimension Voice Drift Scoring
|
||||
|
||||
Read the voice profile and collected post samples from `${CLAUDE_PLUGIN_ROOT}/assets/voice-samples/authentic-voice-samples.md`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isLinkedInContent } from '../linkedin-content-filter.mjs';
|
||||
|
||||
// The short-form de-AI / differentiation gate (Step 14) is scoped by the same
|
||||
// content/non-content boundary the content-gatekeeper hook uses. These tests
|
||||
// pin that boundary so the gate fires on the short-form draft paths the five
|
||||
// creation commands (post/quick/react/carousel/video) actually write to, and
|
||||
// stays silent on plugin infrastructure (commands, hooks, references, config).
|
||||
|
||||
describe('isLinkedInContent — short-form gate scope (positive)', () => {
|
||||
test('fires on a draft post under assets/drafts/', () => {
|
||||
assert.equal(isLinkedInContent('assets/drafts/2026-05-30-ai-governance.md'), true);
|
||||
});
|
||||
|
||||
test('fires on a carousel slide image under assets/drafts/', () => {
|
||||
assert.equal(isLinkedInContent('assets/drafts/carousel-20260530-ai/slide-1.png'), true);
|
||||
});
|
||||
|
||||
test('fires on a nested absolute assets/drafts path', () => {
|
||||
assert.equal(
|
||||
isLinkedInContent('/Users/ktg/work/assets/drafts/video-script.md'),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('fires on a linkedin-posts content path', () => {
|
||||
assert.equal(isLinkedInContent('/Users/ktg/linkedin-posts/quick-take.md'), true);
|
||||
});
|
||||
|
||||
test('fires on a linkedin-studio/assets content path', () => {
|
||||
assert.equal(
|
||||
isLinkedInContent('/repo/plugins/linkedin-studio/assets/examples/post.md'),
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLinkedInContent — gate stays silent on infrastructure (negative)', () => {
|
||||
test('does not fire on a command file', () => {
|
||||
assert.equal(isLinkedInContent('commands/post.md'), false);
|
||||
});
|
||||
|
||||
test('does not fire on a hook prompt', () => {
|
||||
assert.equal(isLinkedInContent('hooks/prompts/voice-guardian.md'), false);
|
||||
});
|
||||
|
||||
test('does not fire on a reference doc', () => {
|
||||
assert.equal(isLinkedInContent('references/linkedin-formats.md'), false);
|
||||
});
|
||||
|
||||
test('does not fire on an agent definition', () => {
|
||||
assert.equal(isLinkedInContent('agents/differentiation-checker.md'), false);
|
||||
});
|
||||
|
||||
test('does not fire on a script or its tests', () => {
|
||||
assert.equal(isLinkedInContent('hooks/scripts/state-updater.mjs'), false);
|
||||
assert.equal(
|
||||
isLinkedInContent('hooks/scripts/__tests__/linkedin-content-filter.test.mjs'),
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLinkedInContent — code/config/template/meta files (negative)', () => {
|
||||
test('does not fire on code/config extensions', () => {
|
||||
for (const p of ['x.json', 'x.mjs', 'x.ts', 'x.yaml', 'x.css', 'x.html']) {
|
||||
assert.equal(isLinkedInContent(p), false, `${p} should be non-content`);
|
||||
}
|
||||
});
|
||||
|
||||
test('does not fire on template files', () => {
|
||||
assert.equal(isLinkedInContent('config/state-file.template.md'), false);
|
||||
});
|
||||
|
||||
test('does not fire on known meta filenames', () => {
|
||||
for (const p of ['CLAUDE.md', 'README.md', 'CHANGELOG.md', 'notes.local.md']) {
|
||||
assert.equal(isLinkedInContent(p), false, `${p} should be non-content`);
|
||||
}
|
||||
});
|
||||
|
||||
test('returns false for empty or missing path', () => {
|
||||
assert.equal(isLinkedInContent(''), false);
|
||||
assert.equal(isLinkedInContent(null), false);
|
||||
assert.equal(isLinkedInContent(undefined), false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue