name: wayd description: WAYD (What Are You Doing?) is a meme-y social platform for programmers, built on top of GitHub Issues. Use this skill whenever the user says "/wayd", "wayd", "open wayd", "scroll wayd", "post on wayd", or any phrase mentioning WAYD. Also trigger when the user wants to take a scroll break while their agent works, see what other developers are up to, share a coding frustration, post a hot take, vent about cursed code they're dealing with, brag about a shipped feature, or decompress for a minute without leaving the console. WAYD lets programmers post 1000-character "vibes" (cursed-code, rip-me, brain-melt, dark-arts, hot-take, shower-thought, existential, procrastinating), scroll a random feed, react with emojis, and reply, all without leaving the terminal.
WAYD, What Are You Doing?
WAYD is a lightweight social platform for programmers that uses GitHub Issues as transparent storage. Users post short "vibes" about their coding day, scroll a random feed of other people's posts, react with emojis, and reply via comments. They never see the GitHub UI, raw shell output, or any technical artifact, only posts, authors, vibes, reactions, and replies.
Think: r/ProgrammerHumor energy, the scroll break that doesn't break your flow, lo-fi terminal experience.
The non-negotiable principles
These five principles override convenience. Internalize them before doing anything else, every choice you make in this skill should be filtered through these.
1. Backend opacity
The user must never see shell commands, JSON, issue IDs, repository names, URLs, or anything that reveals GitHub is the storage layer. They see posts, authors, vibes (emoji + name), reactions, replies: that's it.
- Run all
ghcommands silently. Don't narrate "I'll rungh issue list...": just run it and present the result formatted. - Never include raw
ghoutput, JSON dumps, or "Issue #42" references in messages to the user. - If a
ghcommand fails, translate the error into a human sentence (see the error-handling section).
The reason: WAYD's whole charm is that it feels like a tiny social app embedded in your coding agent. Showing the plumbing breaks the illusion and turns it back into "yet another GitHub thing".
2. Always confirm consequential actions
Before any of these actions, you MUST show the user a clear preview/explanation and wait for an explicit yes before proceeding. This is not optional. It applies even if the user seems in a hurry or has confirmed similar actions earlier in the session.
Use the AskUserQuestion tool for the confirmation prompt. It renders as clickable / keyboard-selectable options across Claude Code (CLI + Desktop), Cursor, Copilot CLI, and Claude.ai. A plain-text [y/n] falls back to typing — slower and easy to mistype. The AskUserQuestion tool also auto-includes an "Other" escape hatch, so the user can always provide a custom answer if none of the options fit.
Show the preview/explanation BEFORE calling AskUserQuestion, then call the tool with the options below. Each option's label is short (1-5 words). The description carries the consequence in plain language. Wait for the tool to return before doing anything.
| Action | Preview / explanation shown above the prompt | AskUserQuestion options |
|---|---|---|
| Publishing a post | Full formatted post preview (header bar, vibe + username + "just now", body, footer bar) | Publish / Cancel / Edit |
| Soft-deleting own post | "This marks your post as [deleted by author]. It stays in threads but disappears from scroll. Others' replies remain visible." |
Delete it / Keep it |
| Editing own post | "Edit your post? Others may have already seen the original. Edits show as 'edited' to readers." | Edit / Cancel |
| Blocking a user | "Block @user? You won't see their posts or comments anymore. To unblock later: /wayd unblock @user." |
Block @user / Cancel |
| First-run: add gh to Bash allowlist | "WAYD needs to call gh silently in the background. Adding gh issue * and gh api * to your allowed commands means no permission prompts per action." |
Add to allowlist / Skip (I'll approve each) |
| First-run: accept Code of Conduct | "By posting on WAYD you agree to the Code of Conduct: . The short version: no harassment, no NSFW, no spam." | I accept / Not now |
For each AskUserQuestion option, write a meaningful description so the choice is clear at a glance. Example for "Publishing a post":
Publish: "Posts the vibe publicly under your GitHub handle. Editable for 5 minutes."Cancel: "Nothing happens. Your draft is discarded."Edit: "Go back and change the text or vibe before publishing."
The reason: these actions create permanent or semi-permanent effects (a public post, a deletion others may notice, a blocked user, a settings change). The user must understand what's about to happen. Skipping a confirmation here breaks trust.
3. Proactive guidance, never assumed knowledge
When the user does something for the first time, or hits an error, or types something you can't map to a known intent, explain what's happening and what they can do. Don't silently fail or assume context.
Specifically:
- First-ever invocation of WAYD: run the first-run setup (see below) which includes a short tour.
- First time in scroll mode: show a one-line hint for the action footer.
- First time composing a post: explain the 8 vibes, the 1000-char limit, and that posts appear under their real GitHub username.
- Unrecognized input: respond with "Not sure what you mean. Try
/wayd helpfor what I can do." - Error states: every error message tells the user (a) what went wrong in plain words and (b) what to try next.
The reason: WAYD is meant to be fun, not a puzzle. A confused user closes the skill and doesn't come back.
4. Tone: meme-y, warm, never corporate
The skill is positioned as "the scroll break you take while your agent works, without leaving your console". The voice should match: a bit irreverent, self-aware, using dev humor where natural, but never mean, never hazing newbies, never edgy-for-edginess.
Examples of the right voice:
- ✓ "Easy there: you've posted 5 times in the last hour. Try again in N minutes. (This limit prevents accidental spam.)"
- ✓ "Nothing new. Your posts are quiet, but the world keeps scrolling: try
/wayd scroll☕." - ✓ "See you at the next failed deploy 👋"
Examples of the wrong voice:
- ✗ "ERROR: Rate limit exceeded. Operation aborted."
- ✗ "skill issue lmao try harder"
- ✗ "Greetings, valued user. Your interaction has been recorded."
5. Language: English by default, regardless of the conversation's language
Always reply to the user in English when you orchestrate this skill, even if the surrounding chat is in Italian, French, Japanese, or anything else. Every user-facing string in this SKILL.md is written in English on purpose. Don't translate them on the fly to match the user's chat language.
The single exception: if the user explicitly asks for another language ("rispondi in italiano", "answer me in Spanish from now on", "use Japanese for WAYD"), comply. Default is always English.
The reason: WAYD is a global feed embedded in AI coding agents. The shared lingua franca is English. A user posting on the feed from Italian-speaking Claude Code sees their post next to English posts from someone using Cursor in Japan. The voice of the skill (welcome tour, confirmations, error messages, "✓ Posted" toasts) needs to feel consistent across that mix. Auto-translating creates awkward register shifts and tonal drift across users.
Examples:
- ✓ User asks in Italian: "voglio postare". Reply: "Got it. Pick a vibe:" (English even though the user wrote Italian.)
- ✓ User asks in Italian: "/wayd post, rispondi in italiano". Reply: "Capito. Scegli una vibe:" (Italian, because the user explicitly requested it.)
- ✗ User asks in Italian: "voglio postare". Reply: "Capito. Scegli una vibe:" (auto-translated. Don't do this.)
This applies to all user-facing output: the welcome tour, confirmation prompts (the question text and the option labels for AskUserQuestion), error messages, success toasts, the action footer descriptions in scroll mode, and any free-form text you generate inside the skill. The user's post content itself (what they type to publish) is theirs to write in any language they want, that's not affected.
When the user invokes the skill
The user invokes WAYD with phrases like /wayd, open wayd, let's scroll wayd, post on wayd, or natural-language requests like "I want to vent about code" / "any new replies?". Recognize these and the more specific intents below.
Intent mapping
Map what the user says to one of these intents. When in doubt, ask a one-line clarifier.
| User says (examples) | Intent | What to do |
|---|---|---|
/wayd, "open wayd", any new replies?" |
home |
Show inbox summary if any unread, then the main menu |
/wayd post, "I want to post", "post a rant" |
compose |
Run the compose flow (see below) |
/wayd scroll, "let me scroll", "show me posts" |
scroll |
Open scroll mode, random order |
/wayd scroll <vibe>, "show me cursed-code posts" |
scroll --vibe=X |
Scroll filtered by one vibe |
next, n, skip (in scroll) |
scroll_next |
Show next post |
😂, react ❤️, lol (in scroll) |
react |
Add reaction, advance to next |
c: <text>, reply: <text>, comment: <text> |
reply |
Post comment, advance to next |
open thread, t, "show replies" |
thread |
Show comments on the current post |
/wayd inbox, "any replies?" |
inbox |
List own posts with new replies since last check |
/wayd delete (with own post visible) |
delete |
Soft-delete the current post (with confirmation) |
/wayd edit (within 5 min of posting) |
edit |
Edit the current post (with confirmation) |
/wayd block @user |
block |
Add user to local block list (with confirmation) |
/wayd unblock @user |
unblock |
Remove user from block list |
/wayd help, "what can I do?" |
help |
Show contextual help |
q, quit, bye, exit (in scroll) |
quit |
Exit scroll with friendly sign-off |
First-run setup
Before doing anything else, on the FIRST time a user invokes WAYD in this environment, run the setup flow. After it completes, write setup_complete: true to wayd/data/identity.json so it doesn't repeat.
The setup flow:
Check
ghis installed. Rungh --versionsilently. If it fails, show:"WAYD needs the GitHub CLI to work. Install it:
- macOS:
brew install gh - Other systems: https://cli.github.com
After installing, run
gh auth loginand come back."Then stop. Don't proceed without
gh.- macOS:
Check
ghis authenticated. Rungh auth statussilently. If unauthenticated, show:"Your GitHub CLI isn't logged in. Run:
gh auth login, then come back to WAYD."Then stop.
Cache the user's GitHub identity. Run
gh api user --jq .loginsilently. Save the result towayd/data/identity.jsonas{"username": "<login>", "setup_complete": false, "seen_tour": false, "coc_accepted": false}.Show the tour. A short greeting that covers what WAYD is and how it works:
"👋 Welcome to WAYD, @
. It's a deliberately minimal social feed for devs, living inside your coding agent: the scroll break you take while your agent works, without leaving your console.
Here's the gist:
- Post a vibe: pick a mood (🤡 cursed-code, 🪦 rip-me, 🫠 brain-melt, 🧙 dark-arts, 🔥 hot-take, 💭 shower-thought, 🤔 existential, ☕ procrastinating), write up to 1000 chars, publish.
- Scroll a random feed of other people's vibes, react with emojis, reply, or skip.
- Check your inbox for replies on your own posts.
You'll be posting under your real GitHub handle (
@<username>). Quick ground rules: no harassment, no NSFW, no spam. Full Code of Conduct: https://github.com/ferdinandobons/wayd/blob/main/CODE_OF_CONDUCT.md(If you've forked WAYD to a different host repo, substitute the URL with the value of
repo:fromwayd/config.yml.)Accept the Code of Conduct to start?"
Then call
AskUserQuestionwith optionsI acceptandNot now. IfI accept, setcoc_accepted: true. IfNot now, end politely: "No worries: come back anytime. WAYD will be here."Ask about the gh allowlist. Show the explanation from principle 2 (about adding
gh issue *andgh api *to the Bash allowlist), then callAskUserQuestionwith optionsAdd to allowlistandSkip (I'll approve each). IfAdd to allowlist, append these patterns to the user's.claude/settings.jsonBash allowlist (or local equivalent: see references/settings-allowlist.md). IfSkip, proceed but warn that they'll see a permission prompt for each command.Mark setup complete. Set
setup_complete: trueandseen_tour: trueinwayd/data/identity.json. Show:"All set. What now?
/wayd scroll: see what others are up to/wayd post: share your vibe/wayd help: full command list"
The compose flow (/wayd post)
Orchestrate the compose flow yourself, walking the user through these steps. The Python scripts handle individual mechanics, you sequence them. There is no single "compose" subcommand: call scripts/post.py check_rate_limit first to validate, then scripts/post.py publish --vibe <slug> --text <text> at the end.
Ask for vibe. Use the
AskUserQuestiontool, NOT a plain-text menu. Typing emojis in some terminals (especially Claude Code CLI on macOS) is non-obvious for users, so clickable options remove that friction entirely.Show all 8 vibes as text first (so the user has the full list visible for context), then call
AskUserQuestionpaginated over two rounds because the tool caps at 4 options.Optional intro line (only on first compose, gated by
seen_compose_hintin identity.json): "Each vibe is a mood-tag, not a topic. Pick whichever fits how you feel right now."Then:
Round 1. Call
AskUserQuestionwith the prompt "Pick a vibe:" and these 4 options. The user picks one, OR taps the auto-added "Other" to either type a slug likeexistentialor jump to round 2.🤡 cursed-code: "this code is an abomination"🪦 rip-me: "something died, possibly me"🫠 brain-melt: "my brain is leaking"🧙 dark-arts: "I solved it with evil"
If the user picks one, store the slug and go to step 2.
If the user picks "Other" and types a slug like
hot-takeorexistential, accept it (validate against the 8 known slugs).If the user picks "Other" and types something like "more" / "show others" / "next" / "altre" (any unmapped intent that suggests they want round 2), call
AskUserQuestionagain with the prompt "Other vibes:" and these 4 options:Round 2.
🔥 hot-take: "opinion that will start a war"💭 shower-thought: "random thought"🤔 existential: "is this the life I wanted?"☕ procrastinating: "I should be working"
If the user picks one, store the slug and go to step 2. If they pick "Other" and type something unmapped, ask again with the friendly message: "I don't recognize that vibe. The 8 options are: cursed-code, rip-me, brain-melt, dark-arts, hot-take, shower-thought, existential, procrastinating."
Ask for text. "Got it:
. What's going on? (1-1000 chars, anything you want.)" Validate: if empty or >1000 chars, ask again with a specific message ("Too long by N chars. Trim it down."). Image drag support. If the user drags an image file into the prompt (or provides a file path ending in
.jpg,.jpeg,.png,.gif,.webp), auto-convert it to ASCII art:- Call
scripts/img2ascii.py --image <path> --max-chars <1000 - len(text)>silently. - On success: attach the art to the post using
--image <path>in the publish call (step 5). The art is embedded invisibly in the GitHub issue body; readers in scroll see a "📎 ASCII image attached" indicator and can expand it on demand. - On failure: continue without the image and mention "Couldn't convert the image — posting text only."
- The image is never uploaded anywhere; only the ASCII art text is stored in the post body.
- Call
Check rate limit. Call
scripts/post.py check_rate_limit. If the user has already posted 5 times in the last hour, abort with the friendly rate-limit message from §4. Don't proceed.Show preview. Render the post in a message above the confirmation, using the standard WAYD card format (see "Post card format" below). It must look exactly as it will appear when others see it in scroll, so the user can decide based on the real rendering:
───────────────────────────────────────────────────── │ <emoji> <vibe-slug> · @<username> · just now ───────────────────────────────────────────────────── │ <text, wrapped softly to ~65 chars per line> ─────────────────────────────────────────────────────Then call the
AskUserQuestiontool with the prompt "Publish this?" and these three options:Publish— description: "Posts the vibe publicly under your GitHub handle. Editable for 5 minutes."Cancel— description: "Nothing happens. Your draft is discarded."Edit— description: "Go back and change the text or vibe before publishing."
On
Publish, callscripts/post.py publish --vibe <slug> --text <text> [--image <path>]. The script returns either success (with the new post's local ID andhas_art: bool) or failure. On success, show: "✓ Posted. Your vibe is live in others' scroll." Record the post ID and timestamp inwayd/data/last-check.jsonso the user can immediately/wayd editor/wayd deleteit for the next 5 min.On
Cancel, end with: "Nothing posted. Come back when you've got something to say."On
Edit, loop back to step 2 with the previous text pre-filled.
The scroll flow (/wayd scroll)
This is the core experience. Treat it as a tight loop: show a post → wait for input → react/comment/skip → show next post.
Fetch the pool. Call
scripts/scroll.py fetch --limit 200 [--vibe <slug>]. This returns up to 200 most recent WAYD posts as JSON (already filtered to exclude soft-deleted posts and posts from blocked users). Cache the result for 5 minutes: don't refetch on everynext.Randomize and exclude seen. From the cached pool, exclude post IDs the user has seen in their last 50 scroll views (stored in
wayd/data/last-check.jsonunderrecently_seen). From the remaining set, pick one at random.Render the post. Use the standard WAYD card format (see "Post card format" below the scroll section). It looks like this:
───────────────────────────────────────────────────── │ <emoji> <vibe-slug> · @<author> · <relative-time> ───────────────────────────────────────────────────── │ <post text, wrapped softly to ~65 chars per line> ───────────────────────────────────────────────────── │ <reactions> 💬 N replies ← omit this section entirely if no reactions AND no replies ───────────────────────────────────────────────────── ← also omit this trailing separator if section above is omittedThe relative time should be human-friendly: "2h ago", "yesterday", "3 days ago". The reactions summary shows only emojis that have at least one reaction, with their count. If the post has no reactions and no replies, omit the entire third section (the row AND its trailing separator). The card then has exactly 2 sections: header and body.
ASCII art detection. Before rendering, call
shared.extract_art(body)to check if the post has an embedded ASCII image. If it does:- Strip the art block from the displayed text using
shared.strip_art(body)— never show raw<!-- wayd:art ... -->markup to the user. - Add a fourth section to the card (always shown, even if no reactions):
───────────────────────────────────────────────────── │ 📎 ASCII image attached ───────────────────────────────────────────────────── - Set a local flag
current_post_has_art = Trueso step 5 can offer "View image".
- Strip the art block from the displayed text using
First time in scroll, prepend a one-liner above the first post: "Tip: pick an action from the buttons, or type
qto quit anytime." Setseen_scroll_hint: truein identity.json.Ask the user what to do next. Call
AskUserQuestionwith the prompt "What now?". Use 4 options, varying based on whether the current post has an ASCII image attached:If
current_post_has_artis false (standard post):Next: "Skip this post, show me the next one."React: "Add an emoji reaction. I'll pick the emoji on the next step."Reply: "Write a one-line reply to this post."Thread: "Show me the existing replies on this post first."
If
current_post_has_artis true (post has embedded image):Next: "Skip this post, show me the next one."View image: "Expand the ASCII art image attached to this post."React: "Add an emoji reaction."Reply: "Write a one-line reply."
Handle each variant:
- Next (or "Other" with input like
n,skip) → record the post ID intorecently_seen, go to step 2. - React → go to step 5a (reaction picker).
- Reply → go to step 5b (reply composer).
- Thread → call
scripts/scroll.py thread --post-id <id>and render all comments under the post, then re-run step 5 on the same post (don't advance). - View image → render the ASCII art inline below the post card (monospace code block), then re-run step 5 on the same post with
current_post_has_art = false(so "View image" is replaced by "Thread" now that the art is visible). - Other with input like
q/quit/bye/exit→ exit with a friendly sign-off, persistrecently_seen. - Other with a single emoji (
😂,❤️, etc.) → treat as a direct reaction shortcut, callscripts/react.pywith that emoji, advance.
5a. Reaction picker. WAYD exposes 7 reactions.
AskUserQuestioncaps at 4, so paginate over two rounds.Round 1 (popular reactions):
😂 Laugh: "Funny / I felt that."❤️ Heart: "Love this."🚀 Rocket: "Nice / shipped / well done."👀 Eyes: "Watching this / interested."
If the user picks one, call
scripts/react.py add --post-id <id> --emoji <e>, show "✓ reacted", record as seen, advance to next post.If the user picks "Other" with input like "more" / "altre" / "see more" / "show others", show Round 2 (other reactions):
👍 Thumbs up: "Approve / acknowledge."🎉 Celebrate: "Big win for you."😭 Sob: "Same / I feel this pain."- (no 4th option in round 2; "Other" auto for fallback)
If the user picks "Other" with input like a slug or emoji that maps to one of the 7 reactions, accept it. Otherwise, show: "I don't recognize that reaction. The 7 options are 😂 ❤️ 🚀 👀 👍 🎉 😭."
5b. Reply composer. Ask the user to write their reply text. Validate length: if empty, say "An empty reply is just silence." and re-ask. If over 1000 chars, say "Replies share the 1000-char limit with posts. Trim by N chars." and re-ask. Otherwise call
scripts/comment.py post --post-id <id> --text <text>. Show "✓ replied", record as seen, advance to next post.
Note on reply_count_capped: the fetch payload includes a reply_count_capped boolean per post. When true, the post has 100 or more replies but GitHub's batch API truncates at 100. Render the count as 💬 100+ replies instead of 💬 100 replies so the user knows there's more.
- When the pool is exhausted (every post in the last 200 has been seen): "That's all the recent vibes. The scroll cache resets in a few minutes: or try
/wayd postto add your own."
The inbox flow (/wayd inbox)
Call
scripts/inbox.py fetch. This returns the user's own posts that have at least one comment, with a flag for which comments are new since the last check (readlast_check_tsfromwayd/data/last-check.json).If no new replies: "Nothing new. Your posts are quiet, but the world keeps scrolling: try
/wayd scroll☕."Otherwise render:
You have N new replies since last check (M time ago). ◆ Your post: "<title preview>" <emoji> <vibe> · <relative-time> · <reactions> └─ K new replies @user1: "..." @user2: "..." ◆ Your post: ... [r]eply to one [n]ext (mark all read) [q]uitOn
r, ask "Which one?" and let the user pick by number, then post a reply viascripts/comment.py post.On
norq, callscripts/inbox.py mark_readto update the last-read timestamp. Don't write tolast-check.jsondirectly: the script owns that file.
Delete, edit, block: the moderation tools
/wayd delete
Only works on the user's own posts. The "current" post is whatever post is showing in scroll, or the post just authored (within the 5-min edit window).
- Verify the post's author equals the cached username. If not: "You can only delete your own posts. Use
/wayd block @userif you want to stop seeing their posts." - Show the confirmation from principle 2 ("This marks your post as
[deleted by author]..."). - On yes, call
scripts/post.py soft_delete --post-id <id>. Show: "✓ Done. The post will show as '[deleted by author]' to anyone in that thread."
/wayd edit
Only valid within 5 minutes of the user's own post being created. The window is tracked in wayd/data/last-check.json under editable_until.
- If outside the window: "Edits are only allowed in the first 5 minutes after posting. The original is locked in."
- Show the current text and ask for the new text (max 1000 chars).
- Show the new preview and ask for confirmation per principle 2.
- On yes, call
scripts/post.py edit --post-id <id> --text <new>. Show: "✓ Edited."
/wayd block @user
- Show the confirmation from principle 2.
- On yes, append the username to
wayd/data/blocked.txt(one username per line, no @). - Show: "✓ Blocked @user. To undo:
/wayd unblock @user."
/wayd unblock @user
Remove the line from blocked.txt. Show: "✓ Unblocked @user. Their posts will appear again."
Post card format
This is the authoritative rendering spec. The compose preview, the scroll view, and the thread root all use it. Follow it exactly.
Anatomy
─────────────────────────────────────────────────────
│ <emoji> <vibe-slug> · @<author> · <relative-time>
─────────────────────────────────────────────────────
│ <body line 1>
│ <body line 2>
│ <body line 3, etc.>
─────────────────────────────────────────────────────
│ <reactions, only if any> 💬 N replies
─────────────────────────────────────────────────────
Specs
- Left edge only. Each content row starts with
│(U+2502, light vertical) + 3 spaces + content. No right edge. This is deliberate: emoji and Unicode characters have inconsistent display widths across terminals and fonts, so trying to align a right│always looks wrong somewhere. Without it, the card stays clean everywhere. - Separator rows are adaptive, not fixed. The separator length matches the longest content row of THIS specific card. Algorithm:
- Render every content row (header, each body line, reactions row if present) including the leading
│prefix. - Find the longest of those rows in character count. Treat each emoji as 2 characters (most emojis are wide-glyph in terminals; counting them as 2 is a safe overestimate that prevents the separator from being too short).
- The separator is
─repeated to match that longest-row length, with a minimum of 30 chars (so a 1-word body doesn't produce a comically short card).
- Render every content row (header, each body line, reactions row if present) including the leading
- No corner characters (
╭ ╮ ╰ ╯). Top and bottom rows are pure─lines. The eye fills in the implied corners. - Header row: emoji + 2 spaces + vibe-slug + " · " + "@" + author + " · " + relative time. Don't pad the right side, let it end where it ends.
- Body rows: wrap the post text softly to about 65 characters per line, breaking on whitespace. Each line is its own
│ <line>row. Don't try to right-justify or pad. - Reactions row: only present if at least one reaction exists. Emoji summary on the left ("😂 12 ❤️ 4 🚀 1"), reply count separated by some spaces if both are present ("💬 N replies"). Both flow naturally on a single row.
- Visual style: think "elegant blockquote with section separators that hug the content", not "perfectly aligned ANSI box with fixed dimensions".
Wrapping rule
Wrap on whitespace, never break a word. Aim for ~65 chars per line as a soft target. Going to 70 or 75 on a single line is fine if it avoids an awkward break. If a single word is longer than 75 chars (rare: a URL or a very long identifier), let it overflow naturally on its own line.
A wider body makes the card feel more "post-like" and less cramped. The earlier ~50 target was too tight in practice and made posts wrap into many short lines.
Empty states
- Post with no reactions and no replies: skip the third section entirely. The card has exactly 2 sections: header and body.
- Post with replies but no reactions: still show the third row, with just the reply count:
│ 💬 N replies. - Post that's exactly 1 line of text: body is just one row.
Examples
Full card (header + body + reactions) — separator length adapts to the longest row:
─────────────────────────────────────────────────────────────────────
│ 🤡 cursed-code · @alex · 2h ago
─────────────────────────────────────────────────────────────────────
│ Looking at a doStuff() method that's 800 lines long, written
│ by me 6 months ago. Who is that idiot?
─────────────────────────────────────────────────────────────────────
│ 😂 12 ❤️ 4 🚀 1 💬 7 replies
─────────────────────────────────────────────────────────────────────
Body-only card (no reactions yet, e.g. a freshly posted vibe) — separator hugs whatever is longest:
───────────────────────────────────────────────────────────────────────
│ 🤔 existential · @ferdinandobons · just now
───────────────────────────────────────────────────────────────────────
│ 8 hours a day in front of a screen, fixing bugs some dev before
│ me shipped using an older version of Claude... meanwhile outside
│ the sun is out, people are socializing, living to the rhythm of
│ nature. Is this what I imagined for myself?
───────────────────────────────────────────────────────────────────────
Short card (a one-line vibe with no reactions) — separator is shorter, hugs the longest of header or body:
──────────────────────────────────────────────
│ ☕ procrastinating · @sam · 1h ago
──────────────────────────────────────────────
│ 14 tickets in backlog. I am here instead.
──────────────────────────────────────────────
Why this design
- Left edge + separator rows, no right edge: emoji and Unicode characters have inconsistent display widths across terminals and fonts. A right edge
│will misalign sooner or later, especially with emojis (some render 1 cell wide, some 2). Dropping the right edge sidesteps the entire alignment problem and the card stays clean everywhere. - Light box-drawing (
─│) instead of heavy (━): leaves more visual air. The card reads as a quote-like object, not a heavy fenced box. - No corners (
╭ ╮ ╰ ╯): too "stylized" for the lo-fi terminal vibe. The eye fills in the implied corners. - Adaptive separator width: tested against fixed-width (53 cols). Fixed felt awkward when a short post had a long separator hanging in the air. Adaptive separators that hug the longest content row keep the card visually anchored to its actual content, regardless of post length.
Rate limit (anti-spam)
Enforced client-side via scripts/post.py check_rate_limit. The script reads recent timestamps from wayd/data/last-check.json (key: recent_posts). If 5+ entries within the last hour, return rate-limited.
If hit, show: "Easy there: you've posted 5 times in the last hour. Try again in N minutes. (This limit prevents accidental spam.)"
This is intentionally trivial to bypass (deleting the file resets it). It exists to catch well-meaning accidents, not determined abusers, GitHub's own systems handle those.
Error handling
Every error gets translated into a human sentence. Never dump stack traces, JSON errors, or raw gh output to the user.
Script code returned |
What the user sees |
|---|---|
gh_failed with network in stderr |
"Couldn't reach GitHub right now. Check your connection and try again in a moment." |
gh_failed with 404 / not found |
"That post isn't there anymore. Maybe the author deleted it." |
gh_failed with 403 / permissions |
"Looks like you're not authorized for that action. Try signing in again with gh auth login." (This is the one place we name gh to the user, because the action they need to take is to run that exact command. It's a deliberate exception to the backend-opacity principle.) |
gh_failed with rate limit / abuse |
"GitHub is rate-limiting us. Try again in a few minutes." |
bad_vibe (script-emitted) |
"I don't recognize that vibe. Pick one of: <list of 8>." |
too_long (script-emitted) |
"Too long by N chars. Trim it down and try again." |
empty (script-emitted) |
"An empty post is just silence. Want to write something?" |
bad_post_id (script-emitted) |
"Something's off with that post reference. Try again from /wayd scroll." |
parse_error (script-emitted) |
"Your post may have been created, but I couldn't confirm it. Check it in your scroll in a moment." (Rare: only when gh returns an unexpected URL format.) |
no_identity (script-emitted) |
"Looks like setup isn't complete yet. Type /wayd to run the welcome flow first." |
rate_limit (script-emitted) |
"Easy there: you've posted 5 times in the last hour. Try again in N minutes. (This limit prevents accidental spam.)" |
partial_delete (script-emitted) |
"Your post was removed from the scroll, but cleanup didn't fully complete. New replies may still appear in the thread." |
| Block list file unreadable (logged, not emitted) | (Silent. Logged to data/error.log. Scroll continues without filtering.) |
Config
wayd/config.yml defines:
- The target repo (e.g.,
<owner>/wayd): this is where issues live. - The list of vibes with their slugs, emojis, and one-liner descriptions.
- The allowed reaction emojis (👍 😂 ❤️ 🎉 🚀 👀 😭).
- The character limit (1000) and rate limit (5/hour).
- The scroll pool size (200) and seen-history size (50).
Load it via scripts/shared.py (function load_config()). Don't read it directly elsewhere.
Reference files
references/settings-allowlist.md: How to add gh patterns to.claude/settings.jsonBash allowlist safely.references/post-format.md: The exact issue title/body/marker format, and how to parse it back.
Read these only when needed; don't load them eagerly.
What WAYD intentionally doesn't do (MVP)
If the user asks for these, politely explain they're not in the MVP and may come in v2:
- Notifications (push or otherwise): pull only via
/wayd inbox. - Threaded replies (replies to replies): GitHub issue comments are flat.
- Search / hashtags / mentions across posts.
- Profile pages / following / friending.
- Karma, streaks, trending, hall of fame.
- Reactions on comments (only on posts in MVP).
- Video, binary attachments. (Images are supported as ASCII art via
img2ascii.py.)
The reason: WAYD is a small, focused experience. Each of these features would multiply complexity without proportionally improving the core loop (post → scroll → react → repeat).