Update beads issue tracking state and expand the GitLab TODOs notifications integration design document with additional implementation details. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
18 KiB
plan, title, status, iteration, target_iterations, beads_revision, related_plans, created, updated, audit_revision
| plan | title | status | iteration | target_iterations | beads_revision | related_plans | created | updated | audit_revision |
|---|---|---|---|---|---|---|---|---|---|
| true | GitLab TODOs Integration | proposed | 4 | 4 | 1 | 2026-02-23 | 2026-02-26 | 4 |
GitLab TODOs Integration
Summary
Add GitLab TODO support to lore. Todos are fetched during sync, stored locally, and surfaced through a standalone lore todos command and integration into the lore me dashboard.
Scope: Read-only. No mark-as-done operations.
Workflows
Workflow 1: Morning Triage (Human)
- User runs
lore meto see personal dashboard - Summary header shows "5 pending todos" alongside issue/MR counts
- Todos section groups items: 2 Assignments, 2 Mentions, 1 Approval Required
- User scans Assignments — sees issue #42 assigned by @manager
- User runs
lore todosfor full detail with body snippets - User clicks target URL to address highest-priority item
- After marking done in GitLab, next
lore syncremoves it locally
Workflow 2: Agent Polling (Robot Mode)
- Agent runs
lore --robot healthas pre-flight check - Agent runs
lore --robot me --fields minimalfor dashboard - Agent extracts
pending_todo_countfrom summary — if 0, skip todos - If count > 0, agent runs
lore --robot todos - Agent iterates
data.todos[], filtering byactiontype - Agent prioritizes
approval_requiredandbuild_failedfor immediate attention - Agent logs external todos (
is_external: true) for manual review
Workflow 3: Cross-Project Visibility
- User is mentioned in a project they don't sync (e.g., company-wide repo)
lore syncfetches the todo anyway (account-wide fetch)lore todosshows item with[external]indicator and project path- User can still click target URL to view in GitLab
- Target title may be unavailable — graceful fallback to "Untitled"
Acceptance Criteria
Behavioral contract. Each AC is a single testable statement.
Storage
| ID | Behavior |
|---|---|
| AC-1 | Todos are persisted locally in SQLite |
| AC-2 | Each todo is uniquely identified by its GitLab todo ID |
| AC-3 | Todos from non-synced projects are stored with their project path |
Sync
| ID | Behavior |
|---|---|
| AC-4 | lore sync fetches all pending todos from GitLab |
| AC-5 | Sync fetches todos account-wide, not per-project |
| AC-6 | Todos marked done in GitLab are removed locally on next sync |
| AC-7 | Transient sync errors do not delete valid local todos |
| AC-8 | lore sync --no-todos skips todo fetching |
| AC-9 | Sync logs todo statistics (fetched, inserted, updated, deleted) |
lore todos Command
| ID | Behavior |
|---|---|
| AC-10 | lore todos displays all pending todos |
| AC-11 | Todos are grouped by action type: Assignments, Mentions, Approvals, Build Issues |
| AC-12 | Each todo shows: target title, project path, author, age |
| AC-13 | Non-synced project todos display [external] indicator |
| AC-14 | lore todos --limit N limits output to N todos |
| AC-15 | lore --robot todos returns JSON with standard {ok, data, meta} envelope |
| AC-16 | lore --robot todos --fields minimal returns reduced field set |
| AC-17 | todo and td are recognized as aliases for todos |
lore me Integration
| ID | Behavior |
|---|---|
| AC-18 | lore me summary includes pending todo count |
| AC-19 | lore me includes a todos section in the full dashboard |
| AC-20 | lore me --todos shows only the todos section |
| AC-21 | Todos are NOT filtered by --project flag (always account-wide) |
| AC-22 | Warning is displayed if --project is passed with --todos |
| AC-23 | Todo events appear in the activity feed for local entities |
Action Types
| ID | Behavior |
|---|---|
| AC-24 | Core actions are displayed: assigned, mentioned, directly_addressed, approval_required, build_failed, unmergeable |
| AC-25 | Niche actions are stored but not displayed: merge_train_removed, member_access_requested, marked |
Attention State
| ID | Behavior |
|---|---|
| AC-26 | Todos do not affect attention state calculation |
| AC-27 | Todos do not appear in "since last check" cursor-based inbox |
Error Handling
| ID | Behavior |
|---|---|
| AC-28 | 403 Forbidden on todos API logs warning and continues sync |
| AC-29 | 429 Rate Limited respects Retry-After header |
| AC-30 | Malformed todo JSON logs warning, skips that item, and disables purge for that sync |
Documentation
| ID | Behavior |
|---|---|
| AC-31 | lore todos appears in CLI help |
| AC-32 | lore robot-docs includes todos schema |
| AC-33 | CLAUDE.md documents the todos command |
Quality
| ID | Behavior |
|---|---|
| AC-34 | All quality gates pass: check, clippy, fmt, test |
Architecture
Designed to fulfill the acceptance criteria above.
Module Structure
src/
├── gitlab/
│ ├── client.rs # fetch_todos() method (AC-4, AC-5)
│ └── types.rs # GitLabTodo struct
├── ingestion/
│ └── todos.rs # sync_todos(), purge-safe deletion (AC-6, AC-7)
├── cli/commands/
│ ├── todos.rs # lore todos command (AC-10-17)
│ └── me/
│ ├── types.rs # MeTodo, extend MeSummary (AC-18)
│ └── queries.rs # query_todos() (AC-19, AC-23)
└── core/
└── db.rs # Migration 028 (AC-1, AC-2, AC-3)
Data Flow
GitLab API Local SQLite CLI Output
─────────── ──────────── ──────────
GET /api/v4/todos → todos table → lore todos
(account-wide) (purge-safe sync) lore me --todos
Key Design Decisions
| Decision | Rationale | ACs |
|---|---|---|
| Account-wide fetch | GitLab todos API is user-scoped, not project-scoped | AC-5, AC-21 |
| Purge-safe deletion | Transient errors should not delete valid data | AC-7 |
| Separate from attention | Todos are notifications, not engagement signals | AC-26, AC-27 |
| Store all actions, display core | Future-proofs for new action types | AC-24, AC-25 |
Existing Code to Extend
| Type | Location | Extension |
|---|---|---|
MeSummary |
src/cli/commands/me/types.rs |
Add pending_todo_count field |
ActivityEventType |
src/cli/commands/me/types.rs |
Add Todo variant |
MeDashboard |
src/cli/commands/me/types.rs |
Add todos: Vec<MeTodo> field |
SyncArgs |
src/cli/mod.rs |
Add --no-todos flag |
MeArgs |
src/cli/mod.rs |
Add --todos flag |
Implementation Specifications
Each IMP section details HOW to fulfill specific ACs.
IMP-1: Database Schema
Fulfills: AC-1, AC-2, AC-3
Migration 028:
CREATE TABLE todos (
id INTEGER PRIMARY KEY,
gitlab_todo_id INTEGER NOT NULL UNIQUE,
project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
gitlab_project_id INTEGER,
target_type TEXT NOT NULL,
target_id TEXT,
target_iid INTEGER,
target_url TEXT NOT NULL,
target_title TEXT,
action_name TEXT NOT NULL,
author_id INTEGER,
author_username TEXT,
body TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
synced_at INTEGER NOT NULL,
sync_generation INTEGER NOT NULL DEFAULT 0,
project_path TEXT
);
CREATE INDEX idx_todos_action_created ON todos(action_name, created_at DESC);
CREATE INDEX idx_todos_target ON todos(target_type, target_id);
CREATE INDEX idx_todos_created ON todos(created_at DESC);
CREATE INDEX idx_todos_sync_gen ON todos(sync_generation);
CREATE INDEX idx_todos_gitlab_project ON todos(gitlab_project_id);
CREATE INDEX idx_todos_target_lookup ON todos(target_type, project_id, target_iid);
Notes:
project_idnullable for non-synced projects (AC-3)gitlab_project_idnullable — TODO targets include non-project entities (Namespace, etc.)- No
statecolumn — we only store pending todos sync_generationenables two-generation grace purge (AC-7)
IMP-2: GitLab API Client
Fulfills: AC-4, AC-5
Endpoint: GET /api/v4/todos?state=pending
Types to add in src/gitlab/types.rs:
#[derive(Debug, Deserialize)]
pub struct GitLabTodo {
pub id: i64,
pub project: Option<GitLabTodoProject>,
pub author: Option<GitLabTodoAuthor>,
pub action_name: String,
pub target_type: String,
pub target: Option<GitLabTodoTarget>,
pub target_url: String,
pub body: Option<String>,
pub state: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Deserialize)]
pub struct GitLabTodoProject {
pub id: i64,
pub path_with_namespace: String,
}
#[derive(Debug, Deserialize)]
pub struct GitLabTodoTarget {
pub id: serde_json::Value, // i64 or String (commit SHA)
pub iid: Option<i64>,
pub title: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct GitLabTodoAuthor {
pub id: i64,
pub username: String,
}
Client method in src/gitlab/client.rs:
pub fn fetch_todos(&self) -> impl Stream<Item = Result<GitLabTodo>> {
self.paginate("/api/v4/todos?state=pending")
}
IMP-3: Sync Pipeline Integration
Fulfills: AC-4, AC-5, AC-6, AC-7, AC-8, AC-9
New file: src/ingestion/todos.rs
Sync position: Account-wide step after per-project sync and status enrichment.
Sync order:
1. Issues (per project)
2. MRs (per project)
3. Status enrichment (account-wide GraphQL)
4. Todos (account-wide REST) ← NEW
Purge-safe deletion pattern:
pub struct TodoSyncResult {
pub fetched: usize,
pub upserted: usize,
pub deleted: usize,
pub generation: i64,
pub purge_allowed: bool,
}
pub fn sync_todos(conn: &Connection, client: &GitLabClient) -> Result<TodoSyncResult> {
// 1. Get next generation
let generation: i64 = conn.query_row(
"SELECT COALESCE(MAX(sync_generation), 0) + 1 FROM todos",
[], |r| r.get(0)
)?;
let mut fetched = 0;
let mut purge_allowed = true;
// 2. Fetch and upsert all todos
for result in client.fetch_todos()? {
match result {
Ok(todo) => {
upsert_todo_guarded(conn, &todo, generation)?;
fetched += 1;
}
Err(e) => {
// Malformed JSON: log warning, skip item, disable purge
warn!("Skipping malformed todo: {e}");
purge_allowed = false;
}
}
}
// 3. Two-generation grace purge: delete only if missing for 2+ consecutive syncs
// This protects against pagination drift (new todos inserted during traversal)
let deleted = if purge_allowed {
conn.execute("DELETE FROM todos WHERE sync_generation < ? - 1", [generation])?
} else {
0
};
Ok(TodoSyncResult { fetched, upserted: fetched, deleted, generation, purge_allowed })
}
Concurrent-safe upsert:
INSERT INTO todos (..., sync_generation) VALUES (?, ..., ?)
ON CONFLICT(gitlab_todo_id) DO UPDATE SET
...,
sync_generation = excluded.sync_generation,
synced_at = excluded.synced_at
WHERE excluded.sync_generation >= todos.sync_generation;
"Success" for purge (all must be true):
- Every page fetch completed without error
- Every todo JSON decoded successfully (any decode failure sets
purge_allowed=false) - Pagination traversal completed (not interrupted)
- Response was not 401/403
- Zero todos IS valid for purge when above conditions met
Two-generation grace purge:
Todos are deleted only if missing for 2 consecutive successful syncs (sync_generation < current - 1).
This protects against false deletions from pagination drift (new todos inserted during traversal).
IMP-4: Project Path Extraction
Fulfills: AC-3, AC-13
use once_cell::sync::Lazy;
use regex::Regex;
pub fn extract_project_path(url: &str) -> Option<&str> {
static RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"https?://[^/]+/(.+?)/-/(?:issues|merge_requests|epics|commits)/")
.expect("valid regex")
});
RE.captures(url)
.and_then(|c| c.get(1))
.map(|m| m.as_str())
}
Usage: Prefer project.path_with_namespace from API when available. Fall back to URL extraction for external projects.
IMP-5: lore todos Command
Fulfills: AC-10, AC-11, AC-12, AC-13, AC-14, AC-15, AC-16, AC-17
New file: src/cli/commands/todos.rs
Args:
#[derive(Parser)]
#[command(alias = "todo")]
pub struct TodosArgs {
#[arg(short = 'n', long)]
pub limit: Option<usize>,
}
Autocorrect aliases in src/cli/mod.rs:
("td", "todos"),
("todo", "todos"),
Action type grouping:
| Group | Actions |
|---|---|
| Assignments | assigned |
| Mentions | mentioned, directly_addressed |
| Approvals | approval_required |
| Build Issues | build_failed, unmergeable |
Robot mode schema:
{
"ok": true,
"data": {
"todos": [{
"id": 123,
"gitlab_todo_id": 456,
"action": "mentioned",
"target_type": "Issue",
"target_iid": 42,
"target_title": "Fix login bug",
"target_url": "https://...",
"project_path": "group/repo",
"author_username": "jdoe",
"body": "Hey @you, can you look at this?",
"created_at_iso": "2026-02-20T10:00:00Z",
"is_external": false
}],
"counts": {
"total": 8,
"assigned": 2,
"mentioned": 5,
"approval_required": 1,
"build_failed": 0,
"unmergeable": 0,
"other": 0
}
},
"meta": {"elapsed_ms": 42}
}
Minimal fields: gitlab_todo_id, action, target_type, target_iid, project_path, is_external
IMP-6: lore me Integration
Fulfills: AC-18, AC-19, AC-20, AC-21, AC-22, AC-23
Types to add/extend in src/cli/commands/me/types.rs:
// EXTEND
pub struct MeSummary {
// ... existing fields ...
pub pending_todo_count: usize, // ADD
}
// EXTEND
pub enum ActivityEventType {
// ... existing variants ...
Todo, // ADD
}
// EXTEND
pub struct MeDashboard {
// ... existing fields ...
pub todos: Vec<MeTodo>, // ADD
}
// NEW
pub struct MeTodo {
pub id: i64,
pub gitlab_todo_id: i64,
pub action: String,
pub target_type: String,
pub target_iid: Option<i64>,
pub target_title: Option<String>,
pub target_url: String,
pub project_path: String,
pub author_username: Option<String>,
pub body: Option<String>,
pub created_at: i64,
pub is_external: bool,
}
Warning for --project with --todos (AC-22):
if args.todos && args.project.is_some() {
eprintln!("Warning: Todos are account-wide; project filter not applied");
}
IMP-7: Error Handling
Fulfills: AC-28, AC-29, AC-30
| Error | Behavior |
|---|---|
| 403 Forbidden | Log warning, skip todo sync, continue with other entities |
| 429 Rate Limited | Respect Retry-After header using existing retry policy |
| Malformed JSON | Log warning with todo ID, skip item, set purge_allowed=false, continue batch |
Rationale for purge disable on malformed JSON: If we can't decode a todo, we don't know its gitlab_todo_id. Without that, we might accidentally purge a valid todo that was simply malformed in transit. Disabling purge for that sync is the safe choice.
IMP-8: Test Fixtures
Fulfills: AC-34
Location: tests/fixtures/todos/
todos_pending.json:
[
{
"id": 102,
"project": {"id": 2, "path_with_namespace": "diaspora/client"},
"author": {"id": 1, "username": "admin"},
"action_name": "mentioned",
"target_type": "Issue",
"target": {"id": 11, "iid": 4, "title": "Inventory system"},
"target_url": "https://gitlab.example.com/diaspora/client/-/issues/4",
"body": "@user please review",
"state": "pending",
"created_at": "2026-02-20T10:00:00.000Z",
"updated_at": "2026-02-20T10:00:00.000Z"
}
]
todos_empty.json: []
todos_commit_target.json: (target.id is string SHA)
todos_niche_actions.json: (merge_train_removed, etc.)
Rollout Slices
Dependency Graph
Slice A ──────► Slice B ──────┬──────► Slice C
(Schema) (Sync) │ (`lore todos`)
│
└──────► Slice D
(`lore me`)
Slice C ───┬───► Slice E
Slice D ───┘ (Polish)
Slice A: Schema + Client
ACs: AC-1, AC-2, AC-3, AC-4, AC-5 IMPs: IMP-1, IMP-2, IMP-4 Deliverable: Migration + client method + deserialization tests pass
Slice B: Sync Integration
ACs: AC-6, AC-7, AC-8, AC-9, AC-28, AC-29, AC-30
IMPs: IMP-3, IMP-7
Deliverable: lore sync fetches todos; --no-todos works
Slice C: lore todos Command
ACs: AC-10, AC-11, AC-12, AC-13, AC-14, AC-15, AC-16, AC-17, AC-24, AC-25
IMPs: IMP-5
Deliverable: lore todos and lore --robot todos work
Slice D: lore me Integration
ACs: AC-18, AC-19, AC-20, AC-21, AC-22, AC-23, AC-26, AC-27
IMPs: IMP-6
Deliverable: lore me --todos works; summary shows count
Slice E: Polish
ACs: AC-31, AC-32, AC-33, AC-34 IMPs: IMP-8 Deliverable: Docs updated; all quality gates pass
Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Write operations | Read-only | Complexity; glab handles writes |
| Storage | SQLite | Consistent with existing architecture |
| Project filter | Account-wide only | GitLab API is user-scoped |
| Action type display | Core only | Reduce noise; store all for future |
| Attention state | Separate signal | Todos are notifications, not engagement |
| History | Pending only | Simplicity; done todos have no value locally |
| Grouping | By action type | Matches GitLab UI; aids triage |
| Purge strategy | Two-generation grace | Protects against pagination drift during sync |
Out of Scope
- Write operations (mark as done)
- Done todo history tracking
- Filters beyond
--limit - Todo-based attention state boosting
- Notification settings API