Design

Principle: do everything deterministic in Python, send a compact digest, let the model only phrase it. Structured JSON comes back, and formatters render it, so layout and numbers never depend on the model.

Last working day

last_working_day(today, holidays) steps back one day at a time until the day is Mon-Fri and not a holiday. Monday 2026-10-19 gives Friday 2026-10-16. With 2026-10-16 as a holiday it gives Thursday 2026-10-15. For week, the window starts on the Monday of the current week; on a Monday it is the week just ended.

Collecting commits

One git log --all --source --numstat -i -F --author=<email>... --since=<date> per repo. --source reports the ref each commit was reached from, which gives the branch name without a per-branch walk. Records are split with ASCII separators (0x1e/0x1f) so subjects cannot break parsing. Stash refs are ignored. Emails are the union of the global identity, configured aliases and the repo's local user.email, so work under a different identity in one repo is still found.

Merges

A commit is treated as a merged PR when its subject is Merge pull request #N from user/branch (label PR #N (branch)), a merge commit Merge branch 'x', or a single-parent commit ending in (#N) (squash merge). Merge commits are reported but not clustered. Merged branch names are remembered so they are not suggested for "today".

Clustering and WIP collapse

Key = (repo, ticket) when a ticket regex matches the subject or the branch name, otherwise (repo, branch). So commits for PAY-142 made on main and on feature/PAY-142-refunds land in one cluster, and the cluster's branch becomes the non-trunk one.

Example:

PAY-142 add refund model            feature/PAY-142-refunds
wip                                 feature/PAY-142-refunds
PAY-142 refund endpoint             feature/PAY-142-refunds
fixup! PAY-142 refund endpoint      feature/PAY-142-refunds
=> 1 cluster, 4 commits, 2 wip/fixup collapsed, 2 subjects

WIP means subjects starting with wip, fixup!, squash!, amend!, tmp, temp, checkpoint, savepoint, only dots, or fix typo. They count toward totals and file lists but their subjects are dropped. Duplicate subjects are shown once.

TODO diff

Items are - [ ] text / - [x] text (also *, +, nested). The snapshot maps text to checked state. Completed = checked now and not checked in the snapshot (including items new and already done). Added = unchecked now and absent before. The first run has no snapshot, so nothing is "completed". The snapshot is stored under STANDUP_STATE_DIR, keyed by the hash of the TODO path, and only written after a successful non-dry run.

Today's plan and blockers

Heuristics, then phrased by the model: clusters on non-trunk, unmerged branches ("continue X"), repos with uncommitted work, and up to eight unchecked TODO items. Blocker hints are unchecked TODO items matching blocked|waiting|stuck|depends on|on hold. If none, the model is told to return an empty list and the formatter prints "None".

Model call

claude-haiku-5-5, output_config={"effort": "low", "format": json_schema}, max_tokens 2048, no sampling parameters or thinking. The cache key is sha256 of (model, system, user, schema). Cost is input*$0.10/M + output*$0.50/M. Errors map to short messages (auth, rate limit with retry-after, bad request, 5xx, connection, truncation, refusal).