feat(linkedin-studio): first-hour/reply-loop command with tracked state
Wire orphan agent #11 (engagement-coach) by giving it a command surface, and add the tracked first-hour state the plan calls for (remediation Step 16). - commands/firsthour.md (new, 27th command): post-publish first-hour / reply-loop sprint. Delegates plan construction to engagement-coach via Task (subagent_type: linkedin-studio:engagement-coach) — returns a grouped target list (whales/inner-circle/ICPs/new connections), 2-3 seed self-comments + 3-5 CEA replies in the user's voice, and a minute-by-minute timeline anchored to publish time. Presents timeline/targets/drafts + velocity checkpoints, auto-copies the drafts to clipboard, persists the plan, then hands off to post-feedback-monitor for the 48h window. - hooks/scripts/state-updater.mjs: new pure mutation recordFirstHourPlan() mirroring updatePostTracking — additive by contract (inserts last_firsthour_date after last_post_date when absent, creates the ## First-Hour Plans section when absent, never touches existing fields). Section name is deliberately non-R-initial so it stays outside pruneContentHistory's "## Recent Posts ... (?=\n## [^R])" capture window. + a --record-firsthour CLI branch for parity with the other mutations. - config/state-file.template.md: additive scalars (last_firsthour_date, firsthour_active) + the ## First-Hour Plans section. - hooks/scripts/__tests__/state-updater.test.mjs: extend (existing file) with 7 recordFirstHourPlan tests — section creation, field insertion vs in-place update (no duplication), round-trip non-interference, graceful empty defaults, changes array. - CLAUDE.md: register the command (## Commands 26 -> 27, table row). - scripts/test-runner.sh: EXPECT_COMMANDS 26 -> 27 (registration guard). Verify: grep 'subagent_type: linkedin-studio:engagement-coach' commands/ -> firsthour.md; node --test state-updater -> 26/26; full hook suite -> 83/83; bash scripts/test-runner.sh -> exit 0 (62 passed, commands 27/27). Plan Step 16 (Wave 4 S3). [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
29229c0b01
commit
3ae8adb6ff
6 changed files with 272 additions and 4 deletions
|
|
@ -32,9 +32,9 @@ Full-spectrum LinkedIn content engine — short-form feed posts, carousels, vide
|
|||
|
||||
**Hook editing:** Edit `hooks/hooks.template.json` + `hooks/prompts/*.md`, then run `python3 hooks/scripts/compile-hooks.py`. Do not edit `hooks.json` directly. Prompts are loaded at runtime by gatekeeper scripts; the compile step is only needed when adding `type: prompt` hooks.
|
||||
|
||||
## Commands (26)
|
||||
## Commands (27)
|
||||
|
||||
All content commands (post, quick, react, pipeline, first-post, video, multiplatform, carousel, newsletter) auto-copy output to clipboard via `clipboard-helper.mjs`. Interactive steps are minimized — angle, format, and post type are inferred from context, with max 2 questions per post. **v2.0.0 net change:** 5 commands removed (`templates`, `publish`, `authority`, `collab`, `speaking` — absorbed into `quick`, `calendar`, `strategy`, `outreach` respectively) + 2 commands added (`newsletter`, `outreach`) = 27 → 24. **v3.1.0** adds 2 longform companions (`headless-review`, `pivot`) = 24 → 26.
|
||||
All content commands (post, quick, react, pipeline, first-post, video, multiplatform, carousel, newsletter) auto-copy output to clipboard via `clipboard-helper.mjs`. Interactive steps are minimized — angle, format, and post type are inferred from context, with max 2 questions per post. **v2.0.0 net change:** 5 commands removed (`templates`, `publish`, `authority`, `collab`, `speaking` — absorbed into `quick`, `calendar`, `strategy`, `outreach` respectively) + 2 commands added (`newsletter`, `outreach`) = 27 → 24. **v3.1.0** adds 2 longform companions (`headless-review`, `pivot`) = 24 → 26. **Remediation Step 16** adds `firsthour` (wiring orphan agent #11 `engagement-coach`) = 26 → 27.
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
|
|
@ -51,6 +51,7 @@ All content commands (post, quick, react, pipeline, first-post, video, multiplat
|
|||
| `/linkedin:pivot` | **(v3.1)** Re-open a long-form edition after a late substantive change so cleared gates (fact-check → editorial → persona → headless) re-run before lock; logs `pivots[]`, resets `currentPhase`, un-locks if needed (pivot heuristic: >20 % word-count change or >2 new sections) |
|
||||
| `/linkedin:batch` | Create a full week of content |
|
||||
| `/linkedin:calendar` | View/manage post scheduling queue + publish action (mark scheduled posts as published) |
|
||||
| `/linkedin:firsthour` | Post-publish first-hour / reply-loop sprint — delegates to `engagement-coach` for a timestamped target list + draft comments + timeline, persists the plan to state (`recordFirstHourPlan`), hands off to `post-feedback-monitor` |
|
||||
| `/linkedin:carousel` | Structured multi-slide carousel generator |
|
||||
| `/linkedin:video` | Video script generator (30s-2min) |
|
||||
| `/linkedin:multiplatform` | Adapt content for other platforms (short-form/cross-format; long-form → `/linkedin:newsletter`) |
|
||||
|
|
|
|||
118
plugins/linkedin-studio/commands/firsthour.md
Normal file
118
plugins/linkedin-studio/commands/firsthour.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
name: linkedin:firsthour
|
||||
description: |
|
||||
Run the critical first hour after you publish — the window that decides ~70% of a post's
|
||||
reach. Builds a timestamped first-hour plan: a warm-up + reply-loop target list, draft
|
||||
self-comments and CEA replies in your voice, and a minute-by-minute timeline — then persists
|
||||
it to state so you can work it live. Hands off to the 48-hour monitor afterwards.
|
||||
Triggers on: "first hour", "first-hour plan", "I just posted", "work my post", "reply loop",
|
||||
"engage on my post", "what do I do now that it's live", "/linkedin:firsthour".
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- AskUserQuestion
|
||||
- Task
|
||||
---
|
||||
|
||||
# First Hour / Reply Loop — Post-Publish Engagement Sprint
|
||||
|
||||
You are a LinkedIn engagement operator. A post just went live (or is about to). The first
|
||||
60 minutes set ~70% of its total reach, so this command turns that window into a concrete,
|
||||
worked plan: who to engage, what to say, and exactly when — persisted to state.
|
||||
|
||||
## Step 0: Load Context
|
||||
|
||||
- Read `~/.claude/linkedin-studio.local.md` for posting state (streak, weekly progress, recent posts, follower phase).
|
||||
- Read `assets/voice-samples/authentic-voice-samples.md` so every draft comment is in the user's voice.
|
||||
- Note the user's growth phase (follower count) — it sets daily comment volume and target split.
|
||||
|
||||
## Step 1: Identify the Post
|
||||
|
||||
Establish what just shipped. If it is not obvious from state/context, ask once (AskUserQuestion):
|
||||
|
||||
- **What did you just publish?** (topic + the hook/first line)
|
||||
- **When did it go live?** (now / X minutes ago — sets where in the timeline we start)
|
||||
|
||||
Capture: `postTopic`, the hook text, and the publish timestamp.
|
||||
|
||||
## Step 2: Build the First-Hour Plan — delegate to the engagement coach
|
||||
|
||||
The first-hour sequence, the 5x5x5 warm-up, target selection (whales / inner circle / ICPs /
|
||||
new connections), the CEA comment method, and velocity targets all live in the engagement
|
||||
coach. Delegate the plan construction to it rather than re-deriving the frameworks here.
|
||||
|
||||
Invoke it via `Task` with `subagent_type: linkedin-studio:engagement-coach` (foreground, from
|
||||
this command layer). Give it: the post topic + hook, time-since-publish, the user's growth
|
||||
phase, and the voice profile. Ask it to return:
|
||||
|
||||
1. **Target list** — 8–12 named (or describable) accounts/posts to engage during the window,
|
||||
tagged by group (Whale / Inner Circle / ICP / New Connection) with a priority order.
|
||||
2. **Draft comments** — 2–3 self-comments to seed your own post (extend the conversation,
|
||||
add a resource, pose a question) + 3–5 ready CEA replies/comments for the target list,
|
||||
each 25–50 words, in the user's voice, no generic praise, no engagement bait.
|
||||
3. **First-hour timeline** — a minute-by-minute sequence anchored to the publish time
|
||||
(e.g. `09:10 — add value self-comment`, `09:30 — reply to every comment`).
|
||||
|
||||
## Step 3: Present the Plan
|
||||
|
||||
Show, in this order:
|
||||
|
||||
1. **Timeline** (anchored to the real publish time) — what to do at each mark.
|
||||
2. **Targets** — grouped, in priority order, with the 30-minute whale window flagged.
|
||||
3. **Draft comments** — self-comments first, then the CEA replies, each labelled.
|
||||
4. **Velocity checkpoints** — the 5/15/30/60-minute reaction+comment targets, with the
|
||||
"below this = hook/timing issue" warnings, so the user can self-diagnose mid-window.
|
||||
|
||||
Auto-copy the self-comments + draft replies to clipboard silently (so they're one paste away):
|
||||
|
||||
```bash
|
||||
printf '%s' '<DRAFT_COMMENTS_BLOCK>' | node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/clipboard-helper.mjs
|
||||
```
|
||||
|
||||
Then confirm: "Copied your draft comments to clipboard."
|
||||
|
||||
## Step 4: Persist the Plan to State
|
||||
|
||||
Record the plan deterministically (additive — it creates the fields/section on older state
|
||||
files and never touches existing fields):
|
||||
|
||||
```bash
|
||||
node --input-type=module -e "
|
||||
import { writeState, recordFirstHourPlan } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/state-updater.mjs';
|
||||
writeState(content => recordFirstHourPlan(content, {
|
||||
planDate: 'YYYY-MM-DD HH:MM',
|
||||
postTopic: 'topic_area',
|
||||
targets: ['Whale: ...', 'Inner circle: ...'],
|
||||
draftComments: ['Self-comment ...', 'Reply ...'],
|
||||
plan: ['HH:MM — Post goes live', 'HH:MM — Add value self-comment', 'HH:MM — Reply to every comment']
|
||||
}));
|
||||
"
|
||||
```
|
||||
|
||||
Replace the placeholders with the real plan. This persists the plan to the **First-Hour Plans**
|
||||
section of `~/.claude/linkedin-studio.local.md` and stamps `last_firsthour_date` /
|
||||
`firsthour_active`.
|
||||
|
||||
## Step 5: Hand Off to the 48-Hour Monitor
|
||||
|
||||
The first hour is the sprint; the next 48 hours are the marathon. Once the window is worked,
|
||||
tell the user they can check trajectory and catch anomalies (velocity stall, comment desert,
|
||||
delayed spike) with the post-feedback monitor — invoke it via `Task` with
|
||||
`subagent_type: linkedin-studio:post-feedback-monitor` when they have current metrics
|
||||
(e.g. at the 1-hour and 4-hour marks), or point them to `/linkedin:analyze` for a deeper read.
|
||||
|
||||
## Principles
|
||||
|
||||
1. **Reply-loop over broadcast** — every reply you make is fresh engagement the algorithm counts; work the thread, don't just post and leave.
|
||||
2. **Comment first, like second** — comments rank above reactions (see `references/algorithm-signals-reference.md`).
|
||||
3. **Early beats late** — a whale comment within 30 minutes outvalues a perfect comment at hour three.
|
||||
4. **Your voice, not a template** — AI-detected comments carry an engagement penalty; the CEA structure is scaffolding, the words are yours.
|
||||
5. **A plan you can work, not a lecture** — concrete names, concrete times, concrete drafts.
|
||||
|
||||
## Reference Files
|
||||
|
||||
- `assets/voice-samples/authentic-voice-samples.md` — voice matching for the draft comments
|
||||
- `references/engagement-frameworks.md` — hook types, CEA, engagement hierarchy
|
||||
- `references/algorithm-signals-reference.md` — first-hour weighting, signal order, timing data
|
||||
|
|
@ -32,6 +32,10 @@ next_planned_topic: ""
|
|||
pending_5x5x5: false
|
||||
content_series_active: ""
|
||||
|
||||
# First-hour / reply-loop engagement
|
||||
last_firsthour_date: null # "YYYY-MM-DD HH:MM" of the most recent first-hour plan
|
||||
firsthour_active: false # true while a first-hour loop is in progress
|
||||
|
||||
# Profile
|
||||
expertise_areas:
|
||||
- "general"
|
||||
|
|
@ -59,3 +63,8 @@ expertise_areas:
|
|||
## Milestone Log
|
||||
|
||||
<!-- Updated when follower_count changes. Format: [YYYY-MM] count (+delta) -->
|
||||
|
||||
## First-Hour Plans
|
||||
|
||||
<!-- First-hour / reply-loop plans, newest first. Written by /linkedin:firsthour. -->
|
||||
<!-- Format: ### [YYYY-MM-DD HH:MM] topic -->
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { updatePostTracking, pruneContentHistory, updateFollowerCount } from '../state-updater.mjs';
|
||||
import { updatePostTracking, pruneContentHistory, updateFollowerCount, recordFirstHourPlan } from '../state-updater.mjs';
|
||||
|
||||
const SAMPLE_STATE = `---
|
||||
last_post_date: "2026-04-05"
|
||||
|
|
@ -293,3 +293,75 @@ describe('updateFollowerCount', () => {
|
|||
assert.ok(result.content.includes('[2026-04] 920 (+70)'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordFirstHourPlan', () => {
|
||||
const PLAN_OPTS = {
|
||||
planDate: '2026-05-30 09:00',
|
||||
postTopic: 'AI governance',
|
||||
targets: ['Whale: @bigvoice (within 30 min)', 'Inner circle: @peer'],
|
||||
draftComments: ['Your point about model-cards resonates — we found...'],
|
||||
plan: ['09:00 — Post goes live', '09:10 — Add value self-comment', '09:30 — Reply to every comment']
|
||||
};
|
||||
|
||||
test('creates a non-R-initial First-Hour Plans section when absent', () => {
|
||||
// SAMPLE_STATE has no ## First-Hour Plans section → must be created (additive)
|
||||
assert.ok(!SAMPLE_STATE.includes('## First-Hour Plans'));
|
||||
const result = recordFirstHourPlan(SAMPLE_STATE, PLAN_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('## First-Hour Plans'));
|
||||
});
|
||||
|
||||
test('records topic, targets, draft comments and timeline in the entry', () => {
|
||||
const result = recordFirstHourPlan(SAMPLE_STATE, PLAN_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('[2026-05-30 09:00]'));
|
||||
assert.ok(result.content.includes('AI governance'));
|
||||
assert.ok(result.content.includes('Whale: @bigvoice (within 30 min)'));
|
||||
assert.ok(result.content.includes('Your point about model-cards resonates'));
|
||||
assert.ok(result.content.includes('09:10 — Add value self-comment'));
|
||||
});
|
||||
|
||||
test('is additive — inserts last_firsthour_date when the field is absent', () => {
|
||||
assert.ok(!/^last_firsthour_date:/m.test(SAMPLE_STATE));
|
||||
const result = recordFirstHourPlan(SAMPLE_STATE, PLAN_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.match(result.content, /^last_firsthour_date: "2026-05-30 09:00"$/m);
|
||||
});
|
||||
|
||||
test('updates an existing last_firsthour_date without duplicating it', () => {
|
||||
const withField = SAMPLE_STATE.replace(
|
||||
'last_post_date: "2026-04-05"',
|
||||
'last_post_date: "2026-04-05"\nlast_firsthour_date: null'
|
||||
);
|
||||
const result = recordFirstHourPlan(withField, { ...PLAN_OPTS, planDate: '2026-05-30 11:00' });
|
||||
assert.notEqual(result, null);
|
||||
const hits = result.content.match(/^last_firsthour_date:/gm) || [];
|
||||
assert.equal(hits.length, 1, 'field must not be duplicated');
|
||||
assert.match(result.content, /^last_firsthour_date: "2026-05-30 11:00"$/m);
|
||||
});
|
||||
|
||||
test('leaves existing fields and sections untouched (round-trip)', () => {
|
||||
const result = recordFirstHourPlan(SAMPLE_STATE, PLAN_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.match(result.content, /^last_post_date: "2026-04-05"$/m);
|
||||
assert.match(result.content, /^follower_count: 850$/m);
|
||||
assert.ok(result.content.includes('- [2026-04-05] "AI governance is not about..."'));
|
||||
});
|
||||
|
||||
test('gracefully defaults empty targets/comments/plan', () => {
|
||||
const result = recordFirstHourPlan(SAMPLE_STATE, {
|
||||
planDate: '2026-05-30 09:00',
|
||||
postTopic: 'minimal'
|
||||
});
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('## First-Hour Plans'));
|
||||
assert.ok(result.content.includes('minimal'));
|
||||
});
|
||||
|
||||
test('returns a changes array describing what changed', () => {
|
||||
const result = recordFirstHourPlan(SAMPLE_STATE, PLAN_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(Array.isArray(result.changes));
|
||||
assert.ok(result.changes.length > 0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -202,6 +202,63 @@ export function updateFollowerCount(stateContent, { count, month }) {
|
|||
return { content, changes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a first-hour / reply-loop engagement plan deterministically.
|
||||
*
|
||||
* Additive by contract (older state files predate these fields): a missing
|
||||
* scalar is inserted, a missing section is created — existing fields are never
|
||||
* touched. Mirrors updatePostTracking: scalar replace + newest-first section
|
||||
* append. The section name is deliberately non-`R`-initial so it falls outside
|
||||
* pruneContentHistory's `## Recent Posts … (?=\n## [^R])` capture window.
|
||||
*
|
||||
* @param {string} stateContent - Full state file content
|
||||
* @param {{ planDate: string, postTopic?: string, targets?: string[], draftComments?: string[], plan?: string[] }} opts
|
||||
* @returns {{ content: string, changes: string[] } | null}
|
||||
*/
|
||||
export function recordFirstHourPlan(stateContent, { planDate, postTopic = '', targets = [], draftComments = [], plan = [] }) {
|
||||
let content = stateContent;
|
||||
const changes = [];
|
||||
|
||||
// 1. last_firsthour_date — replace in place, else insert after last_post_date (additive)
|
||||
if (/^last_firsthour_date: .*/m.test(content)) {
|
||||
content = replaceField(content, 'last_firsthour_date', `"${planDate}"`);
|
||||
} else if (/^last_post_date: .*/m.test(content)) {
|
||||
content = content.replace(/^(last_post_date: .*)$/m, `$1\nlast_firsthour_date: "${planDate}"`);
|
||||
}
|
||||
changes.push(`last_firsthour_date → ${planDate}`);
|
||||
|
||||
// 2. firsthour_active flag — only touch if the field is declared (additive)
|
||||
if (/^firsthour_active: .*/m.test(content)) {
|
||||
content = replaceField(content, 'firsthour_active', 'true');
|
||||
changes.push('firsthour_active → true');
|
||||
}
|
||||
|
||||
// 3. Build the plan entry block
|
||||
const fmtList = (items) => (Array.isArray(items) && items.length ? items.map((i) => `- ${i}`).join('\n') : '- _(none)_');
|
||||
const entry = [
|
||||
`### [${planDate}] ${postTopic}`.trimEnd(),
|
||||
'**Targets:**',
|
||||
fmtList(targets),
|
||||
'**Draft comments:**',
|
||||
fmtList(draftComments),
|
||||
'**First-hour timeline:**',
|
||||
fmtList(plan),
|
||||
''
|
||||
].join('\n');
|
||||
|
||||
// 4. Append to ## First-Hour Plans (newest first); create the section if absent (additive)
|
||||
if (/^## First-Hour Plans\b/m.test(content)) {
|
||||
content = content.replace(/^(## First-Hour Plans\n\n?)/m, `$1${entry}\n`);
|
||||
} else {
|
||||
const trimmed = content.replace(/\s*$/, '');
|
||||
content = `${trimmed}\n\n## First-Hour Plans\n\n<!-- First-hour / reply-loop plans. Format: ### [YYYY-MM-DD HH:MM] topic -->\n\n${entry}\n`;
|
||||
}
|
||||
changes.push(`First-Hour Plans += ${planDate} "${postTopic}"`);
|
||||
|
||||
if (content === stateContent) return null;
|
||||
return { content, changes };
|
||||
}
|
||||
|
||||
/**
|
||||
* I/O wrapper: read state file, apply update function, write atomically.
|
||||
* @param {function(string): {content: string}|null} updateFn - Pure update function
|
||||
|
|
@ -244,10 +301,21 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||
count: parseInt(getArg('--count') || '0', 10),
|
||||
month: getArg('--month') || new Date().toISOString().slice(0, 7)
|
||||
}));
|
||||
} else if (args.includes('--record-firsthour')) {
|
||||
const getArg = (flag) => { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : ''; };
|
||||
const splitList = (s) => (s ? s.split(';').map(x => x.trim()).filter(Boolean) : []);
|
||||
writeState(content => recordFirstHourPlan(content, {
|
||||
planDate: getArg('--date') || new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||
postTopic: getArg('--topic') || '',
|
||||
targets: splitList(getArg('--targets')),
|
||||
draftComments: splitList(getArg('--comments')),
|
||||
plan: splitList(getArg('--plan'))
|
||||
}));
|
||||
} else {
|
||||
console.log('Usage:');
|
||||
console.log(' node state-updater.mjs --update-post --date YYYY-MM-DD --topic "topic" --hook "Hook text" --chars 1500 --format post');
|
||||
console.log(' node state-updater.mjs --prune [days]');
|
||||
console.log(' node state-updater.mjs --update-followers --count 920 --month 2026-04');
|
||||
console.log(' node state-updater.mjs --record-firsthour --date "YYYY-MM-DD HH:MM" --topic "topic" --targets "a;b" --comments "c;d" --plan "e;f"');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ warn() { echo -e "${YELLOW}⚠${NC} $1"; WARN=$((WARN + 1)); }
|
|||
# Source of truth: CLAUDE.md headers + STATE.md Telling. Bump these together
|
||||
# with the files when adding/removing an agent, command, reference, or skill.
|
||||
EXPECT_AGENTS=19
|
||||
EXPECT_COMMANDS=26
|
||||
EXPECT_COMMANDS=27
|
||||
EXPECT_REFS=25
|
||||
EXPECT_SKILLS=6
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue