| name | post-pulse |
| description | Recurring engagement harvest for Pedro's social posts. Opens a browser, sweeps a week or month of his LinkedIn and/or Twitter/X posts, reads each post's impressions, reactions, comments, and reposts plus a few notable comments, and saves it as structured time-series data in the psstack performance store. Promotes high performers to exemplars so /ps:post drafts from what actually works. Use when Pedro wants to measure post performance, run the weekly/monthly engagement pull, refresh the social data store, or runs /ps:post-pulse. Not for drafting posts (that's /ps:post). |
| license | MIT |
| metadata | {"author":"psanders","version":"1.1"} |
post-pulse
Periodically harvest engagement for Pedro's social posts and save it as structured,
version-controlled time-series data that feeds /ps:post. Run it weekly or monthly. It
opens a browser (Claude in Chrome), finds the posts in the window, reads each one's
metrics, upserts a record per post, appends a timestamped snapshot, and promotes the
best performers to exemplar: true.
This is the measurement half of the pair. /ps:post writes; /ps:post-pulse measures and
closes the loop.
Input
- Window (positional):
week (last 7 days, default), month (last 30 days), or an explicit ISO range YYYY-MM-DD..YYYY-MM-DD.
--network linkedin|twitter|all — which network(s) to sweep. Default all.
--dry-run — scrape and report, but don't write to the data store.
The data store
Everything is written to data/social/ in the psstack repo. Read its README.md and
schema.json first if they aren't in context.
Resolve the directory as data/social/ at the root of the psstack repo (the repo
containing the ps plugin). If running as an installed plugin and you can't locate it:
try $PSSTACK_DIR/data/social, then ~/Projects/psstack/data/social, then ask Pedro.
Confirm the resolved absolute path out loud before writing.
posts.jsonl is the source of truth: one JSON object per line per post, with an
append-only snapshots[] time series inside each record. Validate any record you write
against schema.json.
Browser automation
This skill drives Chrome via the claude-in-chrome MCP tools. If they're deferred, load
the core set in ONE ToolSearch call before starting:
ToolSearch select:mcp__claude-in-chrome__tabs_context_mcp,mcp__claude-in-chrome__navigate,mcp__claude-in-chrome__computer,mcp__claude-in-chrome__read_page,mcp__claude-in-chrome__get_page_text,mcp__claude-in-chrome__tabs_create_mcp
Then call tabs_context_mcp first to see existing tabs, and open a new tab for the
work (don't hijack Pedro's tabs). Pedro must be logged in to LinkedIn / X in Chrome already
— this skill reads his own activity feed; it does not log in for him. If a profile/activity
page shows a logged-out view, stop and ask him to sign in, then resume.
Stay focused: if pages won't load, selectors don't resolve, or you hit a login wall after
2–3 attempts, stop and report what you saw rather than thrashing. Never trigger JS dialogs.
Steps
1. Resolve window, networks, and the data dir
Parse the window and --network. Resolve and confirm the data/social/ absolute path.
Load posts.jsonl into memory (it may be large; read it once).
2. Re-measure pass — refresh recent posts already in the store (do this first)
Before sweeping the new window, re-snapshot posts already in posts.jsonl so the
time series actually grows. LinkedIn engagement keeps accruing for weeks, so a single
snapshot per post is nearly worthless for trend.
- Select records whose latest
snapshots[].captured_at is more than ~10 days ago
and whose posted_at is within the last ~60 days (old enough to have moved, recent
enough to still be accruing). Skip anything already snapshotted in the last ~10 days.
- Visit each one's
url and read the current counters. Append a fresh snapshot (Step 4);
never overwrite. This is what turns single points into growth curves — the whole reason
the schema is a time series.
- This pass reuses URLs you already have, so it's cheap. Cap it at ~15 posts per run if the
list is long; prioritize the highest-engagement and most-recent ones.
3. Open the activity feed for each network
New tab, then navigate:
- LinkedIn: Pedro's profile → "Activity" → "Posts" (his own recent posts feed), e.g.
https://www.linkedin.com/in/<handle>/recent-activity/all/. If the handle isn't known, ask once and remember it for the session.
- Twitter/X: Pedro's profile timeline filtered to his posts, e.g.
https://x.com/<handle>.
Scroll to load posts until you pass the start of the window (posts older than the window
boundary). Use get_page_text / read_page to extract; scroll incrementally.
4. For each post in the window, capture
url (canonical permalink — the dedupe key)
posted_at (ISO date)
text (the body as published)
impressions (LinkedIn often shows this on your own posts; X shows "Views"). null if not visible.
reactions, comments, reposts, and saves/clicks when shown (null otherwise)
top_comments: notable comments (author + text) for qualitative signal.
Required for the ~5 highest-engagement posts of this run (across both the
re-measure and window passes): click into each and capture 2–3 comments. These are the
posts whose comments are worth mining for voice, so this is not optional for them. For
the rest, capture comments best-effort if they're already visible; don't click into
every post. (Empty top_comments across the store is a known gap — this rule closes it.)
Best-effort classify pillar, hook_pattern, and cta by matching the text against the
/ps:post references (pillars / hook-library). Leave hook_pattern: null if unsure.
5. Upsert into posts.jsonl
For each captured post:
- Match an existing record by
url (fallback: id).
- If found: append a new
snapshots[] entry with today's captured_at and the fresh metrics. Never overwrite prior snapshots — the time series is the point. Back-fill any previously-null top-level fields (e.g. the placeholder seed record) if you now have better data.
- If not found: create a record. Mint
id as <ln|tw>-<posted_at>-<slug> (slug = 3–5 kebab words from the hook). Fill the fields, set exemplar:false, and add the first snapshot.
- Validate each record against
schema.json. Keep one record per line.
Write the file back as JSONL (one object per line, newest-first by posted_at is fine but
not required). With --dry-run, skip writing and just show the diff you would make.
6. Promote exemplars
After updating, recompute exemplars. Promote a post to exemplar: true when, in its latest
snapshot, it clears the bar for its network:
- LinkedIn: impressions ≥ 3,000 or comments ≥ 15 or reactions ≥ 80. (Anchored to the KARMA hot take: 6,275 impressions / 39 comments.)
- Twitter/X: impressions ≥ 10,000 or comments ≥ 20 or reposts ≥ 30.
Keep roughly the top 1–2 exemplars per pillar per network so the example set stays sharp;
if a stronger post arrives, you may demote the weaker one (exemplar:false) and note it.
When you promote a post, also add or refresh its block in
plugins/ps/skills/post/references/examples.md (record id, performance, hook pattern, why
it worked, scaffold to mirror) so /ps:post picks it up.
7. Recompute the aggregate scorecard (patterns.md) — the learning loop
This is what feeds strategy (not just shape) back to /ps:post. After the store is
updated, regenerate data/social/patterns.md from all records in posts.jsonl:
- Recompute averages of engagement (reactions+comments+reposts, latest snapshot per post)
broken down by pillar, by CTA, by length bucket, and by day-of-week, plus the current
engagement-rate baseline (median + avg over posts with visible impressions) and the
current outliers (highest ER, highest reach).
- Write each section as a short table followed by a plain-language
→ RULE: / → WATCH:
line. Mark every cell with n<4 as directional only — do not state weak cells as rules.
- Surface contradictions explicitly. Wherever a data pattern conflicts with a rule in
/ps:post (e.g. engagement-question underperforming the skill's "always end with a
question" rule, or short posts beating its "700–1,500 chars typical" default), call it
out in the relevant section. These contradictions are the highest-value output of the run.
- Keep the file to ~1 screen: it is a summary that
/ps:post loads every draft, not an
archive. Follow the existing structure in patterns.md if present.
Then flag instruction rewrites: if a contradiction has held across two or more
consecutive pulses, propose the concrete edit to /ps:post (SKILL.md or the relevant
references/*.md) in your report — but don't edit the drafting skill's rules
automatically; let Pedro approve the change. patterns.md and examples.md are yours to
regenerate; the skill's rulebook changes only on his say-so.
With --dry-run, show the patterns.md you would write but don't save it.
8. Report
Summarize to Pedro:
- How many posts captured, per network, and the window.
- A small table: post (short hook) · pillar · impressions · comments · reactions · reposts · Δ vs previous snapshot (if any).
- New exemplars promoted (and anything demoted).
- 2–3 patterns worth acting on, pulled straight from the refreshed
patterns.md:
which pillar/hook over- or under-performed, which CTA type pulled comments, best posting
day if discernible. Keep it specific and honest; if the sample is too small to conclude,
say so. Include any instruction-rewrite proposals from Step 7 (contradictions that
held across ≥2 pulses).
- The absolute paths of the files written (
posts.jsonl, patterns.md, and
examples.md if you touched it).
9. Offer to commit the data to the repo
The data store is version-controlled, so a run isn't finished until it's saved. After
writing (and not on --dry-run):
- Show what changed:
git -C <psstack repo> status --short data/social/ and a short
git diff --stat data/social/.
- Propose a commit and, on Pedro's go-ahead, make it. Stage
data/social/ (which
includes both posts.jsonl and the regenerated patterns.md), plus
plugins/ps/skills/post/references/examples.md if you promoted/demoted an exemplar there.
Suggested message:
chore(social): post-pulse <window> — <N> posts, <M> new exemplars (<captured_at>).
Follow the repo's commit conventions (sole-author; no Co-Authored-By trailer).
- If the working tree isn't the psstack repo, or the data dir resolved outside a git repo,
skip the commit and just tell Pedro the path so he can commit it himself.
Don't push unless Pedro asks. Committing keeps the engagement time-series durable and lets
/ps:post draft from the freshest exemplars.
Running it on a schedule
This is a recurring job. Two good options Pedro can use:
- In-session loop:
/loop to self-pace, or a fixed cadence. Best when he wants to watch it.
- Scheduled cloud agent:
/ps:schedule (or the schedule skill) to run it weekly/monthly unattended. Suggested cron: weekly on Monday morning (0 9 * * 1) for a week sweep, or monthly (0 9 1 * *) for a month sweep.
Browser scraping needs a logged-in Chrome, so a fully-unattended cloud run only works if
that session has Pedro's authenticated browser. If not, schedule a reminder instead and run
the sweep interactively.
Guardrails
- Read-only on the platforms. This skill never posts, likes, comments, or follows — it only reads Pedro's own activity.
- Only Pedro's own posts. Don't scrape other people's profiles.
--dry-run before the first real write if you're unsure about selectors.
- Never fabricate metrics. If a number isn't visible, write
null, don't guess.
- Append, don't overwrite. The value is the time series.
Relationship to /ps:post
/ps:post reads exemplar: true records (via references/examples.md and the data store)
to draft from what works. /ps:post-pulse is what makes those exemplars real and current.
Run pulse on a cadence; the drafting skill gets smarter for free.