Landscape — Morning Catch-up
Show a consolidated landscape of where you are and what to do next, pulling from multiple sources at once. Designed for the start of a work session (especially Monday mornings) to quickly orient.
Usage
/landscape # Full landscape
/landscape quick # Skip PR details (faster, Jira + working-copy + beads-if-present only)
/loop 30m /landscape quick # Unattended refresh through the day (Claude Code); pair with /watch-prs for CI
What It Shows
Separate blocks, rendered from broadest context to most immediate. Order matters — the last block is the most load-bearing for "what am I doing right now":
- 📋 Jira — tickets assigned to you, not Done (with sprint and latest discussion)
- 🔀 PRs — org-wide open PRs, recently closed, unresolved threads
- 🎯 Beads — in-progress and top ready beads in this repo (skipped if
bdnot installed) - 📍 Working copy — current branch and uncommitted/unpushed work; at a multi-repo workspace root this becomes one per-repo table over every member instead, since the root's own branch is never where the work is
- Next — single-sentence suggestion for the most load-bearing action
Each block is independent — if one source fails, the others still render.
Instructions
MUST re-fetch on every invocation. Each
/landscaperun MUST execute every fetch from scratch —date, the Jira MCP query, thegh-pr-list-*andgh-pr-details.shscripts,beads.sh, andworking-copy.sh. NEVER reuse output from a previous run in the same session and NEVER extrapolate timestamps. State changes (PR merges, new approvals, ticket transitions) happen between runs; reusing stale tables has caused real merges to be missed in/pr-statusand the same risk applies here.MUST use the dedicated helper scripts. Never construct ad-hoc
bd …orgit …shell pipelines for this skill. Specifically: do NOT chaincommand -v bdprobes withbd list … && …or… || bd list --readyinside a single Bash call. Always invoke~/.agents/skills/landscape/scripts/beads.shinstead — it handles probing, repo gating, and listing internally. Likewise, do NOT hand-walk sibling service repos with your ownfor … git -C …loop — always go through~/.agents/skills/wrap-up/scripts/multirepo.sh(§4b), which handles workspace detection and per-repo state. Inline chaining bypasses the per-script permission allowlist and produces noisy permission prompts.
Render the blocks in the order listed below. Some data fetches can run in parallel at the top.
0. Header
date '+%A %Y-%m-%d %H:%M'
Output:
## Landscape — {Weekday} {YYYY-MM-DD} {HH:MM}
If the weekday is Monday, add a subtitle: _Monday — extra catch-up across the weekend._
1. 📋 Jira — assigned to you
Query Jira for open tickets assigned to the current user, including sprint membership. The sprint field is the Jira Cloud default custom field customfield_10020. Use the MCP tool:
mcp__jira__jira_get
path: /rest/api/3/search/jql
queryParams:
jql: assignee = currentUser() AND statusCategory != Done ORDER BY cf[10020] ASC, priority DESC, updated DESC
fields: summary,status,priority,issuetype,updated,customfield_10020
maxResults: 20
jq: issues[*].{key: key, summary: fields.summary, type: fields.issuetype.name, status: fields.status.name, priority: fields.priority.name, updated: fields.updated, sprint: fields.customfield_10020}
The sprint field is an array of sprint objects. Extract the active sprint's name (first sprint where state == "active"), or the most recent if none are active. If the array is empty or null, show — (ticket not in a sprint — possibly backlog).
Sort the rows before rendering. JQL's cf[10020] sort is unreliable across sprint states, so re-sort client-side:
- Bucket by sprint state: active sprints first, then future sprints (by
startDateascending), then no-sprint / backlog last. - Within each bucket, sort by priority
P1 → P2 → P3 → P4 → P5(treat missing/unknown priority as lowest). - Within equal priority, preserve the JQL
updated DESCorder.
Keep it as a single table — the Sprint column makes the group boundary visible without subheadings.
Render:
### 📋 Jira — assigned to you
| Key | Sprint | Type | Pri | Status | Updated | Discussion | Summary |
|-----|--------|------|-----|--------|---------|------------|---------|
| [AB-649](https://.../browse/AB-649) | Sprint 42 | Task | P1 | In Progress | 2h | 💬 3 · Jane · 40m | Stabilise identity cookies |
- Key: markdown link to the Jira issue. Use the Jira base URL from the issue's
selffield, or a site-configured base (e.g.https://yourorg.atlassian.net/browse/{key}). - Sprint: active sprint name. Truncate numeric-only names to
S{N}if the column gets wide.—if none. - Type: issuetype name (Task / Story / Bug / Sub-task).
- Pri: shorten long names —
P1 Critical→P1,P2 High→P2, etc. - Status: status name (In Progress / Code Review / Ready for QA / …).
- Updated: relative time since
updated(e.g.2h,4d). - Discussion: filled by the latest-discussion lookup below.
- Summary: truncate to ~50 chars.
If no tickets are assigned, show _No open Jira tickets assigned to you._
If the Jira API returns an error, show _Jira unavailable: {error}_ and move on — do not fail the whole skill.
After the table, note whether the tickets span one sprint or multiple. Example: _All 6 in Sprint 42._ or _Spans 2 sprints: Sprint 42 (4), Sprint 43 (2)._ This answers "am I focused or scattered?" at a glance.
Latest Jira discussion
After the assigned-ticket search succeeds, fetch the newest comment for every returned ticket. These calls may run in parallel, but do not skip tickets just because their updated value is old:
mcp__jira__jira_get
path: /rest/api/3/issue/{key}/comment
queryParams:
orderBy: -created
maxResults: 1
jq: '{total: .total, latest: (.comments[0] // null | if . == null then null else {author: .author.displayName, accountId: .author.accountId, created: .created} end)}'
Add a Discussion column to the assigned-ticket table:
| Key | Sprint | Type | Pri | Status | Updated | Discussion | Summary |
|-----|--------|------|-----|--------|---------|------------|---------|
| [AB-649](https://.../browse/AB-649) | Sprint 42 | Task | P1 | In Progress | 2h | 💬 3 · Jane · 40m | Stabilise identity cookies |
- Discussion:
—whentotalis zero. Otherwise show💬 {total} · {latest author first name or @accountId} · {relative age}. Do not render comment bodies in this compact table. - If a per-ticket comment request fails, show
?for that row and append_Some Jira discussion could not be fetched._after the sprint note. A failed comment lookup must not hide the assigned-ticket table. - Keep the existing row sort; comments are awareness signals, not a reprioritisation rule.
2. 🔀 PRs — delegate to pr-status logic
If /landscape quick was invoked, skip this section entirely and add a one-line note: _PR section skipped (quick mode). Run /pr-status for full view._
Otherwise, follow the pr-status skill's instructions as-is (see ~/.agents/skills/pr-status/SKILL.md). Reuse its scripts directly — do NOT re-invoke the slash command:
- List open PRs org-wide via
gh-pr-list-open.sh - List recently closed via
gh-pr-list-closed.sh - Fetch details via
gh-pr-details.sh(grouped by owner/repo) - Render the same tables
Head this section ### 🔀 PRs instead of pr-status's own headings.
3. 🎯 Beads — in-progress + ready work
Run the beads.sh helper. It probes for bd, checks for .beads/ in the repo, and emits in-progress + ready listings as delimited sections. Do not call bd directly from this skill — always go through this script:
~/.agents/skills/landscape/scripts/beads.sh
Output sections (delimited by ---<NAME>---):
---STATUS---—OK,NO_BD(bd not installed), orNO_BEADS_IN_REPO(no.beads/here)---IN-PROGRESS---— output ofbd list --status=in_progress(only ifSTATUS=OK)---READY---— JSON array fromnext-bd --json(or plain text frombd list --readyfallback) (only ifSTATUS=OK)
If STATUS is NO_BD, render _Beads not installed — skipping._ and stop.
If STATUS is NO_BEADS_IN_REPO, render _No beads in this repo._ and stop.
Otherwise render the tables below.
In-progress beads
**In progress ({count})**
| ID | Pri | Type | Labels | Title |
|----|-----|------|--------|-------|
- Include a Labels column. Show
—if none. - If no in-progress beads: show
_No in-progress beads._
Ready beads — sorted by sprint
The ---READY--- section is a JSON array (from next-bd --json). Each item has id, title, priority, issue_type, and rank.
Step 1 — Extract Jira keys. For each bead, scan title for the first match of [A-Z]+-\d+. If no match, the bead has no Jira link.
Step 2 — Batch Jira sprint lookup. If any keys were found, call:
mcp__jira__jira_get
path: /rest/api/3/search/jql
queryParams:
jql: key in ({comma-separated keys})
fields: summary,status,issuetype,priority,customfield_10020
maxResults: 50
jq: issues[*].{key: key, status: fields.status.name, sprint: fields.customfield_10020}
For each ticket's sprint array, pick the active sprint (first with state=="active"). If none active, pick the earliest future sprint (lowest startDate with state=="future"). If neither, treat as no-sprint.
Step 3 — Sort and cap. Bucket order: active sprint(s) → future sprints (by startDate) → no-sprint (has Jira key) → no Jira link. Within each bucket preserve the rank order from next-bd. Cap the total at 5 beads. If more exist: _+{N} more — run /next sprint to see all._
Step 4 — Render. One table:
**Ready ({shown} of {total})**
| # | ID | Pri | Type | Jira | Sprint | Status | Title |
|---|----|-----|------|------|--------|--------|-------|
#is the picker index1-N.Jiracolumn: markdown link[KEY](https://yourorg.atlassian.net/browse/KEY). Show—if no Jira key.Sprintcolumn: number + state suffix only (31 (active),32 (future)) — strip the project prefix from sprint names like"PROJ Sprint 31". Show—if no sprint or no Jira.Statuscolumn: Jira status. Show—if no Jira.- If no ready beads at all:
_No ready beads. Run /triage to add work._ - If the
---READY---section is not valid JSON (fallback plain text): render it as-is, capped at 5 lines. - If the Jira call fails: render the table without Jira/Sprint/Status columns; add a footnote
_Jira unavailable._
4. 📍 Working copy
Rendered LAST because it's the most immediate context — the branch you're sitting on right now, what needs committing/pushing, and whether it's in sync.
Run the working-copy.sh helper, which emits delimited sections for branch, repo toplevel, dirty status, ahead/behind, last commit, and on-branch stash count:
~/.agents/skills/landscape/scripts/working-copy.sh
Output is grouped by ---SECTION--- markers. Parse and render from that.
Pick the mode first
This section has two shapes, chosen by comparing working-copy.sh's ---TOPLEVEL--- against multirepo.sh's ---ROOT--- (§4b runs in parallel; you have both before rendering):
| Condition | Mode |
|---|---|
MARKER != none and TOPLEVEL == ROOT |
Workspace root — one table over every member (§4a below). §4b does not render separately. |
| anything else (single repo, or cwd inside a member repo) | Single repo — the field table below, then §4b for the siblings. |
MARKER != none alone is not enough to pick the workspace shape. multirepo.sh resolves the workspace from inside a member repo too (its sibling .mgit.conf child-directory step), so a member repo also reports marker=mgit. Only TOPLEVEL == ROOT means "sitting at the root". Inside a member repo the field table is the most load-bearing block on the page — it's the feature branch you're on — so never collapse it there.
Mode: single repo (or inside a member repo)
Render:
### 📍 Working copy
| Field | Value |
|-------|-------|
| Branch | fix/AB-649-device-cookie-combined |
| Dirty | clean _(or: 3 modified, 1 untracked)_ |
| vs upstream | ✅ in sync _(or: ⬆ 2 ahead, ⬇ 1 behind)_ |
| Last commit | `abc1234` commit subject (2h ago) |
Notes:
- If
@{u}fails (no upstream), showno upstream tracking. - If output is empty for dirty, show
clean.
Mode: workspace root (§4a)
At the root, the field table is redundant and misleading, so it is replaced — not supplemented — by a single table over every member repo:
multirepo.shalready emits the root as its own row, so the field table re-renders data §4b would show anyway. Rendering both forces ad-hoc dedup the skill never specified, and the row gets dropped inconsistently.- The fields are structurally uninteresting at a root: it holds no feature branches,
no upstreamis often permanent by design (a deliberately local workspace repo), and its dirt is usually infra noise (.envrc, editor config). - Worse, §4b's "only members with something to report" rule suppresses the root row — so "which branch am I on, is my cwd clean" could vanish from the page entirely.
Render one table from multirepo.sh's ---REPOS---, in emitted order (root first), including clean members — at the root the full inventory is the point:
### 📍 Working copy — {N} repos in this workspace
| Repo | Branch | Unpushed | Behind | Uncommitted |
|------|--------|----------|--------|-------------|
| `workspace` 📍 | main | local-only | — | 2 untracked |
| `repos/blc-2` | main | 0 | — | ✅ clean |
| `repos/docs` | main | 2 | — | ✅ clean |
{N}is the member count (rows emitted, root included).- Repo: the
{name}field. Append📍to the root row — that's where you're standing. - Branch: the
{branch}field. Do not abbreviate; a member sitting on an unexpected branch is a real signal. - Unpushed:
{ahead}, orlocal-onlywhenupstream=no(never pushed — for a deliberately local repo this is the intended state, not a warning). - Behind:
{behind}when >0, else—. When diverged (ahead>0 AND behind>0) it needs a rebase/pull before it can push — flag it in this cell:⚠️ diverged — N ahead / M behind. - Uncommitted:
{modified} modified / {untracked} untracked, omitting zero parts;✅ cleanwhen both are zero.
Then skip §4b entirely — this table is §4b, promoted. Carry the most urgent member (diverged, then unpushed, then uncommitted) into §5 exactly as §4b specifies.
Extras — both modes
These come from working-copy.sh and have no equivalent in the roll-up, so they render under either mode. In workspace-root mode they describe the root repo (the 📍 row); scope them explicitly so that reads unambiguously.
Stashes: do NOT include a stash row in the table. Only surface stashes if there are stashes on the current branch. If non-empty, add a one-line footnote below the table:
⚠️ {N} stash(es) on this branch — run `git stash list` to review.Global stash count is not interesting — omit it.
Other worktrees: the
OTHER-WORKTREES-UNSAFEsection lists only worktrees (excluding the current one) that have uncommitted changes or unpushed commits. If empty, render nothing — worktrees that are clean and pushed are not interesting. If non-empty, add a footnote below the table:⚠️ Other worktrees with unsaved work: - `/path/to/other` on `fix/X` — 3 modified, 2 unpushedOmit the dirty/unpushed parts that are zero (e.g.
3 modifiedalone, or2 unpushedalone).Recent handoffs for this repo: probe
~/.claude/handoffs/via:~/.agents/skills/handoffs/scripts/list.sh --summary-onlyParse from the output:
---SUMMARY---→current_repo_recent_live— recent handoffs for this repo that are still live work: not superseded and not finished (uses the same Mon→3 / Tue→4 / else→3 weekend buffer as the closed-PR list). "Finished" here means all referenced beads are closed — a localbdcheck that runs even on this offline (--summary-only, no--check-branches) call, so a handoff whose task shipped no longer counts as a live thread to resume. Re-wraps of the same branch still collapse to one. (Merged-PR / Jira-Done detection needs the network and only applies under/handoffs's--check-branches; landscape's offline count catches the bead-closed case.)---CURRENT-REPO-LATEST---→ a single{slug}|{branch}|{date}line for the newest current-repo handoff (the "last session"), or empty if none.---CURRENT-REPO-LIVE---→ one{slug}|{branch}|{date}|{time}line per recent live (non-superseded, non-finished) current-repo handoff, newest first (these are the threads behind thecurrent_repo_recent_livecount). The first line equals---CURRENT-REPO-LATEST---when the newest handoff is itself still live; if the newest one is finished it's absent here, so the first line is the most recent resumable thread instead.
If
current_repo_recent_live > 0, render its own table (N=current_repo_recent_live) — a footnote got lost among the other tables, so give it the same### {emoji} {Name}+ table shape as the sections above:### 📥 Recent handoffs ({N} live, last {RECENT-WINDOW-DAYS}d) | Handoff | Branch | When | |---------|--------|------| | `{slug}` 📍 | `{branch}` | {age} | | `{slug}` | `{branch}` | {age} | _`/handoffs` to browse and resume._One row per
---CURRENT-REPO-LIVE---line, newest first.Handoff: the
{slug}. Append📍to the row whose{slug}matches the---CURRENT-REPO-LATEST---line — that's the last session, the most likely resume target. When the newest handoff is already finished it won't appear here; the first row is then simply the most recent resumable thread, left unmarked.Branch: the
{branch}; render—when it is?or empty.When: relative age of
{date}(e.g.today,2d).Cap at 5 rows (newest first). When
N > 5, render the first 5 and replace the caption with_+{N − 5} more —/handoffsto browse._---SUMMARY---→workspace_member_handoffs(M) — handoffs owned by workspace member repos,0outside a multi-repo workspace. WhenM > 0, add a second caption line:_{M} in workspace members — `/handoffs` to see them._Deliberately a raw count, not a live count. Member rows are only classified under
--check-branches, which this call does not pass (workspace_classified=0), so every member row arrives unclassified — and unclassified reads as live. Rendering them as a liveness figure would report already-shipped member work as resumable, the exact failurecurrent_repo_recent_liveexists to prevent. The count itself is computed on this call regardless of the flag, so it adds no work and no network.
Suppress the table when
current_repo_recent_live == 0— silence is shorter. But whenM > 0, still render the heading and the member caption line alone, with no table:### 📥 Recent handoffs _17 in workspace members — `/handoffs` to see them._From a workspace root the current repo is usually the emptiest one, so an empty count there means "the threads are in the members", not "nothing to resume" — suppressing outright is what made the root's silence misleading. Older, superseded, or finished handoffs are still browsable via
/handoffs; this table is just a fresh-work hint, deliberately offline (no--check-branches, so no branch-staleness here). This call can run in parallel withworking-copy.sh.
4b. 🗂️ Other repos in this workspace
Only renders in single-repo mode — i.e. when cwd is inside a member repo, where §4's field table covers your branch and this section covers the siblings. At the workspace root, §4a has already absorbed this table; do not render it twice.
working-copy.sh (§4) inspects only the cwd repo (plus its own worktrees). In a multi-repo workspace — mgit services (.mgit.conf) or git submodules (.gitmodules) — that silently misses uncommitted/unpushed state in sibling service repos. This is the single biggest blind spot at session start: you orient on one repo and never notice that, say, dispatch was left with unpushed commits last night. Roll them up:
~/.agents/skills/wrap-up/scripts/multirepo.sh
It emits ---MARKER--- (mgit | submodules | none), ---ROOT---, and ---REPOS--- lines:
{name}|{branch}|{ahead}|{behind}|{upstream}|{modified}|{untracked} (ahead/behind are - with no upstream; the root repo appears as its own row). Always run it — §4 needs its ---ROOT--- to pick a mode — and run it in parallel with working-copy.sh. It's the same roll-up /wrap-up §3b uses.
---MARKER---isnone→ single repo, no workspace; skip this whole section silently (§4 already covered it).TOPLEVEL == ROOT→ workspace root; skip silently, §4a rendered it.- Otherwise render only the members with something to report (ahead>0, behind>0, or modified+untracked>0), excluding the cwd repo's own row — §4's field table already covers it in this mode. A clean+pushed member is noise. Skip the table entirely if every other member is clean — in a healthy workspace this section shows nothing, which is the point.
### 🗂️ Other repos in this workspace
| Repo | Branch | Unpushed | Behind | Uncommitted |
|------|--------|----------|--------|-------------|
- Unpushed:
{ahead}, orno upstreamwhen upstream=no (local-only, never pushed). - Behind: show only when >0. A member that's diverged (ahead>0 AND behind>0) needs a rebase/pull before it can push — flag it explicitly:
⚠️ diverged — N ahead / M behind. - Uncommitted:
{modified} modified / {untracked} untracked(omit zero parts).
Carry the most urgent member (diverged, then unpushed, then uncommitted) into the §5 Next suggestion — a sibling repo left in that state is exactly what a fresh session forgets.
5. Next step suggestion
After all blocks render, add a short footer with a concrete next step, picked from what's visible. Prefer the most load-bearing single action:
- If the current branch's PR is approved, CI green, 0 threads, clean merge state → suggest merging it (this unblocks stacked PRs).
- If the current branch's PR has unresolved review threads → suggest
/review-comments. - If the current branch's PR is behind main → suggest
/rebase main. - If there is uncommitted work → suggest committing or stashing.
- If a workspace member repo (§4a or §4b) is diverged → suggest rebasing/pulling it before it can block a push (name the repo).
- If a sibling service repo has unpushed commits or uncommitted work → suggest pushing/committing it (name the repo).
Once those "finish / unblock" actions are clear, the suggestion becomes a pick-what-to-work-on decision. This is the morning "resume a handoff or start fresh?" question — arbitrate it here rather than leaving the user to choose between /handoffs and /next. Use the handoff signals from §4 (current_repo_recent_live and the ---CURRENT-REPO-LATEST--- {slug}|{branch}|{date} line):
- If a recent live handoff is on the current branch (its
{branch}equals---BRANCH---, which in workspace-root mode is the 📍 row's branch — handoffs are recorded per repo, so this stays the cwd repo's branch either way) → strongest resume signal: you're already sitting on its branch and it carries the open threads + suggested next step the bare branch doesn't. SuggestResume {slug} — /handoffs. - If exactly one in-progress bead and no current-branch handoff → suggest resuming the bead (show the ID).
- If nothing decisive above but a recent live handoff exists (
current_repo_recent_live > 0) → lead with the handoff and name the fallback in one sentence, e.g.Resume {slug} (last session's thread) via /handoffs — or /next safe for fresh work.A warm thread beats a cold start, but the user keeps the choice. - If no live handoff and nothing in progress but ready beads exist → suggest
/next. - Otherwise → suggest
/triageor pulling a Jira ticket.
Format as one line:
---
**Next:** _{suggestion}_
Single sentence. Pick one action — don't list a menu. The one exception is the handoff-fallback branch above, which deliberately names a single fallback (Resume {slug} … — or /next safe …) so the morning "resume or start fresh?" choice stays visible in one sentence.
Failure modes
Each block is independent — a failure in one must not prevent the others from rendering.
- Not in a git repo: skip the Working-copy block, print
_Not in a git repository._in its place. - No Jira MCP configured: skip the Jira block, print
_Jira MCP not configured._. - gh not authenticated: skip the PR block, print
_GitHub CLI not authenticated (run \gh auth login`)._`. - No beads in repo: skip the Beads block, print
_No beads in this repo._. - Single repo (no
.mgit.conf/.gitmodules): the §4b roll-up emitsMARKER=none— skip that block silently; §4 already covers the one repo. multirepo.shfails or emits no---ROOT---: fall back to §4's single-repo field table. An unknown mode must degrade to the always-valid shape, never to a blank Working-copy block.
Performance notes
- Run the git commands in parallel (single Bash call, or parallel tool calls).
- The PR section dominates runtime — that's why
/landscape quickskips it. - Don't re-render pr-status's "deltas since last check" — landscape is a snapshot, not a diff.