# Sprint Snapshot

> Capture a point-in-time snapshot of a scrum team's current sprint board from Jira and render it into the Obsidian vault as (1) an Obsidian Canvas with sprint overview, team workload, kanban columns, and issue cards, (2) a companion markdown summary, and (3) an append-only JSONL trend log. Auto-detects sprint phase (start / week 1 / week 2 / week 3 / end) from today's date. Supports `--as-of <date>` for historical snapshots. Reuses obsidian-canvas / obsidian-vault / obsidian-markdown for vault writes and follows the issue-* skills' Jira context-gathering patterns. Use when user says "sprint snapshot", "snapshot the sprint", "capture sprint state", "sprint planner", "scrum board snapshot", or invokes /sprint-snapshot.

- Skill: `risadams/sprint-snapshot` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add risadams/sprint-snapshot`
- Raw SKILL.md: https://api.skillmd.com/api/skills/risadams/sprint-snapshot/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: risadams (https://skillmd.com/u/risadams)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/risadams/sprint-snapshot

---


# Sprint Snapshot

Point-in-time snapshot of a scrum team's current sprint. Read-only across Jira. Writes three artifacts per run into the team's vault folder so progress can be tracked over time, scrum-of-scrums and end-of-sprint reporting become low-friction, and trends can be forecast.

This skill is a Claude Code port of ``<scripts-repo>\Sprint-Planner.ps1`` + `<scripts-repo>\Get-Jira-Sprint-Planner.ps1` — same canvas layout, same output path convention, same JQL shape. See [REFERENCE.md](REFERENCE.md) for the canvas layout spec, JSONL schema, and matching algorithm. See [EXAMPLES.md](EXAMPLES.md) for invocation parity with the PS scripts.

## Quick start

```text
/sprint-snapshot                                  → Aurora, INC 28, current sprint, auto phase
/sprint-snapshot Borealis                             → single team override
/sprint-snapshot Aurora --sprint 2 --inc 28       → explicit sprint within increment
/sprint-snapshot Aurora --as-of 2026-04-08        → historical snapshot (Jira `updated <= ...`)
/sprint-snapshot Aurora --phase "week 1"          → override the auto-derived phase
```

## Parameters

| Param | Default | Maps to PS |
| :--- | :--- | :--- |
| `Team` | from memory or prompt | `-Team` |
| `Inc` | from sprint-config or prompt | `-Increment` |
| `Sprint` | from sprint-config or prompt | `-Sprint` (1-4) |
| `Phase` | auto-derived from today | `-Label` (`start`/`week 1`/`week 2`/`week 3`/`end`) |
| `AsOf` | none | `-AsOf` |
| `JQL` | auto-built (see Phase 2) | `-JQL` |

JIRA project key, vault root, GitLab base, default team, capacity, and velocity are all **resolved from memory and the per-sprint config note** — never accept them as raw args. See *Config resolution* below.

## Config resolution (run once at the start of every invocation)

| Placeholder | Source | Default / how to populate |
| :--- | :--- | :--- |
| `{{vault_root}}` | `reference_obsidian_vault.md` (`**Vault root:**` line) | `C:\Users\<user>\dev\<vault>\` |
| `{{jira_project}}` | `reference_jira_default_project.md` | `PROJ` |
| `{{default_team}}` | `reference_default_scrum_team.md` (create if missing) | Prompt once — recommend `Aurora` (matches PS script default), then persist |
| `{{rosters_dir}}` | derived | `{{vault_root}}\Scrum Teams\_rosters\` |
| `{{output_root}}` | derived | `{{vault_root}}\Scrum Teams\<Team>\Scrum 📅\INC <Inc>\Sprint <Sprint>\` |
| `{{sprint_config}}` | derived | `{{output_root}}\_sprint.md` (frontmatter holds capacity / start / end / last_velocity / avg_velocity) |
| Default labels for JQL | `reference_team_jira_labels.md` (per-team table) | First run prompts for each team's label set; persist as a table row. Aurora default = `aurora,dev_common,dev_feature,doc_feature` (PS script). |

If a memory file is missing or its value is `<unset>`, **prompt once via `AskUserQuestion`** with the PS-script default as the recommended option, then write the answer back so the next run skips the prompt. Same pattern as `daily-standup-prep`.

## Sprint config note (per-sprint frontmatter)

`{{sprint_config}}` is the durable source of truth for capacity/velocity per sprint. Schema:

```yaml
---
team: Aurora
increment: 28
sprint: 2
start_date: 2026-04-01
end_date: 2026-04-21
capacity: 80.8
last_sprint_velocity: 55
avg_velocity_last_3: 58
labels: [pyrite, dev_common, dev_feature, doc_feature]
---
# Sprint 2 — Aurora (INC 28)
Notes, links, retro pointers go below.
```

If the file doesn't exist when the skill runs, prompt for **start_date, end_date, capacity, last_sprint_velocity, avg_velocity_last_3** via a single `AskUserQuestion` (recommend the PS-script defaults: capacity 80.8, last_velocity 55, avg 0), then write it. Subsequent snapshots in the same sprint reuse it.

## Roster bootstrap (first-run only)

Same pattern as `daily-standup-prep`. Look for `{{rosters_dir}}\<Team>.csv`. If missing, offer to copy from `<scripts-repo>\bin\Teams\<Team>.csv` via `AskUserQuestion`. CSV format and identity-matching cascade are identical — see [daily-standup-prep/REFERENCE.md](../daily-standup-prep/REFERENCE.md#roster-csv-schema).

## Workflow

```text
- [ ] Phase 1: Resolve config, load roster, load/create sprint config
- [ ] Phase 2: Auto-derive phase + build effective JQL (with optional AsOf)
- [ ] Phase 3: Fetch Jira issues + per-issue details
- [ ] Phase 4: Match assignees to roster + bucket by status
- [ ] Phase 5: Render canvas (delegate to obsidian-canvas)
- [ ] Phase 6: Render companion markdown summary
- [ ] Phase 7: Append snapshot row to _snapshots.jsonl (trend log)
- [ ] Phase 8: Console summary
```

### Phase 1 — Config + roster + sprint config

Resolve every `{{placeholder}}` per the table above. Load the roster CSV using the cascade in [daily-standup-prep/REFERENCE.md](../daily-standup-prep/REFERENCE.md#identity-matching-cascade). Read or create `{{sprint_config}}` and parse its frontmatter into `{Team, Inc, Sprint, StartDate, EndDate, Capacity, LastSprintVelocity, AvgVelocityLast3, Labels[]}`.

### Phase 2 — Phase derivation + JQL

**Auto-derive `Phase`** (unless user overrode):

| Days since `StartDate` (Pittsburgh local) | Phase |
| :--- | :--- |
| ≤ 0 | `start` |
| 1–7 | `week 1` |
| 8–14 | `week 2` |
| 15–20 | `week 3` |
| ≥ days_until_end (within last 2 days, or after) | `end` |

State the chosen phase in one short sentence: *"Snapshotting Aurora Sprint 2 (INC 28) — phase: week 1 (day 5 of 21)."*

**Build effective JQL** (port of `<scripts-repo>\Get-Jira-Sprint-Planner.ps1` lines 24, 554-570):

```text
project = {{jira_project}}
  AND labels in ({comma-joined Labels[] from sprint config})
  AND Sprint in openSprints()
  AND Sprint not in futureSprints()
```

If `--as-of <date>` was supplied, append `AND updated <= "{YYYY-MM-DD}"` and parse via standard date handling. Echo the final JQL one line before issuing the search so the user can confirm.

### Phase 3 — Fetch Jira issues

Single call:

```
jira_search(
  jql=<effective JQL>,
  fields="summary,description,status,assignee,priority,issuetype,labels,customfield_10106",
  max_results=200
)
```

`customfield_10106` is **Story Points** (matches the PS script). Don't refetch per-issue unless the search response is truncated. Cap at 200 — warn if hit.

### Phase 4 — Match + bucket

For each issue:

1. Parse story points: treat as `double`; missing → `0`. Track total + per-status running totals.
2. If `assignee` present, run the identity cascade against the roster. Match → `team_member`. No match → `off_team_member` (still tracked, separate group on canvas).
3. Bucket the issue by status:
   - `In Progress` → IN PROGRESS column
   - `In Review` → IN REVIEW column
   - `Done` / `Won't Fix` / `Duplicate` → DONE column
   - everything else → TO DO (split into Assigned vs Unassigned sub-columns based on whether `assignee` is set)
4. Increment per-member workload `(TicketCount, StoryPoints)` for matched team and off-team members.

Compute summary stats: `totalTickets`, `totalStoryPoints`, per-status counts and points, `unassignedTickets/Points`, `ticketsWithPoints`, `ticketsWithoutPoints`, `percentDone` (tickets), `percentPointsDone`.

### Phase 5 — Render the canvas

**Delegate to `obsidian-canvas` via `Skill`** for actual JSON authoring, ID generation, and validation — do not hand-author canvas JSON inline. The canvas layout (groups, node positions, sizes, colors, edges) is fully specified in [REFERENCE.md](REFERENCE.md#canvas-layout). Pass the rendered structure to `obsidian-canvas`.

Issue card content uses the `JIRA:KEY` convention so each card auto-links to the live ticket via the user's Obsidian Jira plugin:

```markdown
## [PROJ-1234] Summary line truncated to ~80 chars

JIRA:PROJ-1234

**Assignee:** Alex Rivera
**Points:** 3
**Type:** Story
```

The `JIRA:KEY` line goes on its own line — the plugin auto-renders. Do **not** wrap it in a wikilink. Team-member cards use the wikilink form `[[@First Last]]` (verify the vault note exists via `Glob("{{vault_root}}/🤼 Team/**/@*.md")`; if missing, fall back to bold plain text — same rule as `daily-standup-prep`).

Output path: `{{output_root}}\<phase>.canvas` — e.g. `…\Sprint 2\week 1.canvas`. Phase label is preserved literally including the space.

**Never overwrite silently.** If `<phase>.canvas` exists, prompt: `Overwrite` / `Save with timestamp suffix (week 1 (HH-MM).canvas)` / `Skip canvas`. Same pattern as `daily-standup-prep`.

### Phase 6 — Companion markdown summary

Write `{{output_root}}\<phase>.md` next to the canvas. Schema is the Sprint Summary table from the PS script (lines 973-996) plus a small header. **Delegate to `obsidian-markdown`** if extending with callouts, dataview, or new constructs. Template:

```markdown
---
team: {{team}}
increment: {{inc}}
sprint: {{sprint}}
phase: {{phase}}
snapshot_date: {{YYYY-MM-DD HH:mm}}
as_of: {{as_of_or_blank}}
total_tickets: {{n}}
total_story_points: {{n}}
percent_done_tickets: {{n}}
percent_done_points: {{n}}
canvas: "[[{{phase}}.canvas]]"
---
# Sprint {{sprint}} — {{phase}} snapshot

> Captured {{YYYY-MM-DD HH:mm}} (Pittsburgh){{ as_of? " — as-of " + as_of : "" }}

## Sprint Summary

| Metric | Value |
| :--- | --: |
| Total Tickets | … |
| Total Story Points | … |
| Tickets Done / Open / In Review / In Progress / To Do | … |
| Unassigned Tickets / Points | … |
| % Tickets Done / % Points Done | … |
| Tickets With Points / Without Points | … |
| Points Done / In Review / In Progress / To Do | … |
| Capacity / Committed / Remaining | … |
| Last sprint velocity / 3-sprint avg | … |

## Per-member workload

| Member | Tickets | Points | % of capacity |
| :--- | --: | --: | --: |
| [[@First Last]] | … | … | … |

## Off-team workload

(only rendered if non-empty)

## Velocity trend

(↗️ Increasing / ↘️ Decreasing / → Stable — port of PS lines 305-317)
```

Same overwrite-prompt rule as Phase 5.

### Phase 7 — Append to trend log

Append a single JSON line to `{{output_root}}\_snapshots.jsonl`. Create the file if missing — never overwrite. Schema is in [REFERENCE.md](REFERENCE.md#snapshots-jsonl-schema). Minimum fields:

```json
{"snapshot_at":"2026-04-08T09:14:00-04:00","as_of":null,"phase":"week 1","team":"Aurora","inc":28,"sprint":2,"capacity":80.8,"committed":62.5,"remaining":18.3,"totals":{"tickets":24,"points":62.5},"by_status":{"todo":{"tickets":8,"points":21},"in_progress":{"tickets":6,"points":15.5},"in_review":{"tickets":4,"points":11},"done":{"tickets":6,"points":15}},"unassigned":{"tickets":3,"points":8},"velocity":{"last":55,"avg3":58}}
```

This append-only log is what enables forecasting and trend analysis without parsing canvas files. Keep one row per snapshot — never rewrite history.

### Phase 8 — Console summary

Print a brief block (matches the PS `Write-Verbose` summary, lines 893-903):

```
=== Aurora Sprint 2 (INC 28) snapshot — phase: week 1 ===
JQL:        project = PROJ AND labels in (pyrite,dev_common,...) AND Sprint in openSprints()...
Issues:     24 total / 6 done (25%) / 4 in review / 6 in progress / 8 to do
Points:     62.5 total / 15 done / 11 in review / 15.5 in progress / 21 to do
Unassigned: 3 tickets (8 pts)
Capacity:   80.8 (committed 62.5, remaining 18.3)
Saved:      [week 1.canvas](Scrum Teams/Aurora/Scrum 📅/INC 28/Sprint 2/week 1.canvas)
            [week 1.md](Scrum Teams/Aurora/Scrum 📅/INC 28/Sprint 2/week 1.md)
            [_snapshots.jsonl] (+1 row, 4 total)
```

## Rules

- **Read-only across Jira.** Never transition issues, post comments, change assignees, or touch the API beyond `jira_search` / `jira_get_issue`.
- **Append-only on `_snapshots.jsonl`.** Never rewrite or compact. The whole point is point-in-time history.
- **Never overwrite an existing canvas or markdown summary silently.** Always confirm via `AskUserQuestion`.
- **Confirm before persisting to memory or to the sprint config note.** When populating a missing value, use the PS-script default as the recommended option but let the user override before writing.
- **Phase label is preserved literally** including the space (`week 1`, not `week-1`) — the PS script's filenames are the canonical convention and other tools may consume them.
- **Pittsburgh local time** for snapshot timestamps and phase derivation. Jira `updated` cutoffs use ISO date (`YYYY-MM-DD`).
- **No fabricated data.** If Jira fails, surface the failure in the console summary and abort. Do not invent issues. (Unlike daily-standup-prep, partial outputs aren't useful here — a snapshot must reflect real Jira state.)
- **Wikilinks for team members only when the vault person note exists.** Use `Glob("{{vault_root}}/🤼 Team/**/@*.md")` to verify before writing `[[@First Last]]`. Fall back to bold plain text otherwise — never invent a wikilink.
- **`JIRA:KEY` for issue references.** This is the Obsidian Jira plugin's auto-link convention — do not wrap in a wikilink, do not add the URL inline.
- **Story Points = `customfield_10106`.** This is the PS-script's hardcoded ID and it matches the user's Jira instance. If the field is missing for an issue, treat as `0` and increment `ticketsWithoutPoints`.
- **Auto-derived phase is a suggestion.** Always echo the derived phase in Phase 2 output so the user can rerun with `--phase` if they disagree.

## Edge cases

- **Sprint config note missing or incomplete** — prompt once for the missing fields, write the file, continue. Don't run with synthetic capacity/velocity.
- **Roster CSV missing** — offer to copy from the upstream PowerShell scripts repo (same pattern as `daily-standup-prep`); if neither path exists, abort the team.
- **`labels in (...)` returns 0 issues** — render an "empty sprint" canvas (groups + columns, no issue cards), still write the markdown summary and JSONL row. Surface the empty result clearly so the user can fix the label set.
- **`AsOf` date is in the future** — reject with an `AskUserQuestion`: did you mean today, or a different date? Don't silently fall through to "current".
- **Issue without an assignee** — goes in TO DO (Unassigned) column; counted in `unassignedTickets/Points`.
- **Assignee not on roster** — goes in the off-team group on the right of the canvas; tracked separately in `offTeamMembersWorkload`. Don't pollute the team workload total with off-team points.
- **Vault `🤼 Team` folder missing the person note** — render bold plain text instead of a broken wikilink. Same rule as `daily-standup-prep`.
- **Re-run on the same phase** — overwrite/timestamp/skip prompt for `.canvas` and `.md`. The JSONL row is always appended (it's the point-in-time history).
- **`customfield_10106` field name differs in the user's Jira** — the field ID is the user's confirmed Story Points field per the PS script. If a future Jira config change breaks it, the skill will count everything as 0 points and surface the anomaly in the console summary; the fix is to update the field ID in `reference_jira_default_project.md` (add a `**Story Points field:**` line).
- **Pre-sprint snapshot (`AsOf` = sprint start)** — the PS script's intended use case for `start` phase. JQL filter still works because the issues already have the sprint label by then.

## Related skills

This skill writes into the user's Obsidian vault and reads from Jira. Delegate to the relevant sibling skills via `Skill` rather than reimplementing.

| Skill | Use it for |
| :--- | :--- |
| `obsidian-canvas` | **Canonical canvas authoring.** Phase 5 delegates to it for ID generation, JSON shape, and validation. Do not hand-write canvas JSON. |
| `obsidian-markdown` | Use when extending the companion `.md` template with callouts, dataview, embeds, or block IDs beyond the base summary table. |
| `obsidian-vault` | Use for batch wikilink target verification (e.g. validating every roster member has a vault person note). Prefer over repeated `Glob` calls when checking >15 names. |
| `obsidian-cli` | Use for property/task queries against the snapshot — e.g. "show me all snapshots for Aurora where committed > capacity". |
| `obsidian-bases` | Use when the user wants a dashboard `.base` file aggregating snapshots across sprints (e.g. "velocity trend per team", "carry-over rate per increment"). Do not hand-author `.base` YAML. |
| `daily-standup-prep` | **Pattern parent.** Same memory-driven config, same roster CSV format, same identity matcher, same vault-write conventions. Reuse [its REFERENCE.md](../daily-standup-prep/REFERENCE.md) for roster + identity cascade. |
| `issue-feature-breakdown` | Pattern reference for Jira context-gathering depth and read-only discipline. |
| `issue-suggest-component` | Pattern reference for memory-driven default Jira project + bulk pacing if the snapshot ever expands to multi-team. |
| `clarity-council` | Future extension: stand up a "sprint health council" persona session that reviews the latest JSONL trend log and flags risks. Out of scope for v1. When that lands, the council pull-list is **`statistics-expert + scrum-master + infographics-expert`** — statistics-expert reads the trend with uncertainty rendering, infographics-expert chooses the chart format (sparklines per metric? small-multiple per team?) and produces the SVG/Mermaid output that embeds back into the canvas or companion markdown. |

