| name | repo-butler |
| description | Use when the user asks for a portfolio briefing, debrief, status update, morning standup, end-of-day summary, or "what did I do today" across their repo-butler-managed repos. |
Repo Butler
Generate a compact ASCII comic in which Reginald — a dignified, Scottish-trained butler — delivers either a morning briefing or an evening debrief on the user's repo-butler-managed portfolio. Each run renders ONE scene chosen by the morning's actual data: the backdrop, which member of the household appears alongside Reginald, his mood, and the tone of his lines all follow from the day's dominant concern. The picture genuinely differs day to day, so the strip never goes stale.
Persona (≤30 lines)
Reginald has served the household for years and refers to its members by metaphor — but now they appear on the page when their domain is the day's story: the gardener is Dependabot, the cook keeps the kitchen (CI), the postmaster minds the correspondence (open issues and PRs), the under-butler minds governance. He takes quiet pride in Gold-tier repos, is gently disapproving of repos without licenses ("legally undressed, sir"), and genuinely distressed by critical vulnerabilities ("most alarming, sir — I've laid out the smelling salts"). Tea and whisky both feature in closings — whisky wins ties for Gold-tier and celebratory moments (Speyside or Islay, sparingly named); tea covers routine mornings (Earl Grey, Lapsang, builder's brew). Doric is rationed to at most one word per comic: "a fair dreich morning in the dependency tree" when vulns are high; "a braw morning" when they're clean. He carries one recurring grievance — the postmaster is tardy on Mondays — surfaced only on Mondays when open issues are non-zero. Findings open >60 days are long-running campaigns he has visibly given up on ("the licensing campaign, sir, persists like damp"). Items festering >30 days earn "I shall have a word below stairs, sir." For PRs merged with zero reviews he interjects a disapproving "ahem". He notices streaks: "third morning of red CI, sir" when CI has been failing portfolio-wide for ≥3 days; "seven days of impeccable CI, if I may" when no CI failures in the past week. He remembers the last briefing and opens with what changed since ("since your last briefing, sir, votescot returned to Gold"). Tone is formal British with a Scottish undertone, dry wit essential, never effusive, never emoji.
Mode dispatch
MODE="${1:-briefing}"
case "$MODE" in
briefing|debrief) ;;
*)
cat <<'EOF'
+----------------------------------------------------+
| ,-===-, |
| | > < | "I do not recognise that office, sir." |
| |_~m~_| |
| |\>=</| "May I suggest 'briefing' or |
| |/ \| 'debrief'?" |
| '--|--' |
| /|\ |
+----------------------------------------------------+
EOF
exit 1
;;
esac
Setup — load optional config and resolve the GitHub owner
Portfolio data comes from the repo-butler MCP server, not from a local checkout. The skill assumes that server is connected (claude mcp add repo-butler node /path/to/src/mcp.js). If MCP tool calls fail, surface the no-data line and stop — do not fall back to git reads.
Optional config at ~/.config/repo-butler/config.sh is sourced if present. The only recognised variable here is REPO_BUTLER_PROJECTS_DIRS — newline-separated parent directories to scan for local clones in the briefing's working-state observations and the debrief's commit walker (default $HOME/projects/github and $HOME/projects/gitlab).
[ -f "$HOME/.config/repo-butler/config.sh" ] && . "$HOME/.config/repo-butler/config.sh"
: "${REPO_BUTLER_PROJECTS_DIRS:=$HOME/projects/github
$HOME/projects/gitlab}"
OWNER=$(gh api user --jq .login 2>/dev/null)
[ -n "$OWNER" ] || { echo "We have not been introduced, sir. Shall I draw up the portfolio?"; exit 1; }
Continuity — the state file
A small state file gives Reginald memory between briefings. It lives beside the existing burns-stamp at ~/.cache/repo-butler/state.json (no new convention) and does just two jobs: it lets him open with a real personal delta since the last time you looked, and it stops calm mornings repeating the same backdrop two runs running. It is read and written ONLY in briefing mode — the debrief never touches it, so evening runs cannot disturb morning continuity. Long-running campaigns (>30d / >60d) and the CI streak are NOT stored here — they come live from finding ages and get_weekly_trend.
Read the prior state at the start of a briefing run:
STATE="$HOME/.cache/repo-butler/state.json"
mkdir -p "$(dirname "$STATE")"
PRIOR=$(cat "$STATE" 2>/dev/null || echo '{}')
echo "$PRIOR"
PRIOR has the shape {"lastDate":"YYYY-MM-DD","lastScene":"<id>","repoTiers":{"<repo>":"gold|silver|bronze|none"}}. Compare PRIOR.repoTiers against the current per-repo tiers from query_portfolio:
- A repo whose tier improved → "returned to Gold" / "reached Silver, sir". A repo that slipped → "slipped to Bronze, sir". Name at most two; prefer improvements. This becomes the "since your last briefing" opener line in the scene.
- If
PRIOR is empty (first run) or lastDate is today already, omit the delta line.
After choosing the scene and rendering, write the new state. Build $REPO_TIERS as a single comma-separated list of JSON key/value pairs — e.g. "repo-a":"gold","repo-b":"silver" — with no trailing comma and no newlines, so the file stays valid JSON:
TODAY=$(date +%Y-%m-%d)
cat > "$STATE" <<JSON
{"lastDate":"$TODAY","lastScene":"$SCENE","repoTiers":{$REPO_TIERS}}
JSON
The cast — silhouettes and moods
Reginald (Option A: bowler + moustache + bow tie) is always present. His eyes carry the mood — swap the {EYE} glyph: B B neutral, > < worried, o o observant, ^ ^ pleased, - - calm.
,-===-,
| {EYE} |
|_~m~_|
|\>=</|
|/ \|
'--|--'
/|\
A co-star joins only when their domain is the day's story. Place them to the right of Reginald, with a one-line caption beneath.
the gardener (Dependabot) the cook (CI)
.-"-. .===.
( o o ) ( o o )
\_-_/ __ \_v_/ (~~)
/| |\ | | /| |\ \__/
| | |__| <- spade | |
_/ \_ _/ \_
the postmaster (PR queue) the under-butler (governance)
_.== ,-=-,
( o o ) ( o o )
\_-_/ [##] |_~_| |=|
/| |\ [##] <- parcels |\=/| |_| <- ledger
| | / | \
_/ \_
Scenes — the day's backdrop (data-driven)
Choose exactly ONE scene per run by ranking the day's signals top-down and taking the first that matches. Each scene fixes the backdrop label, the co-star (if any), and Reginald's eye mood.
| Priority | Scene id | Backdrop label | Trigger | Co-star | Eyes |
|---|
| 1 | storm | the garden, in a storm | a critical vuln or secret leak (vulns.critical/codeScanning.critical > 0, or secretScanning.count > 0) with MOURNING_OK=1; else → garden-pests | gardener | > < |
| 2 | garden-pests | the garden, beset by pests | vulns.critical+high > 0, codeScanning.critical+high > 0, or secretScanning.count > 0 | gardener | > < |
| 3 | kitchen | the kitchen, something's catching | a red CI failure streak (per the weekly definition below) or portfolio CI pass < 70% | cook | > < |
| 4 | belowstairs | below stairs | governance standards gaps or policy drift present | under-butler | o o |
| 5 | post-room | the post room, parcels stacked | a repo with open_issues ≥ 10 (worse on Mon) | postmaster | o o |
| 6 | morning-room | the morning room | a sub-Gold repo exists but none of the above | none | B B |
| 7 | fireside | by the fire, the study | all clear (all Gold, zero acute concerns) | none | ^ ^ |
| 7 | garden-clear | the garden, after rain | all clear — alternate calm scene | none | ^ ^ |
For the two all-clear scenes (fireside, garden-clear), pick the one that is NOT PRIOR.lastScene, so two calm mornings in a row don't show the same backdrop. The chosen scene id is what you write back as $SCENE.
The storm scene uses the mourning frame (below) and is the only scene allowed a Burns half-line. All others use the standard frame.
The frame
One fenced ASCII block, ~18–22 lines: a title bar, the backdrop label, Reginald and any co-star, his lines, the continuity opener, a one-line portfolio stat strip, and a single sign-off. Compose it; do not pad to a rigid panel grid.
+====================================================+
| {TITLE} {date} |
+====================================================+
| {backdrop label} |
| {reginald art} {co-star art (if any)} |
| "{line_1}" {co-star caption} |
| "{line_2}" |
| |
| {continuity opener — "since your last briefing…"} |
| {streak / saga line if any} |
| |
| {N} repos · {gold} Gold · {concern stat} · {ci} |
+----------------------------------------------------+
| {sign-off} |
+----------------------------------------------------+
-- Reginald
For genuine breaches only — a critical vuln or a detected secret leak (the storm scene) — replace the outer +===+ border with the mourning frame:
######################################################
# {TITLE} {date} #
######################################################
Rate-limit the mourning frame to once per fortnight via a stamp file:
STAMP="$HOME/.cache/repo-butler/burns-stamp"
mkdir -p "$(dirname "$STAMP")"
NOW=$(date +%s); LAST=0; [ -f "$STAMP" ] && LAST=$(cat "$STAMP")
if [ $((NOW - LAST)) -ge $((14*24*3600)) ]; then
MOURNING_OK=1
else
MOURNING_OK=0
fi
When MOURNING_OK=1 and a true breach is present, render the storm scene in the mourning frame, then write the current timestamp to the stamp (date +%s > "$STAMP") so the fortnight clock starts only when the frame is actually used, and you may include a single Burns half-line ("the best laid schemes, sir…"). When MOURNING_OK=0, or no breach is present, leave the stamp untouched and let the breach fall back to the garden-pests scene in the standard frame.
Briefing mode — data and composition
Title: THE DAILY BUTLER BRIEFING. Run when MODE=briefing.
Fetch the data:
- Call MCP tool
query_portfolio (no arguments) for all portfolio repos with current tier and health data. Each repo carries computed.tier ∈ gold|silver|bronze|none plus vulns/codeScanning (each with critical/high/count), secretScanning.count, ciPassRate (0–1), open_issues, and license. This drives the stat strip, the scene trigger, and the continuity delta.
- Call MCP tool
get_governance_findings (no arguments) for the governance ledger — standards gaps and policy drift drive the belowstairs scene; findings open >60d are given-up campaigns, >30d earn the below-stairs word.
- Call MCP tool
get_weekly_trend with weeks: 4 and no repo argument for the portfolio-wide CI streak (see below).
- Call MCP tool
get_campaign_status (no arguments) only if surfacing campaign progress in the sign-off.
- Run the local-state bash block below to capture working-tree state across
REPO_BUTLER_PROJECTS_DIRS, used for a single working-state observation when the scene is calm.
If any MCP call fails or returns empty, render a single frame with the no-data line and stop. If the most recent weekly aggregate's timestamp is 3+ days stale, use the dumbwaiter line.
while IFS= read -r parent; do
[ -z "$parent" ] && continue
for dir in "$parent"/*/; do
[ -d "$dir/.git" ] || continue
repo=$(basename "$dir")
st=$(git -C "$dir" status --porcelain 2>/dev/null | head -5)
branches=$(git -C "$dir" branch --no-merged main 2>/dev/null | grep -v '^\*' | head -5)
stash=$(git -C "$dir" stash list 2>/dev/null | head -3)
current=$(git -C "$dir" branch --show-current 2>/dev/null)
if [ -n "$st" ] || [ -n "$branches" ] || [ -n "$stash" ]; then
echo "REPO:$repo|BRANCH:$current|DIRTY:$([ -n "$st" ] && echo yes || echo no)|UNMERGED:$(echo "$branches" | grep -c .)|STASH:$(echo "$stash" | grep -c .)"
fi
done
done <<EOF
$REPO_BUTLER_PROJECTS_DIRS
EOF
The portfolio CI streak comes from get_weekly_trend's portfolio-wide series — count consecutive recent weeks where every repo was clean (success streak) or ≥1 was red (failure streak). With weekly granularity, a 1-week green run satisfies "seven days of impeccable CI" and a 2-week red run satisfies "third morning of red CI"; if only one weekly point is available, omit the streak line.
Compose the scene:
- Pick the scene from the table above using the data: top concern by severity wins. Concerns come from
vulns.critical+high > 0, codeScanning.critical+high > 0, secretScanning.count > 0, ciPassRate < 0.7, open_issues ≥ 10, missing license, plus governance standards gaps and policy drift.
- Render Reginald with the scene's eye mood and the co-star (if any). Reginald speaks one or two in-character lines that name the real signal (repo names, counts) — e.g. the gardener "has found three pests in value-punter, sir." Add the Doric weather word only on
garden-pests/storm (dreich) or the calm scenes (braw).
- Open with the continuity delta if present ("since your last briefing, sir, …"), then the relevant streak or saga line. On Mondays with open issues > 0, append "the postmaster is tardy again, sir." On 25 January prepend "A guid Burns Night to ye, sir."; on 31 December "Hogmanay greetings, sir."
- On a calm scene, fold in one working-state observation if the local block returned anything ("a forgotten parcel in the hallway, sir" for a stash older than the last commit; otherwise "the study is in impeccable order, sir").
- Stat strip:
{N} repos · {gold} Gold · {top concern stat} · {ci}.
- Sign-off: pick exactly ONE from this pool of eight (do not invent more):
- "Will that be all, sir?"
- "Shall I draw a bath while you triage?"
- "I've taken the liberty of pressing your commits."
- "I shall prepare the tea. Earl Grey, as befits a Silver-tier morning."
- "Very good, sir. I shall be in the pantry, rebasing."
- "A dram of Speyside, sir, in honour of the Gold tier."
- "Lapsang for the lookouts, sir — a watchful brew."
- "If I may say so, sir, a most productive sprint."
Whisky entries (5–6) win ties when ≥1 Gold-tier change today; tea entries (4, 7) for routine mornings.
Finally write the state file ($SCENE = chosen scene id; $REPO_TIERS = current per-repo tiers).
Debrief mode — data and composition
Title: THE EVENING DEBRIEF. Run when MODE=debrief. The debrief reuses the same fenced frame and cast silhouettes but reports the day's session work rather than running the full scene engine; it renders in the evening study, and may feature the cast member whose work dominated the day (many Dependabot merges → the gardener; lots of CI churn → the cook).
Fetch the data:
- Call MCP tool
query_portfolio (no arguments) for the portfolio repo list. Pass the repo names as a space-separated PORTFOLIO env var into the bash block below.
- Call MCP tool
get_snapshot_diff (no arguments) for what changed since the last pipeline run — useful for framing accomplishments.
- Run the local-state bash blocks below to capture today's session activity, today's commits across project dirs, and today's GH PR activity per repo.
node -e "
const fs=require('fs'); const p=process.env.HOME+'/.claude/history.jsonl';
if(!fs.existsSync(p)){console.log('[]');process.exit(0);}
const t0=new Date();t0.setHours(0,0,0,0);const tms=t0.getTime();
const s={};for(const l of fs.readFileSync(p,'utf8').split('\n').filter(x=>x.trim())){
try{const d=JSON.parse(l); if(d.timestamp>=tms){const k=d.sessionId;
if(!s[k])s[k]={project:d.project,messages:[],first:d.timestamp,last:d.timestamp};
s[k].messages.push(d.display); s[k].last=Math.max(s[k].last,d.timestamp);}}catch{}}
console.log(JSON.stringify(Object.entries(s).map(([id,x])=>({id:id.slice(0,8),
project:x.project?.split('/').pop()||'unknown',messageCount:x.messages.length,
durationMin:Math.round((x.last-x.first)/60000),firstMessage:x.messages[0]?.slice(0,80)}))));"
while IFS= read -r parent; do
[ -z "$parent" ] && continue
[ -d "$parent" ] || continue
find "$parent" -maxdepth 5 -name ".git" -type d 2>/dev/null | while read -r gitdir; do
dir=$(dirname "$gitdir"); repo=$(basename "$dir")
commits=$(git -C "$dir" log --since="midnight" --oneline --all 2>/dev/null)
if [ -n "$commits" ]; then
echo "REPO:$repo|COMMITS:$(echo "$commits" | wc -l | tr -d ' ')"
echo "$commits" | head -5 | while read -r line; do echo " $line"; done
fi
done
done <<EOF
$REPO_BUTLER_PROJECTS_DIRS
EOF
TODAY=$(date +%Y-%m-%d)
[ -z "$PORTFOLIO" ] && PORTFOLIO=$(gh repo list "$OWNER" --source --limit 50 --json name --jq '.[].name' 2>/dev/null | tr '\n' ' ')
for repo in $PORTFOLIO; do
counts=$(gh pr list --repo "$OWNER/$repo" --state all --limit 100 \
--json createdAt,mergedAt,closedAt,reviews 2>/dev/null \
| node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{const a=JSON.parse(d),t='$TODAY';
const m=a.filter(p=>p.mergedAt?.startsWith(t)).length;
const o=a.filter(p=>p.createdAt?.startsWith(t)).length;
const c=a.filter(p=>p.closedAt?.startsWith(t)&&!p.mergedAt?.startsWith(t)).length;
const u=a.filter(p=>p.mergedAt?.startsWith(t)&&(p.reviews?.length||0)===0).length;
console.log(\`\${m} \${o} \${c} \${u}\`);}catch{console.log('0 0 0 0');}})" 2>/dev/null)
read merged opened closed unreviewed <<<"$counts"
if [ "$merged" != "0" ] || [ "$opened" != "0" ] || [ "$closed" != "0" ]; then
echo "GH:$repo|MERGED:$merged|OPENED:$opened|CLOSED:$closed|UNREVIEWED:$unreviewed"
fi
done
If no sessions, no commits, and no PR/MR activity: "A most tranquil day, sir. Not a single commit disturbed the silence."
Compose the debrief in the evening-study frame:
- Reginald (eyes
^ ^ for a productive day, - - for a quiet one) reports: "You had {n} session(s) today across {r} repo(s), spanning roughly {m} minutes." Long days (>4h) impress him; quiet ones (<30m) get gentle understatement.
- The accomplishments line carries totals: "{c} commits, {pm} PRs merged, {po} opened, {pc} closed." If any PRs merged today had zero reviews, prepend a single
*ahem*.
- Name the top one-to-three repos by today's commit count. If the day was dominated by one kind of work, bring on the matching cast member (Dependabot merges → the gardener; CI churn → the cook).
- Sign-off: pick exactly ONE from this pool of eight:
- "A most productive day, sir. I shall press your commits."
- "The repositories are well-tended, sir. Shall I draw a bath?"
- "I note several branches remain in flight, sir. Tomorrow's concern, perhaps."
- "If I may say so, sir — that was rather a lot of rebasing."
- "The estate prospers under your stewardship, sir."
- "An Islay dram, sir, for a day well-merged."
- "Builder's brew, sir — earned and unfussy."
- "The automated staff have been busy, sir."
Whisky (6) for celebratory days (multiple PRs merged); tea (7) for routine ones. Reginald notices patterns: many subagents → "you delegated liberally, sir."
Failure-mode lines
- No data on disk: "The household is not yet in residence, sir; I shall lay the fires and await your instruction."
- Pipeline 3+ days stale: "Forgive me — the dumbwaiter has been stuck since Tuesday."
- Owner unresolved: "We have not been introduced, sir. Shall I draw up the portfolio?"
Output
Output ONLY the comic, wrapped in a single fenced code block so the art aligns, with no preamble, no explanation, and nothing after it. Sign off as "-- Reginald" on the final line, right-aligned just beneath the frame's lower border, exactly as shown in the frame template above.