---
spdx-license: AGPL-3.0-or-later
user-invocable: true
description: "View and manage the unscheduled issue backlog."
---

!`bash ~/.claude/hooks/sweetclaude/record-event.sh skill_invoked "sweetclaude:project-backlog" 2>/dev/null || true`

## MIGRATION GUARD

Before any other work, run the read-only recovery guard:

```bash
python3 ~/.claude/scripts/sweetclaude/recovery/recover_project.py guard --project-dir . --pretty 2>/dev/null
```

If the guard status is `run-recover`, stop and route to `/sweetclaude:recover`. If it is `manual-review`, stop and show the guard message. Do not run taxonomy migration from this skill.

## MODE CHECK

Read `mode` from pre-loaded session state.

If `mode` is `shape_up`, output and stop:

> "This skill is not active in **Shape Up** mode.
>
> Shape Up has no backlog by design. New work enters through pitches: describe the problem and proposed solution, get it approved at the betting table, then create issues from the approved pitch.
>
> Write a pitch: `/sweetclaude:project-issues pitch`"

All other modes: proceed normally.

```python
import pathlib, yaml, re, datetime

BACKLOG_BASE = pathlib.Path('.sweetclaude/product/backlog')

def read_issue_file(path):
    raw = pathlib.Path(path).read_bytes().decode('utf-8').replace('\r\n', '\n')
    parts = raw.split('---', 2)
    fm = yaml.safe_load(parts[1]) or {}
    body = parts[2] if len(parts) > 2 else ''
    return fm, body

def find_issue_by_id(issue_id):
    for p in BACKLOG_BASE.rglob('*.md'):
        if p.stem.startswith(issue_id + '-') or p.stem == issue_id:
            return p
    return None

def write_issue_file(path, fm, body):
    content = f"---\n{yaml.safe_dump(fm, default_flow_style=False, sort_keys=False).rstrip()}\n---\n{body}"
    pathlib.Path(path).write_text(content, encoding='utf-8')

def rebuild_cache():
    import subprocess, os
    subprocess.run(['python3', os.path.expanduser('~/.claude/scripts/sweetclaude/cache.py'), '--project-dir', '.', '--rebuild'], capture_output=True)

# Load active backlog items (exclude done/ subdirs and metadata files)
active_files = [
    p for p in BACKLOG_BASE.rglob('*.md')
    if p.name not in ('INDEX.md', 'MIGRATION-MAP.md', 'SCHEMA.md') and '/done/' not in str(p)
]
items = []
for p in active_files:
    fm, _ = read_issue_file(p)
    if fm.get('status') not in ('done', 'abandoned', 'deferred'):
        items.append((p, fm))
```

# Project Backlog

The backlog is every issue with no sprint assignment. Arguments: `$ARGUMENTS`

---

## Routing

| Arguments | Operation |
|---|---|
| (empty) | → **View** the full backlog |
| `promote <ID> <SP-NNN>` | → **Promote** issue into a sprint |
| `defer <ID>` | → **Defer** issue (status → deferred) |
| `review-inferred` | → **Review** imported issues needing confirmation |

---

## View (default)

Use the items loaded above. Present backlog grouped by priority bucket:

```
Backlog — {N} unscheduled issues

P0 ({n})
  ISSUE-001  story  xs  Title of issue

P1 ({n})
  ISSUE-002  story  m   Title of issue
  ISSUE-003  bug    s   Title of issue

P2 ({n})
  ...

UNESTIMATED ({n} — no priority or effort set)
  ISSUE-NNN  story  —   Title of issue
```

Priority buckets: `P0`, `P1`, `P2`, `P3` (in that order). Items with no priority → UNESTIMATED.

After the list, surface any of these conditions if present:

- **Unestimated count ≥ 10:** "Run `/sweetclaude:project-backlog-triage` — {N} issues have no effort or priority estimate."
- **Any items with `origin: imported`:** "{N} imported issues need review. Run `project-backlog review-inferred`."

---

## Promote

Move issue `<ID>` into sprint `<SP-NNN>`.

```python
path = find_issue_by_id('<ID>')
fm, body = read_issue_file(path)
```

Verify:
- Issue status is `new`, `ready`, or `active`. If `done` or `abandoned`, say: "Can't promote a {status} issue."

If valid:

```python
fm['sprint'] = '<SP-NNN>'
# Append to Sprint History in body
write_issue_file(path, fm, body)
```

```bash
python3 ~/.claude/scripts/sweetclaude/status.py set --file {path} --status ready --actor project-backlog --project-dir .
```

Confirm: `Promoted {ID} → {SP-NNN}`

---

## Defer

Set issue status to `deferred`. Hides it from the default backlog view without closing it.

```bash
python3 ~/.claude/scripts/sweetclaude/status.py set --file {path} --status deferred --actor project-backlog --project-dir .
```

Confirm: `Deferred <ID> — removed from active backlog`

---

## Review inferred

Load all items with `origin: imported` that haven't been reviewed:

```python
imported = [(p, fm) for p, fm in items if fm.get('origin') == 'imported']
```

If none: "No imported issues to review."

Otherwise, present them one at a time:

```
Imported issue {n} of {total}:

  {ID} — {title}
  Origin: imported
  Type: {type}

  Keep (accept as-is), Edit (change title/type), or Discard?
```

Wait for response per issue.
- **Keep:** `fm['origin'] = 'manual'` → write_issue_file → confirm "Kept as {ID}"
- **Edit:** ask for new title and/or type, write both fields + set origin=manual
- **Discard:** delete the file and rebuild cache

After all reviewed: "Reviewed {N} imported issues: {kept} kept, {edited} edited, {discarded} discarded."

---

## Rules

- The backlog view never shows `done`, `abandoned`, or `deferred` issues — those are archived.
- Promoting into a sprint does not start the sprint. Sprint activation is done in `project-sprints`.
- An issue promoted into an active sprint gets status `ready`. An issue promoted into a planned sprint stays `ready`.
- Never auto-promote an entire backlog into a sprint — always promote individual issues deliberately.
- All reads and writes go to `.sweetclaude/product/backlog/<ID>-<slug>.md` files.
