refactor(linkedin)!: rename plugin linkedin-thought-leadership → linkedin-studio (v3.0.0)

BREAKING CHANGE: the marketplace slug, the agent namespace
(linkedin-studio:<agent>), and the runtime state-file path
(~/.claude/linkedin-studio.local.md) all change. Reinstall required;
existing state migrated in place (post metrics, streak, history preserved).
The /linkedin:* commands are unchanged — the command namespace is set
per-command in frontmatter and was always independent of the plugin slug.
Functionality is byte-identical to v2.4.0; this release is pure identity.

- dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json
- agent namespace updated in commands/newsletter.md (only functional invoker)
- state path updated in 4 hook scripts + topic-rotation prompt + state template
- catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged)
- docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md
- historical records (CHANGELOG past entries, docs/ build artifacts,
  config-audit v5.0.0 snapshots) intentionally retain the old slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-29 11:32:02 +02:00
commit b6bb61246b
196 changed files with 164 additions and 138 deletions

View file

@ -0,0 +1,61 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Lint-test for the fact-checker fasit fixture.
// Mirrors the structure-only discipline of state-updater.test.mjs: this test
// asserts the SHAPE of the fixture (exactly 3 cases, one of each verdict, a
// non-empty fasit per case). The accuracy comparison — does the agent's live
// output actually match the fasit verdicts — is [GATE]/[OPERATØR], never
// self-certified here.
const FIXTURE_PATH = fileURLToPath(
new URL('../fixtures/fact-checker-cases.md', import.meta.url)
);
const VERDICTS = ['🟢', '🔴', '🟡'];
const fixture = readFileSync(FIXTURE_PATH, 'utf8');
// Split on "## Case N" headings; drop the preamble before the first case.
const blocks = fixture
.split(/^##\s+Case\s+\d+\b.*$/m)
.slice(1)
.map((b) => b.trim());
describe('fact-checker fixture structure', () => {
test('contains exactly 3 cases', () => {
assert.equal(blocks.length, 3, `expected 3 cases, found ${blocks.length}`);
});
test('each case carries exactly one verdict emoji', () => {
for (const [i, block] of blocks.entries()) {
const present = VERDICTS.filter((v) => block.includes(v));
assert.equal(
present.length,
1,
`case ${i + 1} must have exactly one of 🟢/🔴/🟡, found: [${present.join(', ')}]`
);
}
});
test('the three verdicts are one each of 🟢/🔴/🟡', () => {
const seen = blocks.map((b) => VERDICTS.find((v) => b.includes(v)));
assert.deepEqual(
[...seen].sort(),
[...VERDICTS].sort(),
`fixture must cover one true (🟢), one false (🔴), one unverifiable (🟡); saw ${JSON.stringify(seen)}`
);
});
test('each case has a non-empty Fasit field', () => {
for (const [i, block] of blocks.entries()) {
const m = block.match(/\*\*Fasit:\*\*\s*(.+)/);
assert.ok(m, `case ${i + 1} is missing a **Fasit:** field`);
assert.ok(
m[1].trim().length > 0,
`case ${i + 1} has an empty **Fasit:** field`
);
}
});
});