| name | check-follow-ups |
| description | Scan recent daily notes' Follow-ups sections and report what's still open, oldest first, leading with the ones for the repo you're in — plus anything ticked as dropped or handed off, which is closed without being finished. Read-only — walks back to the last real notes, so it survives a weekend, a holiday, or a vacation gap without missing anything, and it groups by repo rather than filtering, so nothing is hidden. Use when the user asks to check my tasks, check follow ups, what's pending, what do I still need to do, or any open items. |
Check follow-ups
Read-only. Reports what is still unchecked in recent daily notes'
## Follow-ups sections. Never writes a new practice note, never commits,
never pushes — that is update-second-brain's job, not this one.
Vault
- Path:
$SBW_VAULT if set, else ~/vaults/second-brain
- Daily notes: vault root,
YYYY-MM-DD.md (local date)
- Section read:
## Follow-ups, items as - [ ] (pending) / - [x] (closed)
Items wrap. An item is the - [ ] line plus every indented line under it,
joined — these are prose and routinely run to three or four lines. Reading only
the first line truncates the item and loses whatever it said about which repo
it belongs to.
Window
Notes back, not days back. List *.md files at the vault root matching
YYYY-MM-DD.md, sort descending, and take today's note (if it exists) plus
the next 3 that actually exist before it — however many calendar days
that spans. A day with no note (weekend, holiday, sick day, vacation) is
simply not in that list, so it costs nothing and is never mistaken for "no
follow-ups that day." This is what makes the window survive a two-week gap
the same way it survives a weekend, with no special-casing of either.
Stop searching backward after 90 days with fewer than 3 notes found — report
what you did find, and say plainly that daily notes thin out beyond that
point, rather than continuing to scan the whole vault.
Skip, without erroring, any note found before this section existed (no
## Follow-ups heading at all).
Widen the note count on request ("check my tasks going back further").
Threads
There is no automatic carry-forward, so a still-open item is rewritten into
today's note by hand — and reworded, because the writer knows more than they did
yesterday. One task therefore appears once per day it survived:
2026-08-08 Merge Flutter barcode PR #28 and ship TestFlight/store build
2026-08-10 Merge Flutter barcode PR #28 and ship TestFlight IPA (`1.1.0+24` on `feature/…`)
2026-08-11 Merge Flutter barcode PR #28 and ship TestFlight IPA (`1.1.0+25`)
Those are one thread, and the script reports them as one line:
- 2026-08-08 (4 days open): Merge Flutter barcode PR #28 and ship TestFlight IPA (`1.1.0+25`)
restated 08-10, 08-11 — newest wording shown
Read the two halves correctly when you summarise:
- the date and the age are the first mention's — that is the number worth
acting on, and reporting the newest restatement's age said "1 day" about a
task that had been sitting for four
- the text is the newest wording — it is what the task is now
A heading reading (3 threads, 6 items) means six lines in the notes, three
tasks. Say the thread count; the item count is on the line so nothing looks
quietly dropped.
If a thread was ticked off in a newer note while an older note still shows it
unchecked, it is closed and not listed, and a footer line says how many. Read
that line out — it is the only thing the report removes. --no-threads shows
every restatement separately.
Outcomes
A tick says an item left the list. It does not say how, and the four ways lead
to opposite actions when the question comes back:
| Tag | Means | This report |
|---|
#outcome/done | finished; you can cite it | closed, not listed |
#outcome/superseded | replaced by something else, which is its own item | closed, not listed |
#outcome/dropped | nobody is doing it, and it was not done | listed, as unresolved risk |
#outcome/handed-off | somebody else's now — #owner/<name> says who | listed, with the owner |
The last two go in a block of their own, above the groups: Closed without
being finished. They are not open work and they are not finished work, and
putting them in either list misreports them — mixed into the open items they
read as things still being carried, hidden with the ticks they read as done.
#outcome/handed-off with no #owner/ renders as "handed off, no owner
recorded", which is a real finding: an item with no name against it is one
nobody is watching.
A bare - [x] closes, exactly as it always did. Every note written before
this convention is full of them, and reopening those would re-raise years of
finished work on the strength of a missing tag. Once a window contains at least
one #outcome/ tag, a footer says how many of its ticks carry none — a count,
never a list, and never a nag about history.
The tags are written on the write side, by update-second-brain, at the moment
the item is closed and the reason is still known. Do not add them here to items
that already exist.
Repo
One day's follow-ups routinely span several repos — a backend, an ingestion
service, an ops task, a decision about the vault itself. Twenty items in one
undifferentiated list, when three of them are about the repo the user is
standing in, reads as noise and gets skimmed.
So when invoked from inside a git repo, the default is this repo in full, every
other repo as a count:
- This repo — every item, oldest first, in full
- Elsewhere — one line:
acme-ingestion 3 · globex-web 3 · no repo identified 4. Not one line per item: one line total.
- Expand any of those only when asked ("what's in the others?", "show
everything").
Two things survive the collapsing, listed in full above your repo's items
whatever repo they belong to, because their urgency is not about where you are
standing:
- a blocker — the note calls it blocking, or a pause point
- a live credential — a key to rotate or revoke, a secret pasted somewhere
Ask about this repo's items when you offer to tick anything off. The reader
asked from a repo; a closing question spanning four repos' worth of items hands
back the exact undifferentiated list the grouping just removed. Offer the rest
separately, in one sentence.
Group, never filter. Every item in the window appears exactly once, and the
total is stated before any grouping. This is not a style preference: an item's
repo is metadata about the item, attribution is best-effort, and the items
with no repo to infer — an email awaiting a reply, a key to revoke, a decision
to make — are the ones that rot longest. Hiding them would make this skill's
one job (nothing quietly falls off) fail precisely where it matters, and the
reader would have no way to know the count was ever higher.
Determine the current repo from git remote get-url origin's last path segment,
falling back to the checkout directory's name — origin first, because that is
the name the vault records and a checkout is often cloned into a differently
named directory. Not in a repo, or the repo is unrecognisable? Skip grouping
entirely, say why in one line, and report as below.
Attribute each item by, strongest signal first:
- a trailing
#repo/<name> tag — recorded by update-second-brain, which knew
the repo because it was running in it. Trust it even if the name is one the
vault has never mentioned: that means a new repo, not a typo.
- a repo name in the item's own text, matched against names the vault already
uses — practice notes'
repos: frontmatter, plus #repo/ tags already
written. A closed vocabulary is what stops hyphenated prose from reading as
a repo name.
- a backticked file path that exists in the current repo — confirms "this
repo", can never name someone else's
- the single repo the note's own
## Built section is about, when it names
exactly one — including the ## Built (<repo>: …) label, which is the
most deliberate statement of a repo in the note and the thing to read first.
Weakest signal overall, and say so when it is what you used ("from the
note's context, not the item"). A day that touched three repos is exactly the
day this guesses wrong, so when the labels disagree, don't pick one.
None of those hit? It goes under No repo identified — that is a real answer,
not a failure.
Landed evidence
An item naming a pull request, a branch, or a commit is checkable, and the
script checks it against that repo's main branch — gh for a PR's real state,
local origin/main ancestry for a branch or a SHA. Items naming none of those
cost nothing and are annotated with nothing.
| Marker | Means |
|---|
[landed] | merged — the work is on main |
[closed] | the PR was closed without merging, so the task is still real and the thing you remember doing about it was thrown away |
[open] | genuinely still open. This is a confirmation, not a nag |
[unchecked] | could not be established, with the reason on the line |
Only this repo's items are checked by default. Items in other repos are
reported as normal but not probed, and a footer says how many carried a
checkable ref and were skipped. --landed-all widens it — worth doing when the
question is "has any of this already been done", since work landing in a repo
you are not standing in is exactly what you would otherwise miss.
[landed] and [closed] threads are lifted into a Looks already done block
above everything else.
That block is a question, never an action. Never tick an item because the
report says it landed. The evidence is about the ref, and the item usually
says more than the ref does — "Merge PR #28 and ship a TestFlight build" is
half done when the PR merges. Read the block out, say what the evidence is, and
tick only what the user confirms, via step 6 below.
Never hide an [unchecked]. "No checkout of foo found under SBW_SCAN_ROOTS"
is a fact about this machine the user can fix in one line; swallowing it turns a
fixable gap into an item that silently never gets checked.
The check never runs git fetch, so a branch or commit verdict is only as
current as that checkout's last fetch — which is why those lines carry a
(last fetched 2d ago) note. Past a week it stops asserting altogether and
reports [unchecked] … too stale to judge, because a stale answer is a false
negative: "not merged" about work that landed a fortnight ago, said with the
same confidence as a true one. Fix it by fetching that repo, then re-running. A
PR verdict is live and never goes stale.
Run the script rather than re-implementing this
All of the above is already implemented. This skill directory contains only
SKILL.md — the script lives in the engine checkout, not next to this file, so
a relative scripts/... path will not resolve:
~/second-brain-workflow/scripts/check-followups.py --recent --brief
~/second-brain-workflow/scripts/check-followups.py --recent
~/second-brain-workflow/scripts/check-followups.py --recent --repo NAME
~/second-brain-workflow/scripts/check-followups.py --recent 8
~/second-brain-workflow/scripts/check-followups.py --recent --no-threads
~/second-brain-workflow/scripts/check-followups.py --recent --no-landed
~/second-brain-workflow/scripts/check-followups.py --recent --landed-all
--recent already implies both threading and the landed check. Reach for
--no-threads when the user disputes a collapse and wants the raw items, and
for --no-landed when they want the answer immediately and the repo checks are
costing seconds they don't want to spend.
Run it with --brief first. It computes exactly the shape described above —
this repo in full, others tallied, flagged items lifted out — so the collapsing is
a command's output rather than a summarisation you perform, which is what kept
drifting back into thirteen fully-described items from three other repos. Drop
--brief when the user asks for everything.
--recent is this skill's window, implemented in the script rather than
described here: the 4 most recent notes that exist, today included, selected by
note count and never by age. Use it, not --stale-days — that flag is the
long-range audit's age cutoff, it reports items strictly older than its
argument, and so even --stale-days 0 silently drops today's note, which is
usually the one you most need. The output states the window and its real date
span, and says so when fewer notes exist than were asked for.
If that path doesn't exist, the engine is checked out somewhere else. Resolve it
from this skill's own install link rather than guessing:
ENGINE="$(cd "$(dirname "$(readlink ~/.claude/skills/check-follow-ups)")/../.." && pwd)"
"${ENGINE}/scripts/check-followups.py" --recent
Reading the notes by hand is the last resort, not the default. Prefer the script:
a hand count and the script disagreeing is a real failure mode — it happened, off
by two, because items had been added between the two readings. If you do fall
back, say so in the report, since the numbers are then yours rather than the
tool's.
What to do
-
Resolve the window per above; note the actual date span it covers (e.g.
"last note before today was 2026-07-18" — this is exactly what makes a
vacation-sized gap visible instead of silently swallowed).
-
For each note in the window with a ## Follow-ups section, collect the
- [ ] items (skip - [x]), joining each item's wrapped lines.
-
Report oldest first, grouped by repo per the section above — that
surfaces what's been sitting longest, in the repo the user is actually in.
Lead with the date span from step 1 and the total.
-
Mark a blocker in place; never list it twice. An item the note calls
blocking, or a live credential, gets a [blocked] / [credential] marker on
its own line where it already sits. It is lifted to the top only when its
group is collapsed and it would otherwise vanish into a count. A "blockers
first" section followed by the same items under their repos produces exactly
the 12. Same as (1) and (2), carried forward line that broke the
appears-exactly-once contract in practice.
-
If nothing is unchecked anywhere in the window, say so plainly rather than
printing an empty report.
-
If the user confirms an item is closed during the conversation, tick it
and record the outcome with it — #outcome/done, #outcome/dropped,
#outcome/superseded, or #outcome/handed-off plus #owner/<name>. A bare
tick is an incomplete write: it is the state that makes "finished" and
"abandoned" indistinguishable a month later. Ask which of the four it was
rather than assuming done — that is one short question, and a wrong
#outcome/done closes the item and asserts something false about it.
Write it through the appender, not by hand. Editing the note directly is
a read-modify-write on the one file two sessions write at once, and a
one-character edit loses another session's block exactly as a whole-file
write does:
A=~/second-brain-workflow/scripts/append-daily-block.py
STAMP="$("$A" --date 2026-08-28 --stamp --quiet)"
"$A" --date 2026-08-28 --expect \
--close \
--close
What this does not do
There is no automatic carry-forward. An item unchecked in a note outside the
window (older than the 3 found, or beyond the 90-day search cap) silently
drops out of the report. If something is still open, rewrite it into today's
## Follow-ups so it stays inside the window — don't rely on widening the
lookback indefinitely. Rewrite it freely: the rewrite is recognised as the
same thread and keeps its original age, so there is no reason to preserve
yesterday's wording for a task you now understand better.
It never ticks anything off by itself, whatever the repo says. A merged PR
is evidence, and step 6 is still the only write — after the user confirms, and
with the outcome the user names, not one inferred from the evidence.
It never filters by repo, and never hides an item it couldn't attribute.
Attribution is best-effort and "No repo identified" is a normal, populated
group, not a defect to work around — most items predate the #repo/ convention
and were written by a hand that knew the context without recording it. If a
repo's items should be easy to find, the fix is on the write side: tag them as
they are created (update-second-brain, Step 3), which also teaches this side
the repo's name.