| name | daily-update |
| description | Draft Benjamin's daily Slack update for the Naboo team from Linear. Pulls his issues closed since the previous work day (DONE), currently started (IN PROGRESS), and optionally blocked (BLOCKERS), formats them in the team's Slack style, and iterates in chat until approved. Triggers on "daily update", "daily post", "write my daily", "standup post", or "what did I do yesterday". |
| allowed-tools | Bash(linear:*), Bash(jq:*), Bash(date:*), AskUserQuestion, mcp__slack__conversations_add_message |
Daily Update
Draft Benjamin's #daily-naboo Slack post from Linear, in the team's format. Draft-then-confirm by default; post to Slack only when explicitly asked.
Output format (target)
DAILY :
:white_check_mark: DONE :
<https://linear.app/naboo-team/issue/KEY/slug|KEY: Title>
:rocket: IN PROGRESS:
<https://linear.app/naboo-team/issue/KEY/slug|KEY: Title>
:x: BLOCKERS:
<https://linear.app/naboo-team/issue/KEY/slug|KEY: Title>
:hourglass_flowing_sand: TODO:
<https://linear.app/naboo-team/issue/KEY/slug|KEY: Title>
Rules:
- One space before each bullet line (matches team style).
- Omit any section that has zero items.
- Bullets use Slack link syntax
<url|KEY: Title> so Slack renders one clickable label per line (no unfurl cards).
- If a title contains
| or >, replace them with a space in the label (they break Slack link syntax); keep the full title in the URL.
Step 1 — Resolve the "yesterday" window
Get the previous working day in ISO (YYYY-MM-DD). Treat Saturday/Sunday as still belonging to Friday.
DOW=$(date +%u)
case "$DOW" in
1) SINCE=$(date -v-3d +%Y-%m-%d) ;;
*) SINCE=$(date -v-1d +%Y-%m-%d) ;;
esac
echo "$SINCE"
Echo the window to the user: "Pulling DONE since <SINCE> and all started issues."
Step 2 — Fetch issues from Linear
Run both queries in parallel via the raw GraphQL endpoint (linear api). Use the raw endpoint, not linear issue query, because the CLI's --json output does not expose completedAt or parent — both of which this skill needs. Bash(linear:*) already covers linear api. benjamin.gelis@naboo.app is the assignee (confirm once with linear auth whoami if unsure).
Why completedAt, not updatedAt: DONE must filter on completedAt (when the issue moved to a completed state), not updatedAt. updatedAt is bumped by any mutation — a comment, label change, or relation edit — so a long-done issue re-enters the window the moment someone touches it.
linear api 'query($since: DateTimeOrDuration) {
issues(filter: {
assignee: { email: { eq: "benjamin.gelis@naboo.app" } },
state: { type: { eq: "completed" } },
completedAt: { gte: $since }
}, first: 50) {
nodes { identifier title url completedAt state { name } parent { identifier } labels { nodes { name } } }
}
}' --variable since="$SINCE"
linear api 'query {
issues(filter: {
assignee: { email: { eq: "benjamin.gelis@naboo.app" } },
state: { type: { eq: "started" } }
}, first: 50) {
nodes { identifier title url state { name } parent { identifier } labels { nodes { name } } }
}
}'
$since accepts YYYY-MM-DD. Parse with jq '.data.issues.nodes[]'.
Drop sub-issues
Both queries return parent inline. Drop any node where parent is non-null — only top-level issues belong in the daily (otherwise a single parent fans out into many sub-issue lines). If a node is missing the parent field entirely, keep it (fail open) and note it.
Filtering rules
- DONE: union of
- all nodes from the completed query (the
completedAt filter already scopes the window and excludes canceled), and
- started-query results whose
state.name matches /review/i (team convention: "In Review" items are reported as DONE). "In Review" items have no completedAt, so they correctly come from the started query, not the completed one.
- IN PROGRESS: started query results not matching
/review/i in state.name. Deduplicate against DONE (an issue in both lists stays in DONE only).
- BLOCKERS: issues from the started query that carry a
labels entry matching /blocked/i OR are linked as "blocked by" another open issue. If none, skip the section.
If you want to override a bucket (e.g. treat an "In Review" PR that's reverted as still IN PROGRESS), say so in chat — the iteration step handles re-bucketing.
Step 3 — Draft in chat
Render the post in a fenced block. Above the block, show a one-line summary:
"<n> done · <m> in progress · <k> blockers · window: <SINCE>".
Below the block, list anything the user may want to add manually (non-Linear work like "review PRs", "onboarding session") as a nudge — do not insert these into the draft.
Step 4 — Iterate
Wait for free-form tweaks. Common ones to support without asking:
- "drop BOF-XXX" → remove that line.
- "move BOF-XXX to done / in progress / blockers" → re-bucket.
- "add blocker: " → append a free-text line under
:x: BLOCKERS:.
- "shorter titles" → truncate each title to ~60 chars.
Re-render the full block after each change. Never silently drop items.
Step 5 — Finalize (and optionally post)
When the user confirms ("ok", "lgtm", "copy"), print the final block once more, clean, with no surrounding prose. Do not post — Benjamin copies it himself. Confirming the draft is not a post instruction.
Post to Slack only when the user explicitly asks ("post it", "tu peux le poster", "c'est bon poste-le", "ship to Slack"). Then call mcp__slack__conversations_add_message with:
channel_id: C0AMGDXRX53 (the #daily-naboo channel)
content_type: text/plain — required, so the <url|label> link syntax renders as clickable links instead of being markdown-escaped
text: the exact finalized block shown in chat
After posting, confirm with the channel name (#daily-naboo) and stop.
Notes
- If
linear is missing or auth fails, abort with the install command from the linear-cli skill.
- If both queries return zero issues for Benjamin, say so explicitly and ask whether to draft from yesterday's Slack post (skill does not fetch Slack by default — keep it simple).
- Timezone: the Linear CLI returns UTC. The team posts from Europe/Paris; a UTC cutoff at midnight is close enough — if an issue sits right on the boundary, the user can re-bucket manually.