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:
Kjell Tore Guttormsen 2026-05-30 05:35:44 +02:00
commit 3ae8adb6ff
6 changed files with 272 additions and 4 deletions

View file

@ -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);
});
});