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) # 1=Mon .. 7=Sun
case "$DOW" in
1) SINCE=$(date -v-3d +%Y-%m-%d) ;; # Mon → previous Fri
*) 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.
# DONE candidates: completed on/after SINCE (excludes canceled by construction —
# canceled issues carry canceledAt, not completedAt)
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"
# Currently started (IN PROGRESS + "In Review")
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
completedAtfilter already scopes the window and excludes canceled), and - started-query results whose
state.namematches/review/i(team convention: "In Review" items are reported as DONE). "In Review" items have nocompletedAt, so they correctly come from the started query, not the completed one.
- all nodes from the completed query (the
- IN PROGRESS: started query results not matching
/review/iinstate.name. Deduplicate against DONE (an issue in both lists stays in DONE only). - BLOCKERS: issues from the started query that carry a
labelsentry matching/blocked/iOR 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-naboochannel)content_type: text/plain— required, so the<url|label>link syntax renders as clickable links instead of being markdown-escapedtext:the exact finalized block shown in chat
After posting, confirm with the channel name (#daily-naboo) and stop.
Notes
- If
linearis missing or auth fails, abort with the install command from thelinear-cliskill. - 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.