# Landscape

> Morning catch-up view — assigned Jira tickets and recent discussion, open PRs, current working copy state, and (if present) in-progress and ready beads in one glance. Run at session start to orient.

- Skill: `flurdy/landscape` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add flurdy/landscape`
- Raw SKILL.md: https://api.skillmd.com/api/skills/flurdy/landscape/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: flurdy (https://skillmd.com/u/flurdy)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/flurdy/landscape

---


# 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

```bash
/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":

1. **📋 Jira** — tickets assigned to you, not Done (with sprint and latest discussion)
2. **🔀 PRs** — org-wide open PRs, recently closed, unresolved threads
3. **🎯 Beads** — in-progress and top ready beads in this repo (skipped if `bd` not installed)
4. **📍 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
5. **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 `/landscape` run MUST execute every fetch from scratch — `date`, the Jira MCP query, the `gh-pr-list-*` and `gh-pr-details.sh` scripts, `beads.sh`, and `working-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-status` and the same risk applies here.
>
> **MUST use the dedicated helper scripts.** Never construct ad-hoc `bd …` or `git …` shell pipelines for this skill. Specifically: do NOT chain `command -v bd` probes with `bd list … && …` or `… || bd list --ready` inside a single Bash call. Always invoke `~/.agents/skills/landscape/scripts/beads.sh` instead — it handles probing, repo gating, and listing internally. Likewise, do NOT hand-walk sibling service repos with your own `for … 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

```bash
date '+%A %Y-%m-%d %H:%M'
```

Output:

```markdown
## 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:

1. Bucket by sprint state: **active** sprints first, then **future** sprints (by `startDate` ascending), then **no-sprint / backlog** last.
2. Within each bucket, sort by priority `P1 → P2 → P3 → P4 → P5` (treat missing/unknown priority as lowest).
3. Within equal priority, preserve the JQL `updated DESC` order.

Keep it as a single table — the Sprint column makes the group boundary visible without subheadings.

Render:

```markdown
### 📋 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 `self` field, 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:

```markdown
| 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**: `—` when `total` is 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:

1. List open PRs org-wide via `gh-pr-list-open.sh`
2. List recently closed via `gh-pr-list-closed.sh`
3. Fetch details via `gh-pr-details.sh` (grouped by owner/repo)
4. 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:

```bash
~/.agents/skills/landscape/scripts/beads.sh
```

Output sections (delimited by `---<NAME>---`):
- `---STATUS---` — `OK`, `NO_BD` (bd not installed), or `NO_BEADS_IN_REPO` (no `.beads/` here)
- `---IN-PROGRESS---` — output of `bd list --status=in_progress` (only if `STATUS=OK`)
- `---READY---` — JSON array from `next-bd --json` (or plain text from `bd list --ready` fallback) (only if `STATUS=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

```markdown
**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:

```markdown
**Ready ({shown} of {total})**
| # | ID | Pri | Type | Jira | Sprint | Status | Title |
|---|----|-----|------|------|--------|--------|-------|
```

- `#` is the picker index `1-N`.
- `Jira` column: markdown link `[KEY](https://yourorg.atlassian.net/browse/KEY)`. Show `—` if no Jira key.
- `Sprint` column: 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.
- `Status` column: 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:

```bash
~/.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:

```markdown
### 📍 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), show `no 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.sh` already 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 upstream` is 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:

```markdown
### 📍 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}`, or `local-only` when `upstream=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; `✅ clean` when 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-UNSAFE` section 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 unpushed
  ```
  Omit the dirty/unpushed parts that are zero (e.g. `3 modified` alone, or `2 unpushed` alone).
- **Recent handoffs for this repo**: probe `~/.claude/handoffs/` via:
  ```bash
  ~/.agents/skills/handoffs/scripts/list.sh --summary-only
  ```
  Parse 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 local `bd` check 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 the `current_repo_recent_live` count). 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:

  ```markdown
  ### 📥 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 — `/handoffs` to browse._`
  - `---SUMMARY---` → `workspace_member_handoffs` (`M`) — handoffs owned by **workspace member** repos, `0` outside a multi-repo workspace. When `M > 0`, add a second caption line:

    ```markdown
    _{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 failure `current_repo_recent_live` exists 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 when `M > 0`, still render the heading and the member caption line alone, with no table:

  ```markdown
  ### 📥 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 with `working-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:

```bash
~/.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---` is `none`** → 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.

```markdown
### 🗂️ Other repos in this workspace

| Repo | Branch | Unpushed | Behind | Uncommitted |
|------|--------|----------|--------|-------------|
```

- **Unpushed**: `{ahead}`, or `no upstream` when 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. Suggest `Resume {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 `/triage` or pulling a Jira ticket.

Format as one line:

```markdown
---
**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 emits `MARKER=none` — skip that block silently; §4 already covers the one repo.
- **`multirepo.sh` fails 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 quick` skips it.
- Don't re-render pr-status's "deltas since last check" — landscape is a snapshot, not a diff.

