linkedin-studio/commands/calendar.md
Kjell Tore Guttormsen 677eab9294 feat(linkedin-studio): N13 — publiserings-slots + vacancy-varsel + WIP-roll-up + slot-default [skip-docs]
A1-10 + D-5. The queue answers "what is scheduled"; nothing answered "is the week
covered, and what is still being written". N13 makes both readable at session start.

- Publishing slots are operator config: profile/publishing-slots.json in the per-user
  data dir, schema + OPT-IN template in config/publishing-slots.template.json. The
  plugin ships no publishing times and writes the grid for nobody; absent config means
  every slot surface stays silent (same never-nag discipline as the trend/brain nudges).
  Data dir rather than the state file, which is machine-written and whose frontmatter
  reader does not parse lists.
- hooks/scripts/slots.mjs (new): one implementation behind four surfaces. Zero-dep (a
  SessionStart hook must not spawn tsx), pure core (dates in as strings, no clock),
  imported by the commands exactly as queue-manager.mjs already is — so session-start's
  vacancy warning and Step 10's slot default cannot drift apart.
- Coverage counts the short-form queue (scheduled/published; cancelled frees the slot)
  AND editions-register rows claiming that date, counted per date, so long-form and
  short-form cannot be double-booked and a two-slot day needs two posts.
- Session start gains "## Editions in Flight" (series, phase, next action, claimed slot,
  days in flight) and "## Publishing Slots" (next open slot, open count in 14 days, and
  how to fill it when the gap is <=3 days). The existing discovery nudge is untouched.
- /linkedin router + /linkedin:calendar surface the same roll-up; calendar documents the
  opt-in copy. /linkedin:newsletter Step 10 defaults to the next open slot (one
  confirmation, not an interview) and writes --slot onto the register row.
- D-5: references/scheduling-strategy.md marked "confidence: low / directional" per the
  algorithm reference's own standard, deferring to the operator's grid and their own
  analytics. Neutral example time replaces the operator-specific one in the N12 docs.

TDD: red proven first (module absent; 5 session-start behaviours failing).
hooks 140 -> 174 · test-runner 184 -> 197 (Section 16t: 13 unconditional greps incl. a
de-niche check that no publishing time is hardcoded in the slot path and no operator grid
is committed, + non-vacuity self-test; anti-erosion floor 166 -> 179).
Suites green: trends 300/0 · brain 134/0 · editions 72/0 · specifics-bank 45/0 ·
tests 35/0 · render 60/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
2026-07-25 07:28:48 +02:00

12 KiB

name description allowed-tools
linkedin:calendar View and manage your post scheduling queue. Shows next 14 days of scheduled posts, format mix, pillar balance, and runs the publish action (mark a scheduled post as published, update queue + state, first-hour engagement plan). Triggers on: "calendar", "schedule", "queue", "upcoming posts", "what's scheduled", "show queue", "my schedule", "content calendar", "publish", "mark as published", "posted today", "just published", "published a post", "post is live".
Read
Bash
Write
Edit
AskUserQuestion
Task

LinkedIn Content Calendar

You are a LinkedIn content calendar manager. Show the user their upcoming scheduled posts, help them manage the queue, and run the publish action when a scheduled post goes live.

Quick Routing

If the user's prompt mentions "publish", "mark as published", "posted today", "just published", or "post is live", jump straight to Step 3 — Action: Mark as Published (skip the full calendar view). Otherwise start at Step 1.

Step 1: Load Queue

Read the queue file and check for scheduled/overdue entries:

node --input-type=module -e "
import { queueToday, queueUpcoming, queueOverdue, queueCount, queueFormatSummary } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/queue-manager.mjs';
console.log('=== TODAY ===');
console.log(queueFormatSummary(queueToday()));
console.log('=== UPCOMING 14 DAYS ===');
console.log(queueFormatSummary(queueUpcoming(14)));
console.log('=== OVERDUE ===');
console.log(queueFormatSummary(queueOverdue()));
console.log('=== COUNTS ===');
console.log(JSON.stringify(queueCount(), null, 2));
console.log('=== ENTRY RECORDS (internal — id / draft_path / character_count etc. for the publish & reschedule actions; do NOT show the user) ===');
const _seen = new Set();
for (const e of [...queueToday(), ...queueOverdue(), ...queueUpcoming(14)]) {
  if (_seen.has(e.id)) continue; _seen.add(e.id);
  console.log(JSON.stringify({ id: e.id, draft_path: e.draft_path, scheduled_date: e.scheduled_date, scheduled_time: e.scheduled_time, hook_preview: e.hook_preview, pillar: e.pillar, format: e.format, character_count: e.character_count }));
}
"

The queueFormatSummary blocks are the human-readable overview; the ENTRY RECORDS block is the agent's lookup table for the id, draft_path, and character_count that the action steps need (these fields are not in the readable summary).

Also read state for context:

  • ~/.claude/linkedin-studio.local.md for weekly goal and current progress

Step 1b: Load Publishing Slots + Editions in Flight

The queue says what is scheduled. The slot grid says what the week is supposed to hold, and the editions register says which long-form pieces are still in production — together they turn "here are some posts" into a capacity picture:

node --input-type=module -e "
import { readSlots, slotVacancies, slotOccurrences, formatSlot, defaultSlotsPath, activeEditions } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/slots.mjs';
const grid = readSlots();
console.log('=== SLOT GRID ===');
console.log(grid.slots.length ? grid.slots.map(s => s.day + ' ' + s.time + (s.label ? ' | ' + s.label : '')).join('\n') : 'NOT CONFIGURED: ' + defaultSlotsPath());
console.log('=== OPEN SLOTS (next 14 days) ===');
console.log(slotVacancies({ days: 14 }).map(s => formatSlot(s)).join('\n') || 'none - every planned slot is covered');
console.log('=== PLANNED SLOTS (next 14 days) ===');
console.log(String(slotOccurrences(grid.slots, new Date().toISOString().slice(0,10), 14).length));
console.log('=== EDITIONS IN FLIGHT ===');
console.log(activeEditions().map(e => e.series + ' #' + e.editionId + ' | ' + e.currentPhase + ' | next: ' + (e.nextAction || 'unset') + ' | slot: ' + (e.slot || 'unclaimed') + ' | ' + e.daysInFlight + 'd in flight').join('\n') || 'none');
"

If the grid is NOT CONFIGURED, say so once and offer the opt-in — the plugin deliberately imposes no schedule:

No publishing slots configured. To get slot-vacancy warnings at session start and a
one-key slot default in /linkedin:newsletter, copy the template and edit the times:
  cp "${CLAUDE_PLUGIN_ROOT}/config/publishing-slots.template.json" \
     "${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/publishing-slots.json"

The template's times are the plugin's directional starting point (see references/scheduling-strategy.md), not a recommendation about your audience — replace them with what /linkedin:report shows for your own account.

Step 2: Display Calendar View

Present a 14-day calendar view:

Content Calendar: [YYYY-MM-DD] to [YYYY-MM-DD]
Weekly goal: X posts/week

Week [YYYY-WXX]:
  Mon [date]: —
  Tue [date]: "[hook preview]" — [pillar] ([format]) [SCHEDULED]
  Wed [date]: —
  Thu [date]: "[hook preview]" — [pillar] ([format]) [SCHEDULED]
  Fri [date]: —
  Sat [date]: "[hook preview]" — [pillar] ([format]) [SCHEDULED]
  Sun [date]: —

Week [YYYY-WXX+1]:
  [same format]

Queue stats: X scheduled | Y published | Z overdue
Format mix: X standard, Y carousel, Z quick
Pillars: [pillar counts]

Mark each configured slot on its day, so an empty slot is visible as a hole rather than inferred from an absence: Tue [date]: [SLOT 08:30 — OPEN] when nothing covers it, [SLOT 08:30 — covered] when the queue or an edition does. Below the calendar, add the capacity lines from Step 1b:

Slots: X planned in 14 days | Y open — next open: [Thu YYYY-MM-DD 12:00 (in N days)]
Editions in flight: [series #NN — phase → next action (Nd)] (or "none")

If there are overdue posts (past scheduled date, still "scheduled"), highlight them:

OVERDUE:
  [date]: "[hook preview]" — Should have been posted [N days ago]

Step 3: Offer Actions

Use AskUserQuestion:

  1. Mark as published — A scheduled post is live; update queue + state + show first-hour plan
  2. Reschedule a post — Move a post to a different date/time
  3. Cancel a post — Remove from queue (set status to "cancelled")
  4. View a draft — Read the full draft content
  5. Looks good — No changes needed

Action: Mark as Published

This is the publish flow (no separate command — runs inline here). It marks a post you posted to LinkedIn manually as published — the tool does not post on your behalf (auto-publish is deliberately not built; see the README boundaries).

3a. Show publishable posts. Present today's scheduled posts and any overdue posts:

Today's Scheduled Posts:
1. "[hook preview]" — [pillar] ([format]) — Scheduled for [time]
2. "[hook preview]" — [pillar] ([format]) — Scheduled for [time]

Overdue (should have been posted):
3. "[hook preview]" — [pillar] — Was scheduled for [date]

If no posts are scheduled and none overdue:

No posts scheduled for today.
- Run /linkedin:batch to schedule content
- Run /linkedin:quick for an unplanned quick post

3b. Pick a post. Use AskUserQuestion to ask which post was published (show the list above). Map the chosen post to its id (and draft_path/character_count if needed downstream) using the ENTRY RECORDS block emitted in Step 1 — that block is the source of the [post-id] used below.

3c. Update queue status:

node --input-type=module -e "import { queueUpdateStatus } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/queue-manager.mjs'; console.log(queueUpdateStatus('[post-id]', 'published'));"

3d. Update state file deterministically:

node --input-type=module -e "
import { writeState, updatePostTracking } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/state-updater.mjs';
writeState(content => updatePostTracking(content, {
  postDate: 'YYYY-MM-DD',
  postTopic: 'topic_area',
  hookText: 'Hook text here...',
  charCount: NNNN,
  format: 'FORMAT'
}));
"

Replace placeholders with actual post data from the published post.

3e. First-hour battle plan. Show the lightweight in-flow nudge below after marking. For the full worked sprint — timestamped engagement targets (whales / inner-circle / ICPs), draft self-comments + CEA replies in voice, and a minute-by-minute timeline — run /linkedin:firsthour, which persists the plan to state and hands off to post-feedback-monitor. The checklist here is the quick version:

Post marked as published! Here's your first-hour plan:

Pre-Post (if not done):
- [ ] Complete 5x5x5 engagement (15-20 min before posting)

First Hour:
- [ ] Respond to comments within 5 minutes
- [ ] Add value in every response (not just "thanks!")
- [ ] Ask follow-up questions to deepen conversation
- [ ] Target: 15+ engagements in first 60 minutes
- [ ] Check back at 30-min and 60-min marks

48-Hour Check-In:
- Run /linkedin:analyze after 48 hours to review performance
- For real-time tracking, delegate to the `post-feedback-monitor` agent — invoke it via `Task` with `subagent_type: linkedin-studio:post-feedback-monitor` (foreground, from this command layer) to watch the post's first-48h engagement and flag anomalies early

3f. Ask about more. Use AskUserQuestion:

  1. Mark another post — I published more than one (loop back to 3a)
  2. View calendar — Show remaining schedule (loop back to Step 2)
  3. Done — All set for now

Action: Reschedule

If they choose to reschedule:

  1. Ask which post (by number or hook preview)
  2. Ask for the new date and time
  3. Re-add the entry with the same id and new date/time — queueAdd replaces any existing entry with that id, so the post moves in place (no duplicate). Carry the unchanged fields (id, draft_path, pillar, format, hook preview, char count) from the ENTRY RECORDS block emitted in Step 1:
node --input-type=module -e "import { queueAdd } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/queue-manager.mjs'; console.log(queueAdd('[post-id]', '[draft_path]', '[new-YYYY-MM-DD]', '[new-HH:MM]', '[pillar]', '[format]', '[hook preview]', [charCount]));"
  1. Show the updated calendar

Action: Cancel

If they choose to cancel:

  1. Ask which post
  2. Confirm cancellation
  3. Update status to "cancelled":
node --input-type=module -e "import { queueUpdateStatus } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/queue-manager.mjs'; console.log(queueUpdateStatus('[post-id]', 'cancelled'));"

Action: View Draft

If they want to see a draft:

  1. Ask which post
  2. Read the draft file from the draft_path
  3. Display full content

Step 4: Balance Analysis

After showing the calendar (or after a publish action loops back), provide brief analysis:

  • Format diversity: Are formats varied enough? Flag if >2 consecutive same format.
  • Pillar balance: Are pillars well-distributed? Flag if any pillar >50%.
  • Gap detection: Are there multi-day gaps that could hurt momentum?
  • Weekly goal alignment: Will the schedule meet the weekly goal?
  • Slot vacancy: Any open slot within 3 days is the most actionable thing on the screen — name it and offer /linkedin:batch (short-form) or /linkedin:newsletter (long-form). An edition already in flight with no claimed slot is the natural filler.

Reference Files

  • ${CLAUDE_PLUGIN_ROOT}/references/scheduling-strategy.md
  • ${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md
  • ${CLAUDE_PLUGIN_ROOT}/config/publishing-slots.template.json (slot-grid schema + opt-in)
  • ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/drafts/queue.json
  • ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/publishing-slots.json (your grid)
  • ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/editions/register.json (editions in flight)