Complete implementation of the lore me command with:
- Config: gitlab.username field for identity resolution
- CLI: MeArgs with --issues, --mrs, --activity, --since, --project, --all, --user, --fields
- Identity: username resolution with precedence (CLI > config > error)
- Scope: project scope resolution with fuzzy matching and mutual exclusivity
- Types: AttentionState enum (5 states with sort ordering), dashboard structs
- Queries: open issues, authored MRs, reviewing MRs (all with attention state CTEs)
- Activity: 5-source feed (notes, state/label/milestone events, assignment detection)
- Human renderer: summary header, attention legend, section cards, event badges
- Robot renderer: {ok,data,meta} envelope with --fields minimal preset
- Handler: full wiring with section filtering, error paths, exit codes
- Autocorrect: me command flags registered
21 beads (bd-qpk3 through bd-32aw) implemented by 3-agent swarm.
978 tests pass, clippy clean.
291 lines
15 KiB
Markdown
291 lines
15 KiB
Markdown
# `lore me` — Personal Work Dashboard
|
|
|
|
## Overview
|
|
|
|
A personal dashboard command that shows everything relevant to the configured user: open issues, authored MRs, MRs under review, and recent activity. Attention state is computed from GitLab interaction data (comments) with no local state tracking.
|
|
|
|
## Command Interface
|
|
|
|
```
|
|
lore me # Full dashboard (default project or all)
|
|
lore me --issues # Issues section only
|
|
lore me --mrs # MRs section only (authored + reviewing)
|
|
lore me --activity # Activity feed only
|
|
lore me --issues --mrs # Multiple sections (combinable)
|
|
lore me --all # All synced projects (overrides default_project)
|
|
lore me --since 2d # Activity window (default: 30d)
|
|
lore me --project group/repo # Scope to one project
|
|
lore me --user jdoe # Override configured username
|
|
```
|
|
|
|
Standard global flags: `--robot`/`-J`, `--fields`, `--color`, `--icons`.
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
### AC-1: Configuration
|
|
|
|
- **AC-1.1**: New optional field `gitlab.username` (string) in config.json
|
|
- **AC-1.2**: Resolution order: `--user` CLI flag > `config.gitlab.username` > exit code 2 with actionable error message suggesting how to set it
|
|
- **AC-1.3**: Username is case-sensitive (matches GitLab usernames exactly)
|
|
|
|
### AC-2: Command Interface
|
|
|
|
- **AC-2.1**: New command `lore me` — single command with flags (matches `who` pattern)
|
|
- **AC-2.2**: Section filter flags: `--issues`, `--mrs`, `--activity` — combinable. Passing multiple shows those sections. No flags = full dashboard (all sections).
|
|
- **AC-2.3**: `--since <duration>` controls activity feed window, default 30 days. Only affects the activity section; work item sections always show all open items regardless of `--since`.
|
|
- **AC-2.4**: `--project <path>` scopes to a single project
|
|
- **AC-2.5**: `--user <username>` overrides configured username
|
|
- **AC-2.6**: `--all` flag shows all synced projects (overrides default_project)
|
|
- **AC-2.7**: `--project` and `--all` are mutually exclusive — passing both is exit code 2
|
|
- **AC-2.8**: Standard global flags: `--robot`/`-J`, `--fields`, `--color`, `--icons`
|
|
|
|
### AC-3: "My Items" Definition
|
|
|
|
- **AC-3.1**: Issues assigned to me (`issue_assignees.username`). Authorship alone does NOT qualify an issue.
|
|
- **AC-3.2**: MRs authored by me (`merge_requests.author_username`)
|
|
- **AC-3.3**: MRs where I'm a reviewer (`mr_reviewers.username`)
|
|
- **AC-3.4**: Scope is **Assigned (issues) + Authored/Reviewing (MRs)** — no participation/mention expansion
|
|
- **AC-3.5**: MR assignees (`mr_assignees`) are NOT used — in Pattern 1 workflows (author = assignee), this is redundant with authorship
|
|
- **AC-3.6**: Activity feed uses CURRENT association only — if you've been unassigned from an issue, activity on it no longer appears. This keeps the query simple and the feed relevant.
|
|
|
|
### AC-4: Attention State Model
|
|
|
|
- **AC-4.1**: Computed per-item from synced GitLab data, no local state tracking
|
|
- **AC-4.2**: Interaction signal: notes authored by the user (`notes.author_username = me` where `is_system = 0`)
|
|
- **AC-4.3**: Future: award emoji will extend interaction signals (separate bead)
|
|
- **AC-4.4**: States (evaluated in this order — first match wins):
|
|
1. `not_ready`: MR only — `draft=1` AND zero entries in `mr_reviewers`
|
|
2. `needs_attention`: Others' latest non-system note > user's latest non-system note
|
|
3. `stale`: Entity has at least one non-system note from someone, but the most recent note from anyone is older than 30 days. Items with ZERO notes are NOT stale — they're `not_started`.
|
|
4. `not_started`: User has zero non-system notes on this entity (regardless of whether others have commented)
|
|
5. `awaiting_response`: User's latest non-system note timestamp >= all others' latest non-system note timestamps (including when user is the only commenter)
|
|
- **AC-4.5**: Applied to all item types (issues, authored MRs, reviewing MRs)
|
|
|
|
### AC-5: Dashboard Sections
|
|
|
|
**AC-5.1: Open Issues**
|
|
- Source: `issue_assignees.username = me`, state = opened
|
|
- Fields: project path, iid, title, status_name (work item status), attention state, relative time since updated
|
|
- Sort: attention-first (needs_attention > not_started > awaiting_response > stale), then most recently updated within same state
|
|
- No limit, no truncation — show all
|
|
|
|
**AC-5.2: Open MRs — Authored**
|
|
- Source: `merge_requests.author_username = me`, state = opened
|
|
- Fields: project path, iid, title, draft indicator, detailed_merge_status, attention state, relative time
|
|
- Sort: same as issues
|
|
|
|
**AC-5.3: Open MRs — Reviewing**
|
|
- Source: `mr_reviewers.username = me`, state = opened
|
|
- Fields: project path, iid, title, MR author username, draft indicator, attention state, relative time
|
|
- Sort: same as issues
|
|
|
|
**AC-5.4: Activity Feed**
|
|
- Sources (all within `--since` window, default 30d):
|
|
- Human comments (`notes.is_system = 0`) on my items
|
|
- State events (`resource_state_events`) on my items
|
|
- Label events (`resource_label_events`) on my items
|
|
- Milestone events (`resource_milestone_events`) on my items
|
|
- Assignment/reviewer system notes (see AC-12 for patterns) on my items
|
|
- "My items" for the activity feed = items I'm CURRENTLY associated with per AC-3 (current assignment state, not historical)
|
|
- Includes activity on items regardless of open/closed state
|
|
- Own actions included but flagged (`is_own: true` in robot, `(you)` suffix + dimmed in human)
|
|
- Sort: newest first (chronological descending)
|
|
- No limit, no truncation — show all events
|
|
|
|
**AC-5.5: Summary Header**
|
|
- Counts: projects, open issues, authored MRs, reviewing MRs, needs_attention count
|
|
- Attention legend (human mode): icon + label for each state
|
|
|
|
### AC-6: Human Output — Visual Design
|
|
|
|
**AC-6.1: Layout**
|
|
- Section card style with `section_divider` headers
|
|
- Legend at top explains attention icons
|
|
- Two-line per item: main data on line 1, project path on line 2 (indented)
|
|
- When scoped to single project (`--project`), suppress project path line (redundant)
|
|
|
|
**AC-6.2: Attention Icons (three tiers)**
|
|
|
|
| State | Nerd Font | Unicode | ASCII | Color |
|
|
|-------|-----------|---------|-------|-------|
|
|
| needs_attention | `\uf0f3` bell | `◆` | `[!]` | amber (warning) |
|
|
| not_started | `\uf005` star | `★` | `[*]` | cyan (info) |
|
|
| awaiting_response | `\uf017` clock | `◷` | `[~]` | dim (muted) |
|
|
| stale | `\uf54c` skull | `☠` | `[x]` | dim (muted) |
|
|
|
|
**AC-6.3: Color Vocabulary** (matches existing lore palette)
|
|
- Issue refs (#N): cyan
|
|
- MR refs (!N): purple
|
|
- Usernames (@name): cyan
|
|
- Opened state: green
|
|
- Merged state: purple
|
|
- Closed state: dim
|
|
- Draft indicator: gray
|
|
- Own actions: dimmed + `(you)` suffix
|
|
- Timestamps: dim (relative time)
|
|
|
|
**AC-6.4: Activity Event Badges**
|
|
|
|
| Event | Nerd/Unicode (colored bg) | ASCII fallback |
|
|
|-------|--------------------------|----------------|
|
|
| note | cyan bg, dark text | `[note]` cyan text |
|
|
| status | amber bg, dark text | `[status]` amber text |
|
|
| label | purple bg, white text | `[label]` purple text |
|
|
| assign | green bg, dark text | `[assign]` green text |
|
|
| milestone | magenta bg, white text | `[milestone]` magenta text |
|
|
|
|
Fallback: when background colors aren't available (ASCII mode), use colored text with brackets instead of background pills.
|
|
|
|
**AC-6.5: Labels**
|
|
- Human mode: not shown
|
|
- Robot mode: included in JSON
|
|
|
|
### AC-7: Robot Output
|
|
|
|
- **AC-7.1**: Standard `{ok, data, meta}` envelope
|
|
- **AC-7.2**: `data` contains: `username`, `since_iso`, `summary` (counts + `needs_attention_count`), `open_issues[]`, `open_mrs_authored[]`, `reviewing_mrs[]`, `activity[]`
|
|
- **AC-7.3**: Each item includes: project, iid, title, state, attention_state (programmatic: `needs_attention`, `not_started`, `awaiting_response`, `stale`, `not_ready`), labels, updated_at_iso, web_url
|
|
- **AC-7.4**: Issues include `status_name` (work item status)
|
|
- **AC-7.5**: MRs include `draft`, `detailed_merge_status`, `author_username` (reviewing section)
|
|
- **AC-7.6**: Activity items include: `timestamp_iso`, `event_type`, `entity_type`, `entity_iid`, `project`, `actor`, `is_own`, `summary`, `body_preview` (for notes, truncated to 200 chars)
|
|
- **AC-7.7**: `--fields minimal` preset: `iid`, `title`, `attention_state`, `updated_at_iso` (work items); `timestamp_iso`, `event_type`, `entity_iid`, `actor` (activity)
|
|
- **AC-7.8**: Metadata-only depth — agents drill into specific items with `timeline`, `issues`, `mrs` for full context
|
|
- **AC-7.9**: No limits, no truncation on any array
|
|
|
|
### AC-8: Cross-Project Behavior
|
|
|
|
- **AC-8.1**: If `config.default_project` is set, scope to that project by default. If no default project, show all synced projects.
|
|
- **AC-8.2**: `--all` flag overrides default project and shows all synced projects
|
|
- **AC-8.3**: `--project` flag narrows to a specific project (supports fuzzy match like other commands)
|
|
- **AC-8.4**: `--project` and `--all` are mutually exclusive (exit 2 if both passed)
|
|
- **AC-8.5**: Project path shown per-item in both human and robot output (suppressed in human when single-project scoped per AC-6.1)
|
|
|
|
### AC-9: Sort Order
|
|
|
|
- **AC-9.1**: Work item sections: attention-first, then most recently updated
|
|
- **AC-9.2**: Attention priority: `needs_attention` > `not_started` > `awaiting_response` > `stale` > `not_ready`
|
|
- **AC-9.3**: Activity feed: chronological descending (newest first)
|
|
|
|
### AC-10: Error Handling
|
|
|
|
- **AC-10.1**: No username configured and no `--user` flag → exit 2 with suggestion
|
|
- **AC-10.2**: No synced data → exit 17 with suggestion to run `lore sync`
|
|
- **AC-10.3**: Username found but no matching items → empty sections with summary showing zeros
|
|
- **AC-10.4**: `--project` and `--all` both passed → exit 2 with message
|
|
|
|
### AC-11: Relationship to Existing Commands
|
|
|
|
- **AC-11.1**: `who @username` remains for looking at anyone's workload
|
|
- **AC-11.2**: `lore me` is the self-view with attention intelligence
|
|
- **AC-11.3**: No deprecation of `who` — they serve different purposes
|
|
|
|
### AC-12: New Assignments Detection
|
|
|
|
- **AC-12.1**: Detect from system notes (`notes.is_system = 1`) matching these body patterns:
|
|
- `"assigned to @username"` — issue/MR assignment
|
|
- `"unassigned @username"` — removal (shown as `unassign` event type)
|
|
- `"requested review from @username"` — reviewer assignment (shown as `review_request` event type)
|
|
- **AC-12.2**: These appear in the activity feed with appropriate event types
|
|
- **AC-12.3**: Shows who performed the action (note author from the associated non-system context, or "system" if unavailable) and when (note created_at)
|
|
- **AC-12.4**: Pattern matching is case-insensitive and matches username at word boundary
|
|
|
|
---
|
|
|
|
## Out of Scope (Follow-Up Work)
|
|
|
|
- **Award emoji sync**: Extends attention signal with reaction timestamps. Requires new table + GitLab REST API integration. Note-level emoji sync has N+1 concern requiring smart batching.
|
|
- **Participation/mention expansion**: Broadening "my items" beyond assigned+authored.
|
|
- **Label filtering**: `--label` flag to scope dashboard by label.
|
|
|
|
---
|
|
|
|
## Design Notes
|
|
|
|
### Why No High-Water Mark
|
|
|
|
GitLab itself is the source of truth for "what I've engaged with." The attention state is computed by comparing the user's latest comment timestamp against others' latest comment timestamps on each item. No local cursor or mark is needed.
|
|
|
|
### Why Comments-Only (For Now)
|
|
|
|
Award emoji (reactions) are a valid "I've engaged" signal but aren't currently synced. The attention model is designed to incorporate emoji timestamps when available — adding them later requires no model changes.
|
|
|
|
### Why MR Assignees Are Excluded
|
|
|
|
GitLab MR workflows have three role fields: Author, Assignee, and Reviewer. In Pattern 1 workflows (the most common post-2020), the author assigns themselves — making assignee redundant with authorship. The Reviewing section uses `mr_reviewers` as the review signal.
|
|
|
|
### Attention State Evaluation Order
|
|
|
|
States are evaluated in priority order (first match wins):
|
|
|
|
```
|
|
1. not_ready — MR-only: draft=1 AND no reviewers
|
|
2. needs_attention — others commented after me
|
|
3. stale — had activity, but nothing in 30d (NOT for zero-comment items)
|
|
4. not_started — I have zero comments (may or may not have others' comments)
|
|
5. awaiting_response — I commented last (or I'm the only commenter)
|
|
```
|
|
|
|
Edge cases:
|
|
- Zero comments from anyone → `not_started` (NOT stale)
|
|
- Only my comments, none from others → `awaiting_response`
|
|
- Only others' comments, none from me → `not_started` (I haven't engaged)
|
|
- Wait: this conflicts with `needs_attention` (step 2). If others have commented and I haven't, then others' latest > my latest (NULL). This should be `needs_attention`, not `not_started`.
|
|
|
|
Corrected logic:
|
|
- `needs_attention` takes priority over `not_started` when others HAVE commented but I haven't. The distinction: `not_started` only applies when NOBODY has commented.
|
|
|
|
```
|
|
1. not_ready — MR-only: draft=1 AND no reviewers
|
|
2. needs_attention — others have non-system notes AND (I have none OR others' latest > my latest)
|
|
3. stale — latest note from anyone is older than 30 days
|
|
4. awaiting_response — my latest >= others' latest (I'm caught up)
|
|
5. not_started — zero non-system notes from anyone
|
|
```
|
|
|
|
### Attention State Computation (SQL Sketch)
|
|
|
|
```sql
|
|
WITH my_latest AS (
|
|
SELECT d.issue_id, d.merge_request_id, MAX(n.created_at) AS ts
|
|
FROM notes n
|
|
JOIN discussions d ON n.discussion_id = d.id
|
|
WHERE n.author_username = ?me AND n.is_system = 0
|
|
GROUP BY d.issue_id, d.merge_request_id
|
|
),
|
|
others_latest AS (
|
|
SELECT d.issue_id, d.merge_request_id, MAX(n.created_at) AS ts
|
|
FROM notes n
|
|
JOIN discussions d ON n.discussion_id = d.id
|
|
WHERE n.author_username != ?me AND n.is_system = 0
|
|
GROUP BY d.issue_id, d.merge_request_id
|
|
),
|
|
any_latest AS (
|
|
SELECT d.issue_id, d.merge_request_id, MAX(n.created_at) AS ts
|
|
FROM notes n
|
|
JOIN discussions d ON n.discussion_id = d.id
|
|
WHERE n.is_system = 0
|
|
GROUP BY d.issue_id, d.merge_request_id
|
|
)
|
|
SELECT
|
|
CASE
|
|
-- MR-only: draft with no reviewers
|
|
WHEN entity_type = 'mr' AND draft = 1
|
|
AND NOT EXISTS (SELECT 1 FROM mr_reviewers WHERE merge_request_id = entity_id)
|
|
THEN 'not_ready'
|
|
-- Others commented and I haven't caught up (or never engaged)
|
|
WHEN others.ts IS NOT NULL AND (my.ts IS NULL OR others.ts > my.ts)
|
|
THEN 'needs_attention'
|
|
-- Had activity but gone quiet for 30d
|
|
WHEN any.ts IS NOT NULL AND any.ts < ?now_minus_30d
|
|
THEN 'stale'
|
|
-- I've responded and I'm caught up
|
|
WHEN my.ts IS NOT NULL AND my.ts >= COALESCE(others.ts, 0)
|
|
THEN 'awaiting_response'
|
|
-- Nobody has commented at all
|
|
ELSE 'not_started'
|
|
END AS attention_state
|
|
FROM ...
|
|
```
|