ops-social-planner · git:20260822.1ddc215 · 2026-08-22 · sha256 2dbd00adf9c9dffc

ops-social-planner git:20260822.1ddc215A

Immutable. This exact content is served forever at /api/v1/blob/2dbd00adf9c9dffc.

---
name: ops-social-planner
description: "This skill should be used when the user asks to \"content calendar\", \"what is scheduled\", or \"/ops:ops-social-planner\". Visual, engine-agnostic planner for all scheduled social posts and ads across every identity and project. Auto-generates the current state of ops-socials + ops-marketing planned content per project, per channel, regardless of posting engine (Typefully, upload-post, Meta, Google Ads), and serves a clean local web UI. Use when the user says social planner, content calendar, what's scheduled, planned posts, show my queue, posting schedule, social dashboard, or runs /ops-social-planner. Read-only — never publishes (mutation stays in /ops-socials + /ops-marketing behind per-message approval)."
argument-hint: '[--project <id>] [--collect-only] [--port <n>] [--no-open]'
allowed-tools:
  - Bash
  - Read
  - Grep
  - Glob
  - mcp__typefully__typefully_list_drafts
  - mcp__typefully__typefully_get_draft
  - mcp__typefully__typefully_get_queue
  - mcp__upload-post__list_scheduled
  - mcp__upload-post__list_profiles
effort: medium
maxTurns: 25
---

# /ops-social-planner — engine-agnostic planned-content viewer

Load `ops-rules` before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If `AskUserQuestion` / `Workflow` are missing, follow Rule 10 in `ops-rules` (Hermes: numbered options / two-turn Telegram card; `delegate_task`).

Read-only dashboard of **every** scheduled post and ad, grouped **identity/project → channel → time**,
**regardless of posting engine**. Personal/founder identity and project brands stay strictly separated
(see `/ops-socials` identity rules). See `SPEC.md` for the full design.

## What it does

1. `bin/ops-social-planner collect` reads `$PREFS_PATH/preferences.json`
   (`marketing.social_identities.personal.*` + `marketing.projects.*.social`), dispatches a
   **per-engine fetcher** keyed on `social.engine.primary`, normalizes to one schema, and writes
   `$OPS_DATA_DIR/social-planner/state.json` (**owner data — never committed**, Rule 0).
2. It then serves the static UI (`ui/`) on localhost and opens the browser.

## Engines

| `engine.primary`          | Source                                                                                 | Status                       |
| ------------------------- | -------------------------------------------------------------------------------------- | ---------------------------- |
| `typefully`               | `GET /v2/social-sets/{id}/drafts` (Bearer; key from `~/.config/typefully/config.json`) | ✅ wired                     |
| `upload-post`             | `GET /api/uploadposts/schedule` (Apikey; key from `engine.upload_post.api_key_ref`)    | ✅ wired                     |
| `meta-graph` / `meta-ads` | Graph Ads API per project BM/token                                                     | 🔌 hook (UI shows "pending") |
| `google-ads`              | GAQL per project customer id                                                           | 🔌 hook                      |
| `null` / unprovisioned    | —                                                                                      | shown fail-closed (0 items)  |

Adding an engine = adding one `fetch<Engine>()` in `bin/ops-social-planner` + a dispatch branch. No UI change.

## Run it

```bash
"${CLAUDE_PLUGIN_ROOT}/bin/ops-social-planner"                 # collect → serve → open
"${CLAUDE_PLUGIN_ROOT}/bin/ops-social-planner" collect          # regenerate state.json only
"${CLAUDE_PLUGIN_ROOT}/bin/ops-social-planner" serve --port 7937  # serve existing state
```

The UI renders `ui/state.sample.json` (synthetic, PII-free) when no live state exists, so it works
with zero credentials — useful for the PR preview.

## Agent enrichment path (optional, when invoked as a skill)

The headless collector covers posts on wired engines. When richer output is wanted, the agent can:

- Pull **ads** via MCP (`mcp__meta__*`, Google Ads) per project and merge `kind:"ad"` items into `state.json`.
- Replace the heuristic `rationale` with an LLM-written one (slot + sequence + channel intent).
- Re-run `serve` to refresh.
  Do **not** publish or edit from here — routing/mutation belongs to `/ops-socials` + `/ops-marketing`.

## Guarantees

- Read-only. No secrets in the client. Server binds `127.0.0.1` only.
- Identity separation honored: personal vs project never merged; unprovisioned projects render fail-closed.
- No owner data committed — only `ui/` + `bin/` + `SPEC.md` + the synthetic fixture.