chore: update beads and GitLab TODOs integration plan
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>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
bd-1tv8
|
bd-8con
|
||||||
|
|||||||
@@ -2,235 +2,433 @@
|
|||||||
plan: true
|
plan: true
|
||||||
title: "GitLab TODOs Integration"
|
title: "GitLab TODOs Integration"
|
||||||
status: proposed
|
status: proposed
|
||||||
iteration: 1
|
iteration: 4
|
||||||
target_iterations: 3
|
target_iterations: 4
|
||||||
beads_revision: 1
|
beads_revision: 1
|
||||||
related_plans: []
|
related_plans: []
|
||||||
created: 2026-02-23
|
created: 2026-02-23
|
||||||
updated: 2026-02-23
|
updated: 2026-02-26
|
||||||
|
audit_revision: 4
|
||||||
---
|
---
|
||||||
|
|
||||||
# GitLab TODOs Integration
|
# GitLab TODOs Integration
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Add GitLab TODO support to lore. Todos are fetched during sync, stored locally, and surfaced through:
|
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.
|
||||||
1. A new `--todos` section in `lore me`
|
|
||||||
2. Enrichment of the activity feed in `lore me`
|
|
||||||
3. A standalone `lore todos` command
|
|
||||||
|
|
||||||
**Scope:** Read-only. No mark-as-done operations.
|
**Scope:** Read-only. No mark-as-done operations.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Design Decisions (from interview)
|
## Workflows
|
||||||
|
|
||||||
| Decision | Choice |
|
### Workflow 1: Morning Triage (Human)
|
||||||
|----------|--------|
|
|
||||||
| Write operations | **Read-only** — no mark-as-done |
|
|
||||||
| Storage | **Persist locally** in SQLite |
|
|
||||||
| Integration | Three-way: activity enrichment + `--todos` flag + `lore todos` |
|
|
||||||
| Action types | Core only: assigned, mentioned, directly_addressed, approval_required, build_failed, unmergeable |
|
|
||||||
| Niche actions | Skip display (but store): merge_train_removed, member_access_requested, marked |
|
|
||||||
| Project filter | **Always account-wide** — `--project` does NOT filter todos |
|
|
||||||
| Sync timing | During normal `lore sync` |
|
|
||||||
| Non-synced projects | Include with `[external]` indicator |
|
|
||||||
| Attention state | **Separate signal** — todos don't boost attention |
|
|
||||||
| Summary header | Include pending todo count |
|
|
||||||
| Grouping | By action type: Assignments \| Mentions \| Approvals \| Build Issues |
|
|
||||||
| History | **Pending only** — done todos not tracked |
|
|
||||||
| `lore todos` filters | **None** — show all pending, simple |
|
|
||||||
| Robot mode | Yes, standard envelope |
|
|
||||||
| Target types | All GitLab supports (Issue, MR, Epic, Commit, etc.) |
|
|
||||||
|
|
||||||
---
|
1. User runs `lore me` to see personal dashboard
|
||||||
|
2. Summary header shows "5 pending todos" alongside issue/MR counts
|
||||||
|
3. Todos section groups items: 2 Assignments, 2 Mentions, 1 Approval Required
|
||||||
|
4. User scans Assignments — sees issue #42 assigned by @manager
|
||||||
|
5. User runs `lore todos` for full detail with body snippets
|
||||||
|
6. User clicks target URL to address highest-priority item
|
||||||
|
7. After marking done in GitLab, next `lore sync` removes it locally
|
||||||
|
|
||||||
## Out of Scope
|
### Workflow 2: Agent Polling (Robot Mode)
|
||||||
|
|
||||||
- Write operations (mark as done)
|
1. Agent runs `lore --robot health` as pre-flight check
|
||||||
- Done todo history tracking
|
2. Agent runs `lore --robot me --fields minimal` for dashboard
|
||||||
- Filters on `lore todos` command
|
3. Agent extracts `pending_todo_count` from summary — if 0, skip todos
|
||||||
- Todo-based attention state boosting
|
4. If count > 0, agent runs `lore --robot todos`
|
||||||
- Notification settings API integration (deferred to separate plan)
|
5. Agent iterates `data.todos[]`, filtering by `action` type
|
||||||
|
6. Agent prioritizes `approval_required` and `build_failed` for immediate attention
|
||||||
|
7. Agent logs external todos (`is_external: true`) for manual review
|
||||||
|
|
||||||
|
### Workflow 3: Cross-Project Visibility
|
||||||
|
|
||||||
|
1. User is mentioned in a project they don't sync (e.g., company-wide repo)
|
||||||
|
2. `lore sync` fetches the todo anyway (account-wide fetch)
|
||||||
|
3. `lore todos` shows item with `[external]` indicator and project path
|
||||||
|
4. User can still click target URL to view in GitLab
|
||||||
|
5. Target title may be unavailable — graceful fallback to "Untitled"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
### AC-1: Database Schema
|
Behavioral contract. Each AC is a single testable statement.
|
||||||
|
|
||||||
- [ ] **AC-1.1:** Create `todos` table with columns:
|
### Storage
|
||||||
- `id` INTEGER PRIMARY KEY
|
|
||||||
- `gitlab_todo_id` INTEGER NOT NULL UNIQUE
|
|
||||||
- `project_id` INTEGER REFERENCES projects(id) ON DELETE SET NULL (nullable for non-synced)
|
|
||||||
- `target_type` TEXT NOT NULL (Issue, MergeRequest, Commit, Epic, etc.)
|
|
||||||
- `target_id` INTEGER (GitLab ID of target entity)
|
|
||||||
- `target_iid` INTEGER (IID for issues/MRs, nullable)
|
|
||||||
- `target_url` TEXT NOT NULL
|
|
||||||
- `target_title` TEXT
|
|
||||||
- `action_name` TEXT NOT NULL (assigned, mentioned, etc.)
|
|
||||||
- `author_id` INTEGER
|
|
||||||
- `author_username` TEXT
|
|
||||||
- `body` TEXT (the todo message/snippet)
|
|
||||||
- `state` TEXT NOT NULL (pending)
|
|
||||||
- `created_at` INTEGER NOT NULL (epoch ms)
|
|
||||||
- `updated_at` INTEGER NOT NULL (epoch ms)
|
|
||||||
- `synced_at` INTEGER NOT NULL (epoch ms)
|
|
||||||
- `project_path` TEXT (for display even if project not synced)
|
|
||||||
- [ ] **AC-1.2:** Create index `idx_todos_state_action` on `(state, action_name)`
|
|
||||||
- [ ] **AC-1.3:** Create index `idx_todos_target` on `(target_type, target_id)`
|
|
||||||
- [ ] **AC-1.4:** Create index `idx_todos_created` on `(created_at DESC)`
|
|
||||||
- [ ] **AC-1.5:** Migration increments schema version
|
|
||||||
|
|
||||||
### AC-2: GitLab API Client
|
| 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 |
|
||||||
|
|
||||||
- [ ] **AC-2.1:** Add `fetch_todos()` method to GitLab client
|
### Sync
|
||||||
- [ ] **AC-2.2:** Fetch only `state=pending` todos
|
|
||||||
- [ ] **AC-2.3:** Handle pagination (use existing pagination pattern)
|
|
||||||
- [ ] **AC-2.4:** Parse all target types GitLab returns
|
|
||||||
- [ ] **AC-2.5:** Extract project path from `target_url` for non-synced projects
|
|
||||||
|
|
||||||
### AC-3: Sync Pipeline
|
| 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) |
|
||||||
|
|
||||||
- [ ] **AC-3.1:** Add todos sync step to `lore sync` pipeline
|
### `lore todos` Command
|
||||||
- [ ] **AC-3.2:** Sync todos AFTER issues/MRs (ordering consistency)
|
|
||||||
- [ ] **AC-3.3:** Snapshot semantics: fetch all pending, upsert, delete missing (= marked done elsewhere)
|
|
||||||
- [ ] **AC-3.4:** Track `synced_at` timestamp
|
|
||||||
- [ ] **AC-3.5:** Log todo sync stats: fetched, inserted, updated, deleted
|
|
||||||
- [ ] **AC-3.6:** Add `--no-todos` flag to skip todo sync
|
|
||||||
|
|
||||||
### AC-4: Action Type Handling
|
| 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` |
|
||||||
|
|
||||||
- [ ] **AC-4.1:** Store ALL action types from GitLab
|
### `lore me` Integration
|
||||||
- [ ] **AC-4.2:** Display only core actions:
|
|
||||||
- `assigned` — assigned to issue/MR
|
|
||||||
- `mentioned` — @mentioned in comment
|
|
||||||
- `directly_addressed` — @mentioned at start of comment
|
|
||||||
- `approval_required` — approval needed on MR
|
|
||||||
- `build_failed` — CI failed on your MR
|
|
||||||
- `unmergeable` — merge conflicts on your MR
|
|
||||||
- [ ] **AC-4.3:** Skip display (but store) niche actions: `merge_train_removed`, `member_access_requested`, `marked`
|
|
||||||
|
|
||||||
### AC-5: `lore todos` Command
|
| 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 |
|
||||||
|
|
||||||
- [ ] **AC-5.1:** New subcommand `lore todos` (alias: `todo`)
|
### Action Types
|
||||||
- [ ] **AC-5.2:** Display all pending todos, no filters
|
|
||||||
- [ ] **AC-5.3:** Group by action type: Assignments | Mentions | Approvals | Build Issues
|
|
||||||
- [ ] **AC-5.4:** Per-todo display: target title, project path, author, age, action
|
|
||||||
- [ ] **AC-5.5:** Flag non-synced project todos with `[external]` indicator
|
|
||||||
- [ ] **AC-5.6:** Human-readable output with colors/icons
|
|
||||||
- [ ] **AC-5.7:** Robot mode: standard `{ok, data, meta}` envelope
|
|
||||||
|
|
||||||
### AC-6: `lore me --todos` Section
|
| 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 |
|
||||||
|
|
||||||
- [ ] **AC-6.1:** Add `--todos` flag to `MeArgs`
|
### Attention State
|
||||||
- [ ] **AC-6.2:** When no section flags: show todos in full dashboard
|
|
||||||
- [ ] **AC-6.3:** When `--todos` flag only: show only todos section
|
|
||||||
- [ ] **AC-6.4:** Todos section grouped by action type
|
|
||||||
- [ ] **AC-6.5:** Todos NOT filtered by `--project` (always account-wide)
|
|
||||||
- [ ] **AC-6.6:** Robot mode includes `todos` array in dashboard response
|
|
||||||
|
|
||||||
### AC-7: `lore me` Summary Header
|
| ID | Behavior |
|
||||||
|
|----|----------|
|
||||||
|
| AC-26 | Todos do not affect attention state calculation |
|
||||||
|
| AC-27 | Todos do not appear in "since last check" cursor-based inbox |
|
||||||
|
|
||||||
- [ ] **AC-7.1:** Add `pending_todo_count` to `MeSummary` struct
|
### Error Handling
|
||||||
- [ ] **AC-7.2:** Display todo count in summary line (human mode)
|
|
||||||
- [ ] **AC-7.3:** Include `pending_todo_count` in robot mode summary
|
|
||||||
|
|
||||||
### AC-8: Activity Feed Enrichment
|
| 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 |
|
||||||
|
|
||||||
- [ ] **AC-8.1:** Todos with local issue/MR target appear in activity feed
|
### Documentation
|
||||||
- [ ] **AC-8.2:** New `ActivityEventType::Todo` variant
|
|
||||||
- [ ] **AC-8.3:** Todo events show: action type, author, target in summary
|
|
||||||
- [ ] **AC-8.4:** Sorted chronologically with other activity events
|
|
||||||
- [ ] **AC-8.5:** Respect `--since` filter on todo `created_at`
|
|
||||||
|
|
||||||
### AC-9: Non-Synced Project Handling
|
| 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 |
|
||||||
|
|
||||||
- [ ] **AC-9.1:** Store todos even if target project not in config
|
### Quality
|
||||||
- [ ] **AC-9.2:** Display `[external]` indicator for non-synced project todos
|
|
||||||
- [ ] **AC-9.3:** Show project path (extracted from target URL)
|
|
||||||
- [ ] **AC-9.4:** Graceful fallback when target title unavailable
|
|
||||||
|
|
||||||
### AC-10: Attention State
|
| ID | Behavior |
|
||||||
|
|----|----------|
|
||||||
- [ ] **AC-10.1:** Attention state calculation remains note-based (unchanged)
|
| AC-34 | All quality gates pass: check, clippy, fmt, test |
|
||||||
- [ ] **AC-10.2:** Todos are separate signal, do not affect attention state
|
|
||||||
- [ ] **AC-10.3:** Document this design decision in code comments
|
|
||||||
|
|
||||||
### AC-11: Robot Mode Schema
|
|
||||||
|
|
||||||
- [ ] **AC-11.1:** `lore todos --robot` returns:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta": {"elapsed_ms": 42}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- [ ] **AC-11.2:** `lore me --robot` includes `todos` and `pending_todo_count` in response
|
|
||||||
- [ ] **AC-11.3:** Support `--fields minimal` for token efficiency
|
|
||||||
|
|
||||||
### AC-12: Documentation
|
|
||||||
|
|
||||||
- [ ] **AC-12.1:** Update CLAUDE.md with `lore todos` command reference
|
|
||||||
- [ ] **AC-12.2:** Update `lore robot-docs` manifest with todos schema
|
|
||||||
- [ ] **AC-12.3:** Add todos to CLI help output
|
|
||||||
|
|
||||||
### AC-13: Quality Gates
|
|
||||||
|
|
||||||
- [ ] **AC-13.1:** `cargo check --all-targets` passes
|
|
||||||
- [ ] **AC-13.2:** `cargo clippy --all-targets -- -D warnings` passes
|
|
||||||
- [ ] **AC-13.3:** `cargo fmt --check` passes
|
|
||||||
- [ ] **AC-13.4:** `cargo test` passes with new tests
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Technical Notes
|
## Architecture
|
||||||
|
|
||||||
### GitLab API Endpoint
|
Designed to fulfill the acceptance criteria above.
|
||||||
|
|
||||||
|
### Module Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /api/v4/todos?state=pending
|
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)
|
||||||
```
|
```
|
||||||
|
|
||||||
Response fields: id, project, author, action_name, target_type, target, target_url, body, state, created_at, updated_at
|
### Data Flow
|
||||||
|
|
||||||
### Sync Deletion Strategy
|
|
||||||
|
|
||||||
Snapshot semantics: a todo disappearing from API response means it was marked done elsewhere. Delete from local DB to stay in sync.
|
|
||||||
|
|
||||||
### Project Path Extraction
|
|
||||||
|
|
||||||
For non-synced projects, extract path from `target_url`:
|
|
||||||
```
|
```
|
||||||
https://gitlab.com/group/subgroup/repo/-/issues/42
|
GitLab API Local SQLite CLI Output
|
||||||
^^^^^^^^^^^^^^^^^ extract this
|
─────────── ──────────── ──────────
|
||||||
|
GET /api/v4/todos → todos table → lore todos
|
||||||
|
(account-wide) (purge-safe sync) lore me --todos
|
||||||
```
|
```
|
||||||
|
|
||||||
### Action Type Grouping
|
### 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:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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_id` nullable for non-synced projects (AC-3)
|
||||||
|
- `gitlab_project_id` nullable — TODO targets include non-project entities (Namespace, etc.)
|
||||||
|
- No `state` column — we only store pending todos
|
||||||
|
- `sync_generation` enables 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`:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[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`:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(alias = "todo")]
|
||||||
|
pub struct TodosArgs {
|
||||||
|
#[arg(short = 'n', long)]
|
||||||
|
pub limit: Option<usize>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Autocorrect aliases in `src/cli/mod.rs`:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
("td", "todos"),
|
||||||
|
("todo", "todos"),
|
||||||
|
```
|
||||||
|
|
||||||
|
**Action type grouping:**
|
||||||
|
|
||||||
| Group | Actions |
|
| Group | Actions |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
@@ -239,36 +437,212 @@ https://gitlab.com/group/subgroup/repo/-/issues/42
|
|||||||
| Approvals | `approval_required` |
|
| Approvals | `approval_required` |
|
||||||
| Build Issues | `build_failed`, `unmergeable` |
|
| Build Issues | `build_failed`, `unmergeable` |
|
||||||
|
|
||||||
|
**Robot mode schema:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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`:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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):**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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`:**
|
||||||
|
```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
|
## 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
|
### Slice A: Schema + Client
|
||||||
- Migration 028
|
|
||||||
- `GitLabTodo` type
|
**ACs:** AC-1, AC-2, AC-3, AC-4, AC-5
|
||||||
- `fetch_todos()` client method
|
**IMPs:** IMP-1, IMP-2, IMP-4
|
||||||
- Unit tests for deserialization
|
**Deliverable:** Migration + client method + deserialization tests pass
|
||||||
|
|
||||||
### Slice B: Sync Integration
|
### Slice B: Sync Integration
|
||||||
- `src/ingestion/todos.rs`
|
|
||||||
- Integrate into `lore sync`
|
**ACs:** AC-6, AC-7, AC-8, AC-9, AC-28, AC-29, AC-30
|
||||||
- `--no-todos` flag
|
**IMPs:** IMP-3, IMP-7
|
||||||
- Sync stats
|
**Deliverable:** `lore sync` fetches todos; `--no-todos` works
|
||||||
|
|
||||||
### Slice C: `lore todos` Command
|
### Slice C: `lore todos` Command
|
||||||
- CLI args + dispatch
|
|
||||||
- Human + robot rendering
|
**ACs:** AC-10, AC-11, AC-12, AC-13, AC-14, AC-15, AC-16, AC-17, AC-24, AC-25
|
||||||
- Autocorrect aliases
|
**IMPs:** IMP-5
|
||||||
|
**Deliverable:** `lore todos` and `lore --robot todos` work
|
||||||
|
|
||||||
### Slice D: `lore me` Integration
|
### Slice D: `lore me` Integration
|
||||||
- `--todos` flag
|
|
||||||
- Summary count
|
**ACs:** AC-18, AC-19, AC-20, AC-21, AC-22, AC-23, AC-26, AC-27
|
||||||
- Activity feed enrichment
|
**IMPs:** IMP-6
|
||||||
|
**Deliverable:** `lore me --todos` works; summary shows count
|
||||||
|
|
||||||
### Slice E: Polish
|
### Slice E: Polish
|
||||||
- Edge case tests
|
|
||||||
- Documentation updates
|
**ACs:** AC-31, AC-32, AC-33, AC-34
|
||||||
- `robot-docs` manifest
|
**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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user