AgentSkillsCN

progress-tracker

基于 SQLite 的长期任务进度追踪。仅在用户明确输入“sqlite-tracker-init”或“setup agent sqlite tracker”时使用。

SKILL.md
--- frontmatter
name: progress-tracker
description: "SQLite-based progress tracking for long-lived tasks. ONLY use when user explicitly says 'sqlite-tracker-init' or 'setup agent sqlite tracker'."

Progress Tracker Skill

Set up SQLite-based progress tracking for long-lived tasks that span multiple agent sessions.

Why SQLite Over Markdown

  • Agents are excellent at SQL - structured queries, updates, joins
  • Markdown pollutes context windows with raw text
  • SQL allows precise retrieval: "get incomplete tasks", "show blockers", "last 5 updates"
  • Schema enforces structure; markdown drifts
  • Single source of truth with ACID guarantees

Prerequisites: AGENTS.md File

Before setting up progress tracking, check for an AGENTS.md file in the project root.

If AGENTS.md Exists

Read it to understand:

  • Project purpose and goals
  • Key components and architecture
  • Conventions and constraints
  • What success looks like

If AGENTS.md Does NOT Exist

Stop and prompt the user:

code
I need to set up progress tracking, but this project lacks an AGENTS.md file.

This file helps agents understand the project context. Please create one with:

1. **Project Overview**: What is this project? What problem does it solve?
2. **Key Goals**: What are we trying to achieve?
3. **Architecture**: Main components, tech stack, important patterns
4. **Conventions**: Coding standards, naming, file structure
5. **Current State**: What's done, what's in progress, what's blocked

Would you like me to create a template AGENTS.md for you to fill in?

Setup Process

1. Create the PEP 723 Tracker Script

Create .agent/tracker.py:

python
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "sqlite-utils>=3.35",
# ]
# ///
"""
Agent progress tracker - SQLite-based task and context management.

Usage:
    uv run .agent/tracker.py init                          # Initialize DB
    uv run .agent/tracker.py add "task description"        # Add task
    uv run .agent/tracker.py start <id>                    # Mark in-progress
    uv run .agent/tracker.py done <id> "completion note"   # Mark complete
    uv run .agent/tracker.py block <id> "blocker reason"   # Mark blocked
    uv run .agent/tracker.py note <id> "note text"         # Add note to task
    uv run .agent/tracker.py context                       # Show handoff context
    uv run .agent/tracker.py status                        # Show all tasks
    uv run .agent/tracker.py sql "SELECT * FROM tasks"     # Raw SQL query
"""
from __future__ import annotations

import sys
from datetime import datetime, timezone
from pathlib import Path

from sqlite_utils import Database

DB_PATH = Path(".agent/progress.db")


def get_db() -> Database:
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    return Database(DB_PATH)


def init_db() -> None:
    db = get_db()
    
    db.executescript("""
        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY,
            description TEXT NOT NULL,
            status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'done', 'blocked')),
            priority TEXT DEFAULT 'medium' CHECK(priority IN ('high', 'medium', 'low')),
            created_at TEXT DEFAULT (datetime('now')),
            updated_at TEXT DEFAULT (datetime('now')),
            completed_at TEXT,
            completion_note TEXT,
            blocker TEXT,
            parent_id INTEGER REFERENCES tasks(id)
        );
        
        CREATE TABLE IF NOT EXISTS notes (
            id INTEGER PRIMARY KEY,
            task_id INTEGER REFERENCES tasks(id),
            content TEXT NOT NULL,
            created_at TEXT DEFAULT (datetime('now'))
        );
        
        CREATE TABLE IF NOT EXISTS decisions (
            id INTEGER PRIMARY KEY,
            description TEXT NOT NULL,
            rationale TEXT,
            created_at TEXT DEFAULT (datetime('now'))
        );
        
        CREATE TABLE IF NOT EXISTS context (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL,
            updated_at TEXT DEFAULT (datetime('now'))
        );
        
        CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
        CREATE INDEX IF NOT EXISTS idx_notes_task ON notes(task_id);
    """)
    print("Initialized progress tracker database")


def add_task(description: str, priority: str = "medium", parent_id: int | None = None) -> None:
    db = get_db()
    task_id = db["tasks"].insert({
        "description": description,
        "priority": priority,
        "parent_id": parent_id,
    }).last_pk
    print(f"Added task #{task_id}: {description}")


def update_status(task_id: int, status: str, note: str | None = None) -> None:
    db = get_db()
    update = {"status": status, "updated_at": datetime.now(timezone.utc).isoformat()}
    
    if status == "done":
        update["completed_at"] = update["updated_at"]
        if note:
            update["completion_note"] = note
    elif status == "blocked" and note:
        update["blocker"] = note
    
    db["tasks"].update(task_id, update)
    print(f"Task #{task_id} -> {status}" + (f": {note}" if note else ""))


def add_note(task_id: int, content: str) -> None:
    db = get_db()
    db["notes"].insert({"task_id": task_id, "content": content})
    print(f"Added note to task #{task_id}")


def show_status() -> None:
    db = get_db()
    
    for status in ["in_progress", "blocked", "pending", "done"]:
        tasks = list(db["tasks"].rows_where("status = ?", [status], order_by="-priority"))
        if tasks:
            print(f"\n## {status.upper()} ({len(tasks)})")
            for t in tasks:
                notes = list(db["notes"].rows_where("task_id = ?", [t["id"]], order_by="-created_at", limit=1))
                extra = ""
                if t.get("blocker"):
                    extra = f" [BLOCKED: {t['blocker']}]"
                elif notes:
                    extra = f" (last note: {notes[0]['content'][:50]}...)" if len(notes[0]['content']) > 50 else f" (note: {notes[0]['content']})"
                print(f"  [{t['priority'][0].upper()}] #{t['id']}: {t['description']}{extra}")


def show_context() -> None:
    """Generate handoff context for next agent session."""
    db = get_db()
    
    print("# Agent Handoff Context")
    print(f"Generated: {datetime.now(timezone.utc).isoformat()}\n")
    
    # Current focus
    in_progress = list(db["tasks"].rows_where("status = ?", ["in_progress"]))
    if in_progress:
        print("## Currently In Progress")
        for t in in_progress:
            print(f"- #{t['id']}: {t['description']}")
            notes = list(db["notes"].rows_where("task_id = ?", [t["id"]], order_by="-created_at", limit=3))
            for n in notes:
                print(f"  - {n['content']}")
    
    # Blockers
    blocked = list(db["tasks"].rows_where("status = ?", ["blocked"]))
    if blocked:
        print("\n## Blocked Tasks")
        for t in blocked:
            print(f"- #{t['id']}: {t['description']}")
            print(f"  Blocker: {t['blocker']}")
    
    # Recent decisions
    decisions = list(db["decisions"].rows_where(order_by="-created_at", limit=5))
    if decisions:
        print("\n## Recent Decisions")
        for d in decisions:
            print(f"- {d['description']}")
            if d.get("rationale"):
                print(f"  Rationale: {d['rationale']}")
    
    # Pending high priority
    high_pending = list(db["tasks"].rows_where("status = ? AND priority = ?", ["pending", "high"]))
    if high_pending:
        print("\n## High Priority Pending")
        for t in high_pending:
            print(f"- #{t['id']}: {t['description']}")
    
    # Custom context
    ctx = list(db["context"].rows)
    if ctx:
        print("\n## Project Context")
        for c in ctx:
            print(f"- {c['key']}: {c['value']}")


def run_sql(query: str) -> None:
    db = get_db()
    try:
        for row in db.execute(query).fetchall():
            print(row)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


def main() -> None:
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)
    
    cmd = sys.argv[1]
    args = sys.argv[2:]
    
    match cmd:
        case "init":
            init_db()
        case "add":
            if not args:
                print("Usage: add <description> [priority]", file=sys.stderr)
                sys.exit(1)
            add_task(args[0], args[1] if len(args) > 1 else "medium")
        case "start":
            update_status(int(args[0]), "in_progress")
        case "done":
            update_status(int(args[0]), "done", args[1] if len(args) > 1 else None)
        case "block":
            update_status(int(args[0]), "blocked", args[1] if len(args) > 1 else None)
        case "note":
            add_note(int(args[0]), args[1])
        case "status":
            show_status()
        case "context":
            show_context()
        case "sql":
            run_sql(args[0])
        case _:
            print(f"Unknown command: {cmd}", file=sys.stderr)
            sys.exit(1)


if __name__ == "__main__":
    main()

2. Initialize the Database

bash
uv run .agent/tracker.py init

3. Add .agent to .gitignore (Optional)

If progress tracking should be local-only:

bash
echo ".agent/" >> .gitignore

Or commit it if progress should persist across clones.

Agent Workflow

Starting a Session

bash
# Check current state
uv run .agent/tracker.py context

# See all tasks
uv run .agent/tracker.py status

During Work

bash
# Pick up a task
uv run .agent/tracker.py start 3

# Add progress notes
uv run .agent/tracker.py note 3 "Implemented base class, need to add error handling"

# Hit a blocker
uv run .agent/tracker.py block 3 "Need API credentials from user"

# Complete a task
uv run .agent/tracker.py done 3 "Implemented with full test coverage"

Ending a Session

bash
# Generate handoff context for next agent
uv run .agent/tracker.py context

SQL Patterns for Agents

Get actionable tasks

sql
SELECT id, description, priority FROM tasks 
WHERE status IN ('pending', 'in_progress') 
ORDER BY 
  CASE priority WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,
  created_at;

Get task with full history

sql
SELECT t.*, GROUP_CONCAT(n.content, ' | ') as notes
FROM tasks t
LEFT JOIN notes n ON t.id = n.task_id
WHERE t.id = ?
GROUP BY t.id;

Find stale in-progress tasks

sql
SELECT * FROM tasks 
WHERE status = 'in_progress' 
AND updated_at < datetime('now', '-1 hour');

Record a decision

sql
INSERT INTO decisions (description, rationale) 
VALUES ('Use SQLite over Postgres', 'Zero deployment overhead, sufficient for tracking');

Set project context

sql
INSERT OR REPLACE INTO context (key, value, updated_at) 
VALUES ('tech_stack', 'Python 3.11, FastAPI, SQLite', datetime('now'));

Schema Reference

tasks

ColumnTypeDescription
idINTEGERPrimary key
descriptionTEXTTask description
statusTEXTpending, in_progress, done, blocked
priorityTEXThigh, medium, low
created_atTEXTISO timestamp
updated_atTEXTISO timestamp
completed_atTEXTWhen marked done
completion_noteTEXTHow it was completed
blockerTEXTWhy it's blocked
parent_idINTEGERFor subtasks

notes

ColumnTypeDescription
idINTEGERPrimary key
task_idINTEGERFK to tasks
contentTEXTNote content
created_atTEXTISO timestamp

decisions

ColumnTypeDescription
idINTEGERPrimary key
descriptionTEXTWhat was decided
rationaleTEXTWhy
created_atTEXTISO timestamp

context

ColumnTypeDescription
keyTEXTPrimary key
valueTEXTContext value
updated_atTEXTISO timestamp

Best Practices

  1. Always run context at session start - understand what's in progress
  2. Use notes liberally - future agents will thank you
  3. Record decisions - why matters more than what
  4. Mark blockers clearly - next agent knows what to ask
  5. Complete tasks with notes - explain how, not just that