diff --git a/backlog/SKILL.md.tmpl b/backlog/SKILL.md.tmpl
new file mode 100644
index 0000000..82c70fe
--- /dev/null
+++ b/backlog/SKILL.md.tmpl
@@ -0,0 +1,225 @@
+---
+name: backlog
+version: 1.0.0
+description: |
+ Persistent task backlog across Claude Code sessions. Track review findings, QA bugs,
+ and improvement suggestions in a SQLite database. Commands: list, add, complete,
+ search, stats, next. Use when asked to "add to backlog", "show backlog", "track this",
+ "what's pending", or after reviews to capture non-critical findings.
+allowed-tools:
+ - Bash
+ - Read
+ - AskUserQuestion
+---
+
+{{PREAMBLE}}
+
+# /backlog: Persistent Task Tracking
+
+Track tasks, review findings, QA bugs, and improvement suggestions in a SQLite database that persists across Claude Code sessions. Data lives at `~/.gstack/backlog.db`.
+
+---
+
+## Setup
+
+Locate the backlog script:
+
+```bash
+_SKILL_DIR="${_SKILL_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)}"
+[ -z "$_SKILL_DIR" ] && _SKILL_DIR="$HOME/.claude/skills/gstack"
+_BL="$_SKILL_DIR/backlog/scripts/backlog.sh"
+if [ ! -x "$_BL" ]; then
+ echo "ERROR: backlog.sh not found or not executable at $_BL"
+fi
+```
+
+Use `$_BL` as the script path for all commands below.
+
+---
+
+## Subcommands
+
+Parse the user's input after `/backlog` and run the matching command.
+
+### `/backlog list` (default when no subcommand)
+
+Show pending tasks, sorted by priority (critical first).
+
+```bash
+$_BL list # pending tasks (default)
+$_BL list --status all # all tasks including completed
+$_BL list --status completed # completed tasks only
+$_BL list --priority critical # only critical tasks
+$_BL list --limit 20 # limit output
+```
+
+Present the output as-is -- the script formats a table with summary counts.
+
+### `/backlog add "
"` or `/backlog add` (then parse from context)
+
+Add a new task. Deduplicates automatically (skips if identical pending title exists).
+
+```bash
+$_BL add "Fix input validation on settings page" --priority important --source review
+$_BL add "Refactor auth middleware" --priority suggestion --source manual
+$_BL add "Critical SQL injection in /api/users" --priority critical --source review --file "app/api/users/route.ts" --line 42
+```
+
+**Priority levels**: `critical` | `important` | `suggestion` (default: suggestion)
+**Source values**: `review` | `qa` | `manual` (default: manual)
+
+**Optional flags**:
+- `--description "longer explanation"` -- details about the task
+- `--file "path/to/file.ts"` -- file reference
+- `--line 42` -- line number reference
+
+When the user says things like "add to backlog", "track this", "remember to fix this later", parse their intent and construct the add command. Infer priority from urgency words:
+- "critical", "urgent", "security", "broken" -> `critical`
+- "important", "should fix", "needs", "bug" -> `important`
+- "nice to have", "could", "might", "idea", "suggestion" -> `suggestion`
+
+### `/backlog complete `
+
+Mark a task as completed.
+
+```bash
+$_BL complete 42
+```
+
+After completing, show a brief confirmation. If you just finished a task from the backlog, offer to show the next task.
+
+### `/backlog delete `
+
+Soft-delete a task (marks as deleted, does not remove from DB).
+
+```bash
+$_BL delete 42
+```
+
+### `/backlog search `
+
+Search tasks by title or description.
+
+```bash
+$_BL search "validation"
+$_BL search "auth middleware"
+```
+
+### `/backlog stats`
+
+Show backlog statistics: counts by status, priority, and source.
+
+```bash
+$_BL stats
+```
+
+### `/backlog next`
+
+Get the highest-priority pending task as JSON. Useful for automated workflows.
+
+```bash
+$_BL next
+```
+
+If the result is empty (`{}`), tell the user "No pending tasks in the backlog."
+
+### `/backlog start `
+
+Mark a task as in-progress. Used when beginning work on a backlog item.
+
+```bash
+$_BL start 42
+```
+
+### `/backlog reset `
+
+Reset an in-progress task back to pending (e.g., if work was interrupted).
+
+```bash
+$_BL reset 42
+```
+
+### `/backlog get `
+
+Show full details of a specific task.
+
+```bash
+$_BL get 42
+```
+
+### `/backlog dedup`
+
+Remove duplicate pending tasks (keeps the lowest ID for each title).
+
+```bash
+$_BL dedup
+```
+
+### `/backlog cleanup`
+
+Remove garbage entries (code fragments, shell syntax that got accidentally added as tasks).
+
+```bash
+$_BL cleanup
+```
+
+---
+
+## Integration with Other Skills
+
+The backlog is the canonical place to capture non-critical findings from other gstack workflows.
+
+### After `/review`
+
+When a `/review` run finds non-critical (informational) issues, add them to the backlog:
+
+```bash
+# For each informational finding from /review:
+$_BL add "Review: " --priority important --source review --file "" --line
+```
+
+Tell the user: "Added N informational findings to the backlog. Run `/backlog list` to see them."
+
+### After `/qa` or `/qa-only`
+
+When QA finds bugs that won't be fixed in the current session:
+
+```bash
+# For each unfixed QA finding:
+$_BL add "QA: " --priority --source qa --description ""
+```
+
+Map QA severity to backlog priority:
+- Critical/High QA severity -> `critical`
+- Medium QA severity -> `important`
+- Low QA severity -> `suggestion`
+
+### During Development
+
+When you notice something that could be improved but isn't the current task:
+
+```bash
+$_BL add "Noticed: " --priority suggestion --source manual --file ""
+```
+
+Tell the user: "Added to backlog: . Continuing with the current task."
+
+**Do NOT stop the current workflow to fix backlog items.** Capture and continue.
+
+---
+
+## Output Formatting
+
+- Always show the raw script output -- it's already formatted as a table
+- After `list`, add a brief interpretation: "You have N critical items that should be addressed first."
+- After `add`, confirm what was added
+- After `stats`, highlight if there are critical items: "Heads up: N critical items need attention."
+- After `next`, show the task details clearly and ask: "Want to start working on this?"
+
+---
+
+## Error Handling
+
+- If `sqlite3` is not installed, tell the user: "The backlog requires sqlite3. Install it with your package manager (brew install sqlite3, apt install sqlite3, etc.)"
+- If the script is not found or not executable, tell the user to run the gstack setup
+- If the database is corrupted, suggest: "Try running `/backlog cleanup` and `/backlog dedup`. If issues persist, the database can be reset by deleting `~/.gstack/backlog.db`."
diff --git a/backlog/scripts/backlog.sh b/backlog/scripts/backlog.sh
new file mode 100755
index 0000000..a6f3b72
--- /dev/null
+++ b/backlog/scripts/backlog.sh
@@ -0,0 +1,507 @@
+#!/usr/bin/env bash
+# gstack Backlog Management — persistent task tracking across Claude Code sessions
+# Data stored in ~/.gstack/backlog.db (SQLite)
+#
+# Usage: backlog.sh [args]
+# backlog.sh add [--priority critical|important|suggestion] [--source review|qa|manual]
+# backlog.sh list [--status pending|completed|all] [--priority critical|important|suggestion] [--limit N]
+# backlog.sh complete
+# backlog.sh delete
+# backlog.sh search
+# backlog.sh stats
+# backlog.sh next
+# backlog.sh start
+# backlog.sh reset
+# backlog.sh get
+# backlog.sh dedup
+# backlog.sh cleanup
+
+set -e
+
+# Database location
+GSTACK_DIR="$HOME/.gstack"
+mkdir -p "$GSTACK_DIR"
+DB_PATH="$GSTACK_DIR/backlog.db"
+
+# Check if sqlite3 is available
+if ! command -v sqlite3 &> /dev/null; then
+ echo "Error: sqlite3 is required but not installed." >&2
+ echo "Install with: brew install sqlite3 (macOS) or apt install sqlite3 (Linux)" >&2
+ exit 1
+fi
+
+# Initialize database if needed
+init_db() {
+ if [ ! -f "$DB_PATH" ]; then
+ sqlite3 "$DB_PATH" << 'SQL'
+CREATE TABLE IF NOT EXISTS tasks_backlog (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ source TEXT NOT NULL DEFAULT 'manual',
+ priority TEXT NOT NULL DEFAULT 'suggestion',
+ title TEXT NOT NULL,
+ description TEXT,
+ file_path TEXT,
+ line_number INTEGER,
+ status TEXT NOT NULL DEFAULT 'pending',
+ created_at TEXT NOT NULL,
+ completed_at TEXT,
+ metadata TEXT
+);
+CREATE INDEX IF NOT EXISTS idx_backlog_status ON tasks_backlog(status);
+CREATE INDEX IF NOT EXISTS idx_backlog_priority ON tasks_backlog(priority);
+SQL
+ fi
+}
+
+add_task() {
+ local title="$1"
+ local priority="${2:-suggestion}"
+ local source="${3:-manual}"
+ local description="${4:-}"
+ local file_path="${5:-}"
+ local line_number="${6:-}"
+
+ if [ -z "$title" ]; then
+ echo "Error: title is required" >&2
+ echo "Usage: backlog.sh add [--priority critical|important|suggestion] [--source review|qa|manual]" >&2
+ exit 1
+ fi
+
+ init_db
+
+ # Dedup: skip if an identical pending task already exists
+ local escaped_title
+ escaped_title=$(echo "$title" | sed "s/'/''/g")
+ local existing
+ existing=$(sqlite3 "$DB_PATH" "SELECT id FROM tasks_backlog WHERE title = '$escaped_title' AND status = 'pending' LIMIT 1;")
+ if [ -n "$existing" ]; then
+ echo "Skipped (duplicate of #$existing): $title"
+ return 0
+ fi
+
+ local created_at
+ created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+
+ local id
+ id=$(sqlite3 "$DB_PATH" << SQL
+INSERT INTO tasks_backlog (source, priority, title, description, file_path, line_number, status, created_at)
+VALUES ('$source', '$priority', '$escaped_title',
+ $([ -n "$description" ] && echo "'$(echo "$description" | sed "s/'/''/g")'" || echo "NULL"),
+ $([ -n "$file_path" ] && echo "'$file_path'" || echo "NULL"),
+ $([ -n "$line_number" ] && echo "$line_number" || echo "NULL"),
+ 'pending', '$created_at');
+SELECT last_insert_rowid();
+SQL
+)
+
+ echo "Added task #$id: $title (priority: $priority, source: $source)"
+}
+
+list_tasks() {
+ local status_filter="${1:-pending}"
+ local priority_filter="${2:-}"
+ local limit="${3:-50}"
+
+ init_db
+
+ local where_clause="WHERE status != 'deleted'"
+
+ if [ "$status_filter" != "all" ]; then
+ where_clause="$where_clause AND status = '$status_filter'"
+ fi
+
+ if [ -n "$priority_filter" ]; then
+ where_clause="$where_clause AND priority = '$priority_filter'"
+ fi
+
+ echo "=== Tasks Backlog ==="
+ echo ""
+
+ sqlite3 -header -column "$DB_PATH" << SQL
+SELECT
+ id,
+ priority,
+ substr(title, 1, 60) as title,
+ source,
+ status,
+ date(created_at) as created
+FROM tasks_backlog
+$where_clause
+ORDER BY
+ CASE priority WHEN 'critical' THEN 1 WHEN 'important' THEN 2 WHEN 'suggestion' THEN 3 END,
+ created_at DESC
+LIMIT $limit;
+SQL
+
+ echo ""
+
+ # Show summary counts
+ local pending
+ pending=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM tasks_backlog WHERE status = 'pending'")
+ local in_progress
+ in_progress=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM tasks_backlog WHERE status = 'in_progress'")
+ local completed
+ completed=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM tasks_backlog WHERE status = 'completed'")
+
+ echo "Summary: $pending pending, $in_progress in progress, $completed completed"
+}
+
+complete_task() {
+ local id="$1"
+
+ if [ -z "$id" ]; then
+ echo "Error: task ID is required" >&2
+ echo "Usage: backlog.sh complete " >&2
+ exit 1
+ fi
+
+ init_db
+
+ local completed_at
+ completed_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+
+ local changes
+ changes=$(sqlite3 "$DB_PATH" << SQL
+UPDATE tasks_backlog SET status = 'completed', completed_at = '$completed_at' WHERE id = $id;
+SELECT changes();
+SQL
+)
+
+ if [ "$changes" -gt 0 ]; then
+ echo "Completed task #$id"
+ else
+ echo "Task #$id not found" >&2
+ exit 1
+ fi
+}
+
+delete_task() {
+ local id="$1"
+
+ if [ -z "$id" ]; then
+ echo "Error: task ID is required" >&2
+ echo "Usage: backlog.sh delete " >&2
+ exit 1
+ fi
+
+ init_db
+
+ local changes
+ changes=$(sqlite3 "$DB_PATH" << SQL
+UPDATE tasks_backlog SET status = 'deleted' WHERE id = $id;
+SELECT changes();
+SQL
+)
+
+ if [ "$changes" -gt 0 ]; then
+ echo "Deleted task #$id"
+ else
+ echo "Task #$id not found" >&2
+ exit 1
+ fi
+}
+
+search_tasks() {
+ local query="$1"
+
+ if [ -z "$query" ]; then
+ echo "Error: search query is required" >&2
+ echo "Usage: backlog.sh search " >&2
+ exit 1
+ fi
+
+ init_db
+
+ echo "=== Search Results for: $query ==="
+ echo ""
+
+ sqlite3 -header -column "$DB_PATH" << SQL
+SELECT
+ id,
+ priority,
+ substr(title, 1, 60) as title,
+ source,
+ status
+FROM tasks_backlog
+WHERE status != 'deleted'
+ AND (title LIKE '%$query%' OR description LIKE '%$query%')
+ORDER BY
+ CASE priority WHEN 'critical' THEN 1 WHEN 'important' THEN 2 WHEN 'suggestion' THEN 3 END,
+ created_at DESC
+LIMIT 50;
+SQL
+}
+
+show_stats() {
+ init_db
+
+ echo "=== Backlog Statistics ==="
+ echo ""
+
+ echo "By Status:"
+ sqlite3 "$DB_PATH" "SELECT status, COUNT(*) as count FROM tasks_backlog GROUP BY status ORDER BY count DESC;"
+ echo ""
+
+ echo "By Priority (excluding deleted):"
+ sqlite3 "$DB_PATH" "SELECT priority, COUNT(*) as count FROM tasks_backlog WHERE status != 'deleted' GROUP BY priority ORDER BY CASE priority WHEN 'critical' THEN 1 WHEN 'important' THEN 2 WHEN 'suggestion' THEN 3 END;"
+ echo ""
+
+ echo "By Source (excluding deleted):"
+ sqlite3 "$DB_PATH" "SELECT source, COUNT(*) as count FROM tasks_backlog WHERE status != 'deleted' GROUP BY source ORDER BY count DESC;"
+}
+
+get_task() {
+ local id="$1"
+
+ if [ -z "$id" ]; then
+ echo "Error: task ID is required" >&2
+ echo "Usage: backlog.sh get " >&2
+ exit 1
+ fi
+
+ init_db
+
+ sqlite3 -header -column "$DB_PATH" << SQL
+SELECT * FROM tasks_backlog WHERE id = $id;
+SQL
+}
+
+next_task() {
+ init_db
+
+ local result
+ result=$(sqlite3 -json "$DB_PATH" << 'SQL'
+SELECT id, source, priority, title, description, file_path, line_number, status, created_at, metadata
+FROM tasks_backlog
+WHERE status = 'pending'
+ORDER BY
+ CASE priority
+ WHEN 'critical' THEN 1
+ WHEN 'important' THEN 2
+ WHEN 'suggestion' THEN 3
+ END,
+ created_at ASC
+LIMIT 1;
+SQL
+)
+
+ if [ -z "$result" ] || [ "$result" = "[]" ]; then
+ echo "{}"
+ return 1
+ fi
+
+ # sqlite3 -json returns an array; extract the first element
+ if command -v jq &> /dev/null; then
+ echo "$result" | jq -c '.[0]'
+ else
+ # Fallback: return the raw array if jq is not available
+ echo "$result"
+ fi
+}
+
+start_task() {
+ local id="$1"
+
+ if [ -z "$id" ]; then
+ echo "Error: task ID is required" >&2
+ echo "Usage: backlog.sh start " >&2
+ exit 1
+ fi
+
+ init_db
+
+ local changes
+ changes=$(sqlite3 "$DB_PATH" << SQL
+UPDATE tasks_backlog SET status = 'in_progress' WHERE id = $id AND status = 'pending';
+SELECT changes();
+SQL
+)
+
+ if [ "$changes" -gt 0 ]; then
+ echo "Started task #$id"
+ else
+ echo "Task #$id not found or not in pending status" >&2
+ exit 1
+ fi
+}
+
+reset_task() {
+ local id="$1"
+
+ if [ -z "$id" ]; then
+ echo "Error: task ID is required" >&2
+ echo "Usage: backlog.sh reset " >&2
+ exit 1
+ fi
+
+ init_db
+
+ local changes
+ changes=$(sqlite3 "$DB_PATH" << SQL
+UPDATE tasks_backlog SET status = 'pending' WHERE id = $id AND status = 'in_progress';
+SELECT changes();
+SQL
+)
+
+ if [ "$changes" -gt 0 ]; then
+ echo "Reset task #$id to pending"
+ else
+ echo "Task #$id not found or not in_progress" >&2
+ exit 1
+ fi
+}
+
+dedup_tasks() {
+ init_db
+
+ # Find and remove duplicate pending tasks (keep lowest ID)
+ local removed
+ removed=$(sqlite3 "$DB_PATH" << 'SQL'
+DELETE FROM tasks_backlog
+WHERE id NOT IN (
+ SELECT MIN(id) FROM tasks_backlog
+ WHERE status = 'pending'
+ GROUP BY title
+)
+AND status = 'pending'
+AND id NOT IN (
+ SELECT MIN(id) FROM tasks_backlog
+ WHERE status = 'pending'
+ GROUP BY title
+);
+SELECT changes();
+SQL
+)
+
+ echo "Removed $removed duplicate pending tasks"
+}
+
+cleanup_garbage() {
+ init_db
+
+ # Remove entries that are clearly code fragments, not real tasks
+ local removed
+ removed=$(sqlite3 "$DB_PATH" << 'SQL'
+UPDATE tasks_backlog SET status = 'deleted'
+WHERE status = 'pending'
+AND (
+ length(title) < 10
+ OR title LIKE '%${%'
+ OR title LIKE '%$(%'
+ OR title LIKE '%\${NC}%'
+ OR title LIKE '%2>/dev/null%'
+ OR title LIKE '%]; then%'
+ OR title LIKE '%_FILES%'
+ OR title LIKE '%...${%'
+ OR title LIKE '%CHANGEME%'
+ OR title GLOB '*[|]*[|]*'
+);
+SELECT changes();
+SQL
+)
+
+ echo "Cleaned up $removed garbage entries"
+}
+
+show_help() {
+ echo "gstack Backlog — Persistent Task Tracking"
+ echo ""
+ echo "Usage: backlog.sh [args]"
+ echo ""
+ echo "Commands:"
+ echo " add [options] - Add a new task (skips duplicates)"
+ echo " --priority - critical, important, or suggestion (default: suggestion)"
+ echo " --source - review, qa, or manual (default: manual)"
+ echo " --description - Optional description"
+ echo " --file - Optional file path reference"
+ echo " --line - Optional line number reference"
+ echo ""
+ echo " list [options] - List tasks"
+ echo " --status - pending, completed, or all (default: pending)"
+ echo " --priority - Filter by priority"
+ echo " --limit - Limit results (default: 50)"
+ echo ""
+ echo " get - Get full details of a task"
+ echo " complete - Mark task as completed"
+ echo " delete - Delete a task (soft delete)"
+ echo " search - Search tasks by title or description"
+ echo " stats - Show backlog statistics"
+ echo " next - Get highest-priority pending task (JSON)"
+ echo " start - Mark task as in_progress"
+ echo " reset - Reset in_progress task back to pending"
+ echo " dedup - Remove duplicate pending tasks"
+ echo " cleanup - Remove garbage entries (code fragments)"
+ echo ""
+ echo "Examples:"
+ echo " backlog.sh add 'Add input validation to API' --priority important --source review"
+ echo " backlog.sh list --status pending --priority critical"
+ echo " backlog.sh complete 42"
+ echo " backlog.sh search 'validation'"
+ echo " backlog.sh dedup"
+ echo ""
+ echo "Database: $DB_PATH"
+}
+
+# Parse command and arguments
+command="${1:-}"
+shift || true
+
+case "$command" in
+ add)
+ # Parse options
+ title=""
+ priority="suggestion"
+ source="manual"
+ description=""
+ file_path=""
+ line_number=""
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --priority) priority="$2"; shift 2 ;;
+ --source) source="$2"; shift 2 ;;
+ --description) description="$2"; shift 2 ;;
+ --file) file_path="$2"; shift 2 ;;
+ --line) line_number="$2"; shift 2 ;;
+ *)
+ if [ -z "$title" ]; then
+ title="$1"
+ else
+ title="$title $1"
+ fi
+ shift
+ ;;
+ esac
+ done
+
+ add_task "$title" "$priority" "$source" "$description" "$file_path" "$line_number"
+ ;;
+ list)
+ status="pending"
+ priority=""
+ limit="50"
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --status) status="$2"; shift 2 ;;
+ --priority) priority="$2"; shift 2 ;;
+ --limit) limit="$2"; shift 2 ;;
+ *) shift ;;
+ esac
+ done
+
+ list_tasks "$status" "$priority" "$limit"
+ ;;
+ get) get_task "$1" ;;
+ complete) complete_task "$1" ;;
+ delete) delete_task "$1" ;;
+ search) search_tasks "$*" ;;
+ stats) show_stats ;;
+ next) next_task ;;
+ start) start_task "$1" ;;
+ reset) reset_task "$1" ;;
+ dedup) dedup_tasks ;;
+ cleanup) cleanup_garbage ;;
+ -h|--help|help|"") show_help ;;
+ *) echo "Unknown command: $command"; show_help; exit 1 ;;
+esac