diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2a832d5..9d7137b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -65,7 +65,7 @@ {"id":"bd-1mf","title":"[CP1] gi sync-status enhancement","description":"Enhance sync-status from CP0 stub to show issue cursors.\n\nOutput:\n- Last run timestamp and duration\n- Cursor positions per project (issues resource_type)\n- Entity counts (issues, discussions, notes)\n\nFiles: src/cli/commands/sync-status.ts (update existing)\nDone when: Shows cursor positions and counts after ingestion","status":"tombstone","priority":3,"issue_type":"task","created_at":"2026-01-25T15:20:36.449088Z","created_by":"tayloreernisse","updated_at":"2026-01-25T15:21:35.157235Z","closed_at":"2026-01-25T15:21:35.157235Z","deleted_at":"2026-01-25T15:21:35.157232Z","deleted_by":"tayloreernisse","delete_reason":"delete","original_type":"task","compaction_level":0,"original_size":0} {"id":"bd-1mju","title":"Vertical slice integration test + SLO verification","description":"## Background\nThe vertical slice gate validates that core screens work together end-to-end with real data flows and meet performance SLOs. This is a manual + automated verification pass.\n\n## Approach\nCreate integration tests in crates/lore-tui/tests/:\n- test_full_nav_flow: Dashboard -> press i -> IssueList loads -> press Enter -> IssueDetail loads -> press Esc -> back to IssueList with cursor preserved -> press H -> Dashboard\n- test_filter_requery: IssueList -> type filter -> verify re-query triggers and results update\n- test_stale_result_guard: rapidly navigate between screens, verify no stale data displayed\n- Performance benchmarks: run M-tier fixture, measure p95 nav latency, assert < 75ms\n- Stuck-input check: fuzz InputMode transitions, assert always recoverable via Esc or Ctrl+C\n- Cancel latency: start sync, cancel, measure time to acknowledgment, assert < 2s\n\n## Acceptance Criteria\n- [ ] Full nav flow test passes without panic\n- [ ] Filter re-query test shows updated results\n- [ ] No stale data displayed during rapid navigation\n- [ ] p95 nav latency < 75ms on M-tier fixtures\n- [ ] Zero stuck-input states across 1000 random key sequences\n- [ ] Sync cancel acknowledged p95 < 2s\n- [ ] All state preserved correctly on back-navigation\n\n## Files\n- CREATE: crates/lore-tui/tests/vertical_slice.rs\n\n## TDD Anchor\nRED: Write test_dashboard_to_issue_detail_roundtrip that navigates Dashboard -> IssueList -> IssueDetail -> Esc -> IssueList, asserts cursor position preserved.\nGREEN: Ensure all navigation and state preservation is wired up.\nVERIFY: cargo test --manifest-path crates/lore-tui/Cargo.toml vertical_slice\n\n## Edge Cases\n- Tests need FakeClock and synthetic DB fixtures (not real GitLab)\n- ftui test harness required for rendering tests without TTY\n- Performance benchmarks may vary by machine — use relative thresholds\n\n## Dependency Context\nRequires all Phase 2 screens: Dashboard, Issue List, Issue Detail, MR List, MR Detail.\nRequires NavigationStack, TaskSupervisor, DbManager from Phase 1.","status":"tombstone","priority":2,"issue_type":"task","created_at":"2026-02-12T17:00:18.310264Z","created_by":"tayloreernisse","updated_at":"2026-03-11T18:34:21.993329Z","deleted_at":"2026-03-11T18:34:21.993322Z","deleted_by":"tayloreernisse","delete_reason":"delete","original_type":"task","compaction_level":0,"original_size":0,"labels":["TUI"]} {"id":"bd-1n5","title":"[CP1] gi ingest --type=issues command","description":"CLI command to orchestrate issue ingestion.\n\nImplementation:\n1. Acquire app lock with heartbeat\n2. Create sync_run record (status='running')\n3. For each configured project:\n - Call ingestIssues()\n - For each ingested issue, call ingestIssueDiscussions()\n - Show progress (spinner or progress bar)\n4. Update sync_run (status='succeeded', metrics_json)\n5. Release lock\n\nFlags:\n- --type=issues (required)\n- --project=PATH (optional, filter to single project)\n- --force (override stale lock)\n\nOutput: Progress bar, then summary with counts\n\nFiles: src/cli/commands/ingest.ts\nTests: tests/integration/sync-runs.test.ts\nDone when: Full issue + discussion ingestion works end-to-end","status":"tombstone","priority":2,"issue_type":"task","created_at":"2026-01-25T15:20:05.114751Z","created_by":"tayloreernisse","updated_at":"2026-01-25T15:21:35.153598Z","closed_at":"2026-01-25T15:21:35.153598Z","deleted_at":"2026-01-25T15:21:35.153595Z","deleted_by":"tayloreernisse","delete_reason":"delete","original_type":"task","compaction_level":0,"original_size":0} -{"id":"bd-1n5q","title":"lore brief: situational awareness for topic/module/person","description":"## Background\nComposable capstone command. An agent says \"I am about to work on auth\" and gets everything in one call: open issues, active MRs, experts, recent activity, unresolved threads, related context. Replaces 5 separate lore calls with 1.\n\n## Input Modes\n1. Topic: `lore brief 'authentication'` — FTS search to find relevant entities, aggregate\n2. Path: `lore brief --path src/auth/` — who expert internals for path expertise\n3. Person: `lore brief --person teernisse` — who workload internals\n4. Entity: `lore brief issues 3864` — single entity focus with cross-references\n\n## Section Assembly Architecture\n\n### Reuse existing run_* functions (ship faster, recommended for v1)\nEach section calls existing CLI command functions and converts their output.\n\nIMPORTANT: All existing run_* functions take `&Config`, NOT `&Connection`. The Config contains the db_path and each function opens its own connection internally.\n\n```rust\n// In src/cli/commands/brief.rs\n\nuse crate::cli::commands::list::{run_list_issues, run_list_mrs, ListFilters, MrListFilters};\nuse crate::cli::commands::who::{run_who, WhoArgs, WhoMode};\nuse crate::core::config::Config;\n\npub async fn run_brief(config: &Config, args: BriefArgs) -> Result {\n let mut sections_computed = Vec::new();\n\n // 1. open_issues: reuse list.rs\n // Signature: pub fn run_list_issues(config: &Config, filters: ListFilters) -> Result\n // Located at src/cli/commands/list.rs:268\n let open_issues = run_list_issues(config, ListFilters {\n state: Some(\"opened\".into()),\n limit: Some(5),\n project: args.project.clone(),\n // ... scope by topic/path/person based on mode\n ..Default::default()\n })?;\n sections_computed.push(\"open_issues\");\n\n // 2. active_mrs: reuse list.rs\n // Signature: pub fn run_list_mrs(config: &Config, filters: MrListFilters) -> Result\n // Located at src/cli/commands/list.rs:476\n let active_mrs = run_list_mrs(config, MrListFilters {\n state: Some(\"opened\".into()),\n limit: Some(5),\n project: args.project.clone(),\n ..Default::default()\n })?;\n sections_computed.push(\"active_mrs\");\n\n // 3. experts: reuse who.rs\n // Signature: pub fn run_who(config: &Config, args: &WhoArgs) -> Result\n // Located at src/cli/commands/who.rs:276\n let experts = run_who(config, &WhoArgs {\n mode: WhoMode::Expert,\n path: args.path.clone(),\n limit: Some(3),\n ..Default::default()\n })?;\n sections_computed.push(\"experts\");\n\n // 4. recent_activity: reuse timeline internals\n // The timeline pipeline is 5-stage (SEED->HYDRATE->EXPAND->COLLECT->RENDER)\n // Types in src/core/timeline.rs, seed in src/core/timeline_seed.rs\n // ...etc\n}\n```\n\nNOTE: ListFilters and MrListFilters may not implement Default. Check before using `..Default::default()`. If they don't, derive it or construct all fields explicitly.\n\n### Concrete Function References (src/cli/commands/)\n| Module | Function | Signature | Line |\n|--------|----------|-----------|------|\n| list.rs | run_list_issues | `(config: &Config, filters: ListFilters) -> Result` | 268 |\n| list.rs | run_list_mrs | `(config: &Config, filters: MrListFilters) -> Result` | 476 |\n| who.rs | run_who | `(config: &Config, args: &WhoArgs) -> Result` | 276 |\n| search.rs | run_search | `(config: &Config, query: &str, cli_filters: SearchCliFilters, fts_mode: FtsQueryMode, requested_mode: &str, explain: bool) -> Result` | 61 |\n\nNOTE: run_search is currently synchronous (pub fn, not pub async fn). If bd-1ksf ships first, it becomes async. Brief should handle both cases — call `.await` if async, direct call if sync.\n\n### Section Details\n| Section | Source | Limit | Fallback |\n|---------|--------|-------|----------|\n| open_issues | list.rs with state=opened | 5 | empty array |\n| active_mrs | list.rs with state=opened | 5 | empty array |\n| experts | who.rs Expert mode | 3 | empty array (no path data) |\n| recent_activity | timeline pipeline | 10 events | empty array |\n| unresolved_threads | SQL: discussions WHERE resolved=false | 5 | empty array |\n| related | search_vector() via bd-8con | 5 | omit section (no embeddings) |\n| warnings | computed from dates/state | all | empty array |\n\n### Warning Generation\n```rust\nfn compute_warnings(issues: &[IssueRow]) -> Vec {\n let now = chrono::Utc::now();\n issues.iter().filter_map(|i| {\n let updated = parse_timestamp(i.updated_at)?;\n let days_stale = (now - updated).num_days();\n if days_stale > 30 {\n Some(format!(\"Issue #{} has no activity for {} days\", i.iid, days_stale))\n } else { None }\n }).chain(\n issues.iter().filter(|i| i.assignees.is_empty())\n .map(|i| format!(\"Issue #{} is unassigned\", i.iid))\n ).collect()\n}\n```\n\n## Robot Mode Output Schema\n```json\n{\n \"ok\": true,\n \"data\": {\n \"mode\": \"topic\",\n \"query\": \"authentication\",\n \"summary\": \"3 open issues, 2 active MRs, top expert: teernisse\",\n \"open_issues\": [{ \"iid\": 123, \"title\": \"...\", \"state\": \"opened\", \"assignees\": [...], \"updated_at\": \"...\", \"labels\": [...] }],\n \"active_mrs\": [{ \"iid\": 456, \"title\": \"...\", \"state\": \"opened\", \"author\": \"...\", \"draft\": false, \"updated_at\": \"...\" }],\n \"experts\": [{ \"username\": \"teernisse\", \"score\": 42, \"last_activity\": \"...\" }],\n \"recent_activity\": [{ \"timestamp\": \"...\", \"event_type\": \"state_change\", \"entity_ref\": \"issues#123\", \"summary\": \"...\", \"actor\": \"...\" }],\n \"unresolved_threads\": [{ \"discussion_id\": \"abc\", \"entity_ref\": \"issues#123\", \"started_by\": \"...\", \"note_count\": 5, \"last_note_at\": \"...\" }],\n \"related\": [{ \"iid\": 789, \"title\": \"...\", \"similarity_score\": 0.85 }],\n \"warnings\": [\"Issue #3800 has no activity for 45 days\"]\n },\n \"meta\": { \"elapsed_ms\": 1200, \"sections_computed\": [\"open_issues\", \"active_mrs\", \"experts\", \"recent_activity\"] }\n}\n```\n\n## Clap Registration\n```rust\n// In src/main.rs Commands enum, add:\nBrief {\n /// Free-text topic, entity type, or omit for project-wide brief\n query: Option,\n /// Focus on a file path (who expert mode)\n #[arg(long)]\n path: Option,\n /// Focus on a person (who workload mode)\n #[arg(long)]\n person: Option,\n /// Scope to project (fuzzy match)\n #[arg(short, long)]\n project: Option,\n /// Maximum items per section\n #[arg(long, default_value = \"5\")]\n section_limit: usize,\n},\n```\n\n## TDD Loop\nRED: Tests in src/cli/commands/brief.rs:\n- test_brief_topic_returns_all_sections: insert test data, search 'auth', assert all section keys present in response\n- test_brief_path_uses_who_expert: brief --path src/auth/, assert experts section populated\n- test_brief_person_uses_who_workload: brief --person user, assert open_issues filtered to user's assignments\n- test_brief_warnings_stale_issue: insert issue with updated_at > 30 days ago, assert warning generated\n- test_brief_token_budget: robot mode output for topic query is under 12000 bytes (~3000 tokens)\n- test_brief_no_embeddings_graceful: related section omitted (not errored) when no embeddings exist\n- test_brief_empty_topic: zero matches returns valid JSON with empty arrays + \"No data found\" summary\n\nGREEN: Implement brief with section assembly, calling existing run_* functions\n\nVERIFY:\n```bash\ncargo test brief:: && cargo clippy --all-targets -- -D warnings\ncargo run --release -- -J brief 'throw time' | jq '.data | keys'\ncargo run --release -- -J brief 'throw time' | wc -c # target <12000\n```\n\n## Acceptance Criteria\n- [ ] lore brief TOPIC returns all sections for free-text topic\n- [ ] lore brief --path PATH returns path-focused briefing with experts\n- [ ] lore brief --person USERNAME returns person-focused briefing\n- [ ] lore brief issues N returns entity-focused briefing\n- [ ] Robot mode output under 12000 bytes (~3000 tokens)\n- [ ] Each section degrades gracefully if its data source is unavailable\n- [ ] summary field is auto-generated one-liner from section counts\n- [ ] warnings detect: stale issues (>30d), unassigned, no due date\n- [ ] Performance: <2s total (acceptable since composing multiple queries)\n- [ ] Command registered in main.rs and robot-docs\n\n## Edge Cases\n- Topic with zero matches: return empty sections + \"No data found for this topic\" summary\n- Path that nobody has touched: experts empty, related may still have results\n- Person not found in DB: exit code 17 with suggestion\n- All sections empty: still return valid JSON with empty arrays\n- Very broad topic (\"the\"): may return too many results — each section respects its limit cap\n- ListFilters/MrListFilters may not derive Default — construct all fields explicitly if needed\n\n## Dependencies\n- Hybrid search (bd-1ksf) for topic relevance ranking\n- lore who (already shipped) for expertise\n- lore related (bd-8con) for semantic connections (BLOCKER — related section is core to the feature)\n- Timeline pipeline (already shipped) for recent activity\n\n## Dependency Context\n- **bd-1ksf (hybrid search)**: Provides `search_hybrid()` which brief uses for topic mode to find relevant entities. Without it, topic mode falls back to FTS-only via `search_fts()`.\n- **bd-8con (related)**: Provides `run_related()` which brief calls to populate the `related` section with semantically similar entities. This is a blocking dependency — the related section is a core differentiator.\n\n## Files to Create/Modify\n- NEW: src/cli/commands/brief.rs\n- src/cli/commands/mod.rs (add pub mod brief; re-export)\n- src/main.rs (register Brief subcommand in Commands enum, add handle_brief fn)\n- Reuse: list.rs, who.rs, timeline.rs, search.rs, show.rs internals","status":"open","priority":2,"issue_type":"feature","created_at":"2026-02-12T15:47:22.893231Z","created_by":"tayloreernisse","updated_at":"2026-02-18T21:44:58.454144Z","compaction_level":0,"original_size":0,"labels":["cli-imp","intelligence"],"dependencies":[{"issue_id":"bd-1n5q","depends_on_id":"bd-13lp","type":"parent-child","created_at":"2026-03-04T20:00:21Z","created_by":"import"},{"issue_id":"bd-1n5q","depends_on_id":"bd-8con","type":"blocks","created_at":"2026-03-04T20:00:21Z","created_by":"import"}]} +{"id":"bd-1n5q","title":"lore brief: situational awareness for topic/module/person","description":"## Background\nComposable capstone command. An agent says \"I am about to work on auth\" and gets everything in one call: open issues, active MRs, experts, recent activity, unresolved threads, related context. Replaces 5 separate lore calls with 1.\n\n## Input Modes\n1. Topic: `lore brief 'authentication'` — FTS search to find relevant entities, aggregate\n2. Path: `lore brief --path src/auth/` — who expert internals for path expertise\n3. Person: `lore brief --person teernisse` — who workload internals\n4. Entity: `lore brief issues 3864` — single entity focus with cross-references\n\n## Topic-to-Entity Scoping Strategy (Option A: Search-First, IID Filter)\n\nTopic mode requires scoping issue/MR lists to a free-text query. The existing `ListFilters` has no \"search\" field, so brief uses a two-pass approach:\n\n1. Run `run_search(config, topic, ...)` to find matching entity IIDs via FTS (or hybrid search if bd-1ksf is shipped)\n2. Pass those IIDs into `ListFilters { iids: Some(&iid_vec), state: Some(\"opened\"), ... }`\n3. List module handles sorting, field selection, project scoping\n\n**Required change:** Add `iids: Option<&'a [i64]>` to both `ListFilters` and `MrListFilters` in `src/cli/commands/list/`. In the query builder, when `iids` is `Some`, add `AND iid IN (?)` (use repeated `?` placeholders). This is a small, clean change that benefits any future command needing \"give me issues matching a search.\"\n\nFor path mode and person mode, the existing filters suffice — path mode uses `run_who` for experts and scopes issues by file path mentions; person mode filters by `assignee`/`author`.\n\n## Section Assembly Architecture\n\n### Reuse existing run_* functions\nEach section calls existing CLI command functions and converts their output.\n\nIMPORTANT: All existing run_* functions take `&Config`, NOT `&Connection`. The Config contains the db_path and each function opens its own connection internally.\n\n**Sync/async mix:** `run_brief` is `async`. Sync callees (`run_list_issues`, `run_list_mrs`, `run_who`) are called directly — no `spawn_blocking` needed, they're fast single-SQLite queries. Async callees (`run_related`, `run_timeline`) use `.await`.\n\n### Verified Function Signatures (as of 2026-03-13)\n\n| Module | Function | Signature |\n|--------|----------|-----------|\n| list/issues.rs:133 | `run_list_issues` | `(config: &Config, filters: ListFilters<'a>) -> Result` |\n| list/mrs.rs:120 | `run_list_mrs` | `(config: &Config, filters: MrListFilters<'a>) -> Result` |\n| who/mod.rs:115 | `run_who` | `(config: &Config, args: &WhoArgs) -> Result` |\n| search.rs:69 | `run_search` | `(config: &Config, query: &str, cli_filters: SearchCliFilters, fts_mode: FtsQueryMode, requested_mode: &str, explain: bool) -> Result` — currently `pub async fn` |\n| related.rs:92 | `run_related` | `async (config: &Config, query_or_type: &str, iid: Option, limit: usize, project: Option<&str>) -> Result` |\n| timeline.rs:71 | `run_timeline` | `async (config: &Config, params: &TimelineParams) -> Result` |\n\n### CRITICAL: ListFilters and MrListFilters have lifetimes and do NOT derive Default\n\nBoth structs use `&'a str` and `&'a [String]` references. All fields must be constructed explicitly.\n\n`ListFilters<'a>` fields: `limit: usize`, `project: Option<&'a str>`, `state: Option<&'a str>`, `author: Option<&'a str>`, `assignee: Option<&'a str>`, `labels: Option<&'a [String]>`, `milestone: Option<&'a str>`, `since: Option<&'a str>`, `due_before: Option<&'a str>`, `has_due_date: bool`, `statuses: &'a [String]`, `sort: &'a str`, `order: &'a str`. After the iids change: `iids: Option<&'a [i64]>`.\n\n`MrListFilters<'a>` fields: `limit: usize`, `project: Option<&'a str>`, `state: Option<&'a str>`, `author: Option<&'a str>`, `assignee: Option<&'a str>`, `reviewer: Option<&'a str>`, `labels: Option<&'a [String]>`, `since: Option<&'a str>`, `draft: bool`, `no_draft: bool`, `target_branch: Option<&'a str>`, `source_branch: Option<&'a str>`, `sort: &'a str`, `order: &'a str`. After the iids change: `iids: Option<&'a [i64]>`.\n\n### CRITICAL: WhoArgs is a clap struct, NOT a domain struct\n\n`WhoArgs` (defined in `src/cli/args.rs:605`) is a clap `#[derive(Parser)]` struct. There is no `mode` field — mode is resolved internally by `resolve_mode()` in `who/mod.rs` based on which fields are set. Key fields: `target: Option`, `path: Option`, `active: bool`, `overlap: Option`, `reviews: bool`, `since: Option`, `project: Option`, `limit: Option`, `fields: Option>`, `detail: bool`, `no_detail: bool`, `as_of: Option`, `explain_score: bool`.\n\nTo trigger expert mode: set `path = Some(...)`. To trigger workload mode: set `target = Some(username)`. Do not attempt to set a `mode` field.\n\n### Corrected Example Code\n\n```rust\n// In src/cli/commands/brief.rs\n\nuse crate::cli::commands::list::issues::{run_list_issues, ListFilters};\nuse crate::cli::commands::list::mrs::{run_list_mrs, MrListFilters};\nuse crate::cli::commands::who::run_who;\nuse crate::cli::commands::related::run_related;\nuse crate::cli::commands::timeline::{run_timeline, TimelineParams};\nuse crate::cli::WhoArgs;\nuse crate::core::config::Config;\nuse crate::core::error::Result;\n\npub async fn run_brief(config: &Config, args: &BriefArgs) -> Result {\n let empty_statuses: Vec = vec![];\n let section_limit = args.section_limit;\n\n // 1. Topic mode: search first to get relevant entity IIDs\n let search_iids: Vec = if let Some(ref query) = args.query {\n // run_search returns SearchResponse with results containing entity IIDs\n // Extract and deduplicate IIDs for filtering\n let search_result = run_search(config, query, /* filters */).await?;\n search_result.results.iter().map(|r| r.iid).collect()\n } else {\n vec![]\n };\n\n // 2. open_issues (sync — call directly)\n let open_issues = run_list_issues(config, ListFilters {\n limit: section_limit,\n project: args.project.as_deref(),\n state: Some(\"opened\"),\n author: None,\n assignee: args.person.as_deref(), // person mode: filter by assignee\n labels: None,\n milestone: None,\n since: None,\n due_before: None,\n has_due_date: false,\n statuses: &empty_statuses,\n sort: \"updated_at\",\n order: \"desc\",\n iids: if search_iids.is_empty() { None } else { Some(&search_iids) },\n })?;\n\n // 3. active_mrs (sync — call directly)\n let active_mrs = run_list_mrs(config, MrListFilters {\n limit: section_limit,\n project: args.project.as_deref(),\n state: Some(\"opened\"),\n author: args.person.as_deref(), // person mode: filter by author\n assignee: None,\n reviewer: None,\n labels: None,\n since: None,\n draft: false,\n no_draft: false,\n target_branch: None,\n source_branch: None,\n sort: \"updated_at\",\n order: \"desc\",\n iids: if search_iids.is_empty() { None } else { Some(&search_iids) },\n })?;\n\n // 4. experts (sync — call directly; path mode only or derived from topic)\n let experts = if args.path.is_some() {\n Some(run_who(config, &WhoArgs {\n target: None,\n path: args.path.clone(),\n active: false,\n overlap: None,\n reviews: false,\n since: None,\n project: args.project.clone(),\n limit: Some(3),\n fields: None,\n detail: false,\n no_detail: false,\n as_of: None,\n explain_score: false,\n })?)\n } else {\n None\n };\n\n // 5. related (async — graceful fallback if no embeddings)\n let related = run_related(\n config,\n args.query.as_deref().unwrap_or(\"\"),\n None, // iid (None for query mode)\n section_limit,\n args.project.as_deref(),\n ).await.ok(); // Ok(response) or None on error\n\n // 6. recent_activity (async — timeline pipeline)\n let activity = run_timeline(config, &TimelineParams {\n query: args.query.clone().unwrap_or_default(),\n project: args.project.clone(),\n since: Some(\"30d\".to_string()),\n depth: 0,\n no_mentions: true,\n limit: section_limit,\n max_seeds: 5,\n max_entities: 10,\n max_evidence: 5,\n robot_mode: false,\n }).await.ok();\n\n // 7. unresolved_threads — direct SQL (no existing run_* function)\n // 8. warnings — computed from issues/MRs\n // ... assemble BriefResponse from all sections\n}\n```\n\n### Section Details\n| Section | Source | Limit | Fallback |\n|---------|--------|-------|----------|\n| open_issues | list/issues.rs with state=opened, iids filter for topic mode | section_limit | empty array |\n| active_mrs | list/mrs.rs with state=opened, iids filter for topic mode | section_limit | empty array |\n| experts | who/mod.rs Expert mode (path mode) or omit (topic/person mode) | 3 | empty array (no path data) or omit section |\n| recent_activity | timeline pipeline | section_limit events | empty array |\n| unresolved_threads | Direct SQL: discussions WHERE resolved=false | section_limit | empty array |\n| related | related.rs run_related() | section_limit | omit section (no embeddings) |\n| warnings | computed from dates/state | all | empty array |\n\n### Warning Generation\n```rust\nfn compute_warnings(issues: &[IssueRow]) -> Vec {\n let now = chrono::Utc::now();\n issues.iter().filter_map(|i| {\n let updated = parse_timestamp(i.updated_at)?;\n let days_stale = (now - updated).num_days();\n if days_stale > 30 {\n Some(format!(\"Issue #{} has no activity for {} days\", i.iid, days_stale))\n } else { None }\n }).chain(\n issues.iter().filter(|i| i.assignees.is_empty())\n .map(|i| format!(\"Issue #{} is unassigned\", i.iid))\n ).collect()\n}\n```\n\n## Human Output Design\n\nThe human renderer follows the established pattern from `me/render_human.rs` — the closest architectural precedent (multi-section dashboard using Theme, Table, section_divider, Icons, StyledCell). New file: `src/cli/commands/brief/render_human.rs`.\n\n### Rendering Primitives per Section\n\n| Section | Primitives |\n|---------|------------|\n| Header | `section_divider(\"Brief: {query}\")`, `Theme::bold()`, `Theme::username()` |\n| Open Issues | `Icons::issue_opened()`, `Theme::issue_ref()`, `truncate()` (titles only), `format_relative_time()`, `Theme::dim()` |\n| Active MRs | `Icons::mr_opened()`/`mr_draft()`, `Theme::mr_ref()`, `Theme::username()`, `Theme::state_draft()` |\n| Experts | `Table` (3 cols, indent 4), `Theme::username()`, `Theme::dim()` |\n| Recent Activity | `Table` (5 cols, indent 4) — mirrors `me`'s `print_activity_section` pattern |\n| Unresolved Threads | `Theme::issue_ref()`/`mr_ref()`, full note text (no truncation), note count, `Theme::dim()` |\n| Related | `Table` (4 cols, indent 4), `Theme::issue_ref()`/`mr_ref()`, similarity score, `Theme::dim()` |\n| Warnings | `Icons::warning()`, `Theme::warning()`, indent 2 |\n\n### CRITICAL: No truncation on notes or discussions\nNote/discussion body text is NEVER truncated in human output. Full text wraps naturally within the terminal width. This applies to: unresolved thread first-note body, activity feed body previews, related section titles. Issue/MR titles still truncate since they're single-line tabular rows (matches existing `me` behavior).\n\n### Design Decisions\n\n| Decision | Choice | Rationale |\n|----------|--------|-----------|\n| Header style | Single `section_divider(\"Brief: {query}\")` + counts line | Lighter than `me`'s full header — brief is a quick lookup, not a personal dashboard |\n| Attention states | No — use entity state icons (`Icons::issue_opened`, `Icons::mr_opened`) | Brief shows someone else's domain, not your personal items. Attention states are ego-centric. |\n| Empty sections | Omit entirely (don't show \"Open Issues (0)\") | Brief should be scannable; empty sections are noise. Differs from `me` which always shows sections. |\n| Warnings | Bottom of output, `Icons::warning()` + `Theme::warning()` | Separated from data so they're scannable |\n\n### Mode-Specific Variations\n\n- **`--path src/auth/`**: Header says `Brief: src/auth/`. Experts section is prominent (moved to position after header, before issues). Issues/MRs scoped by file path mentions. Activity scoped similarly.\n- **`--person @teernisse`**: Header says `Brief: @teernisse`. No experts section (you're looking at the person). Issues = their assignments. MRs = their authored. Activity = their actions.\n- **Entity `issues 3864`**: Header says `Brief: Issue #3864`. Single entity focus — cross-references replace the standard sections.\n\n### Example Output: `lore brief 'authentication'`\n\n```\n -- Brief: authentication -----------------------------------------------\n 3 open issues 2 active MRs top expert: @teernisse\n\n -- Open Issues (3) -----------------------------------------------------\n o #3864 Fix token refresh race condition [In progress] 3d\n o #3801 Add OAuth2 PKCE support 12d\n o #3800 Session expiry not respecting timezone 45d\n\n -- Active MRs (2) ------------------------------------------------------\n <-> !456 Implement refresh token rotation @jdoe [draft] 1d\n <-> !443 Add PKCE flow to auth middleware @asmith 5d\n\n -- Experts (3) ----------------------------------------------------------\n @teernisse 42 pts last active 2d ago\n @jdoe 28 pts last active 5d ago\n @asmith 15 pts last active 12d ago\n\n -- Recent Activity (5) -------------------------------------------------\n note #3864 \"Added retry logic for token refresh\" @jdoe 1d\n status #3864 reopened @teernisse 3d\n note !456 \"PKCE challenge method discussion\" @asmith 5d\n\n -- Unresolved Threads (2) ----------------------------------------------\n #3864 \"Should we invalidate all sessions on token rotation, or\n only the compromised one? The RFC recommends full rotation\n but our mobile clients would all disconnect.\" 5 notes 3d\n !456 \"PKCE code verifier length seems short at 43 chars,\n RFC 7636 recommends 43-128. Should we bump to 128\n for extra security margin?\" 2 notes 1d\n\n -- Related (3) ----------------------------------------------------------\n #3750 Session management overhaul 0.85 12d\n !412 Add CORS headers for auth endpoints 0.72 20d\n #3699 Password reset flow broken 0.68 30d\n\n ! Issue #3800 has no activity for 45 days\n ! Issue #3801 is unassigned\n```\n\n(Unicode/Nerd glyphs render in place of ASCII placeholders above)\n\n## Robot Mode Output Schema\n```json\n{\n \"ok\": true,\n \"data\": {\n \"mode\": \"topic\",\n \"query\": \"authentication\",\n \"summary\": \"3 open issues, 2 active MRs, top expert: teernisse\",\n \"open_issues\": [{ \"iid\": 123, \"title\": \"...\", \"state\": \"opened\", \"assignees\": [...], \"updated_at\": \"...\", \"labels\": [...] }],\n \"active_mrs\": [{ \"iid\": 456, \"title\": \"...\", \"state\": \"opened\", \"author\": \"...\", \"draft\": false, \"updated_at\": \"...\" }],\n \"experts\": [{ \"username\": \"teernisse\", \"score\": 42, \"last_activity\": \"...\" }],\n \"recent_activity\": [{ \"timestamp\": \"...\", \"event_type\": \"state_change\", \"entity_ref\": \"issues#123\", \"summary\": \"...\", \"actor\": \"...\" }],\n \"unresolved_threads\": [{ \"discussion_id\": \"abc\", \"entity_ref\": \"issues#123\", \"started_by\": \"...\", \"note_count\": 5, \"last_note_at\": \"...\", \"body\": \"full note text, never truncated\" }],\n \"related\": [{ \"iid\": 789, \"title\": \"...\", \"similarity_score\": 0.85 }],\n \"warnings\": [\"Issue #3800 has no activity for 45 days\"]\n },\n \"meta\": { \"elapsed_ms\": 1200, \"sections_computed\": [\"open_issues\", \"active_mrs\", \"experts\", \"recent_activity\"] }\n}\n```\n\n## Clap Registration\n```rust\n// In src/main.rs Commands enum, add:\nBrief {\n /// Free-text topic, entity type, or omit for project-wide brief\n query: Option,\n /// Focus on a file path (who expert mode)\n #[arg(long)]\n path: Option,\n /// Focus on a person (who workload mode)\n #[arg(long)]\n person: Option,\n /// Scope to project (fuzzy match)\n #[arg(short, long)]\n project: Option,\n /// Maximum items per section\n #[arg(long, default_value = \"5\")]\n section_limit: usize,\n},\n```\n\n## TDD Loop\nRED: Tests in src/cli/commands/brief.rs (or brief/mod.rs):\n- test_brief_topic_returns_all_sections: insert test data, search 'auth', assert all section keys present in response\n- test_brief_path_uses_who_expert: brief --path src/auth/, assert experts section populated\n- test_brief_person_uses_who_workload: brief --person user, assert open_issues filtered to user's assignments\n- test_brief_warnings_stale_issue: insert issue with updated_at > 30 days ago, assert warning generated\n- test_brief_token_budget: robot mode output for topic query is under 12000 bytes (~3000 tokens)\n- test_brief_no_embeddings_graceful: related section omitted (not errored) when no embeddings exist\n- test_brief_empty_topic: zero matches returns valid JSON with empty arrays + \"No data found\" summary\n- test_list_filters_iids: verify ListFilters with iids field correctly filters to matching IIDs only\n\nGREEN: Implement brief with section assembly, calling existing run_* functions\n\nVERIFY:\n```bash\ncargo test brief:: && cargo clippy --all-targets -- -D warnings\ncargo run --release -- -J brief 'throw time' | jq '.data | keys'\ncargo run --release -- -J brief 'throw time' | wc -c # target <12000\n```\n\n## Acceptance Criteria\n- [ ] lore brief TOPIC returns all sections for free-text topic\n- [ ] lore brief --path PATH returns path-focused briefing with experts\n- [ ] lore brief --person USERNAME returns person-focused briefing\n- [ ] lore brief issues N returns entity-focused briefing\n- [ ] Robot mode output under 12000 bytes (~3000 tokens)\n- [ ] Each section degrades gracefully if its data source is unavailable\n- [ ] summary field is auto-generated one-liner from section counts\n- [ ] warnings detect: stale issues (>30d), unassigned, no due date\n- [ ] Performance: <2s total (acceptable since composing multiple queries)\n- [ ] Command registered in main.rs and robot-docs\n- [ ] Human output uses lipgloss Theme/Table/section_divider/Icons from render.rs\n- [ ] Human output never truncates note/discussion body text\n- [ ] Empty sections omitted in human output (not shown with count 0)\n- [ ] ListFilters and MrListFilters gain `iids: Option<&'a [i64]>` field\n\n## Edge Cases\n- Topic with zero matches: return empty sections + \"No data found for this topic\" summary\n- Path that nobody has touched: experts empty, related may still have results\n- Person not found in DB: exit code 17 with suggestion\n- All sections empty: still return valid JSON with empty arrays\n- Very broad topic (\"the\"): may return too many results — each section respects its limit cap\n\n## Dependencies\n- Hybrid search (bd-1ksf) for topic relevance ranking (optional — falls back to FTS-only)\n- lore who (shipped) for expertise\n- lore related (bd-8con, SHIPPED/CLOSED) for semantic connections\n- Timeline pipeline (shipped) for recent activity\n\n## Dependency Context\n- **bd-1ksf (hybrid search)**: Provides `search_hybrid()` which brief uses for topic mode to find relevant entities. Without it, topic mode falls back to FTS-only via `run_search()`.\n- **bd-8con (related)**: CLOSED. `run_related()` exists at `src/cli/commands/related.rs:92`. No longer a blocker.\n\n## Files to Create/Modify\n- NEW: src/cli/commands/brief/mod.rs (or brief.rs)\n- NEW: src/cli/commands/brief/render_human.rs (following me/render_human.rs pattern)\n- MODIFY: src/cli/commands/list/issues.rs (add `iids: Option<&'a [i64]>` to ListFilters, add WHERE clause)\n- MODIFY: src/cli/commands/list/mrs.rs (add `iids: Option<&'a [i64]>` to MrListFilters, add WHERE clause)\n- MODIFY: src/cli/commands/mod.rs (add pub mod brief; re-export)\n- MODIFY: src/main.rs (register Brief subcommand in Commands enum, add handle_brief dispatch)\n- Reuse: list.rs, who.rs, timeline.rs, search.rs, related.rs internals","status":"open","priority":2,"issue_type":"epic","created_at":"2026-02-12T15:47:22.893231Z","created_by":"tayloreernisse","updated_at":"2026-03-13T15:12:38.004221Z","compaction_level":0,"original_size":0,"labels":["cli-imp","intelligence"],"dependencies":[{"issue_id":"bd-1n5q","depends_on_id":"bd-13lp","type":"parent-child","created_at":"2026-03-04T20:00:21Z","created_by":"import"}]} {"id":"bd-1nf","title":"Register 'lore timeline' command with all flags","description":"## Background\n\nThis bead wires the `lore timeline` command into the CLI — adding the subcommand to the Commands enum, defining all flags, registering in VALID_COMMANDS, and dispatching to the timeline handler. The actual query logic and rendering are in separate beads.\n\n**Spec reference:** `docs/phase-b-temporal-intelligence.md` Section 3.1 (Command Design).\n\n## Codebase Context\n\n- Commands enum in `src/cli/mod.rs` (line ~86): uses #[derive(Subcommand)] with nested Args structs\n- VALID_COMMANDS in `src/main.rs` (line ~448): &[&str] array for fuzzy command matching\n- Handler dispatch in `src/main.rs` match on Commands:: variants\n- robot-docs manifest in `src/main.rs`: registers commands for `lore robot-docs` output\n- Existing pattern: `Sync(SyncArgs)`, `Search(SearchArgs)`, etc.\n- No timeline module exists yet — this bead creates the CLI entry point only\n\n## Approach\n\n### 1. TimelineArgs struct (`src/cli/mod.rs`):\n\n```rust\n/// Show a chronological timeline of events matching a query\n#[derive(Parser, Debug)]\npub struct TimelineArgs {\n /// Search query (keywords to find in issues, MRs, and discussions)\n pub query: String,\n\n /// Scope to a specific project (fuzzy match)\n #[arg(short = 'p', long)]\n pub project: Option,\n\n /// Only show events after this date (e.g. \"6m\", \"2w\", \"2024-01-01\")\n #[arg(long)]\n pub since: Option,\n\n /// Cross-reference expansion depth (0 = no expansion)\n #[arg(long, default_value = \"1\")]\n pub depth: usize,\n\n /// Also follow 'mentioned' edges during expansion (high fan-out)\n #[arg(long = \"expand-mentions\")]\n pub expand_mentions: bool,\n\n /// Maximum number of events to display\n #[arg(short = 'n', long = \"limit\", default_value = \"100\")]\n pub limit: usize,\n}\n```\n\n### 2. Commands enum variant:\n\n```rust\n/// Show a chronological timeline of events matching a query\n#[command(name = \"timeline\")]\nTimeline(TimelineArgs),\n```\n\n### 3. Handler in `src/main.rs`:\n\n```rust\nCommands::Timeline(args) => {\n // Placeholder: will be filled by bd-2f2 (human) and bd-dty (robot)\n // For now: resolve project, call timeline query, dispatch to renderer\n}\n```\n\n### 4. VALID_COMMANDS: add `\"timeline\"` to the array\n\n### 5. robot-docs: add timeline command description to manifest\n\n## Acceptance Criteria\n\n- [ ] `TimelineArgs` struct with all 6 flags: query, project, since, depth, expand-mentions, limit\n- [ ] Commands::Timeline variant registered in Commands enum\n- [ ] Handler stub in src/main.rs dispatches to timeline logic\n- [ ] `\"timeline\"` added to VALID_COMMANDS array\n- [ ] robot-docs manifest includes timeline command description\n- [ ] `lore timeline --help` shows correct help text\n- [ ] `lore timeline` without query shows error (query is required positional)\n- [ ] `cargo check --all-targets` passes\n- [ ] `cargo clippy --all-targets -- -D warnings` passes\n\n## Files\n\n- `src/cli/mod.rs` (TimelineArgs struct + Commands::Timeline variant)\n- `src/main.rs` (handler dispatch + VALID_COMMANDS + robot-docs entry)\n\n## TDD Loop\n\nNo unit tests for CLI wiring. Verify with:\n\n```bash\ncargo check --all-targets\ncargo run -- timeline --help\n```\n\n## Edge Cases\n\n- --since parsing: reuse existing date parsing from ListFilters (src/cli/mod.rs handles \"7d\", \"2w\", \"YYYY-MM-DD\")\n- --depth 0: valid, means no cross-reference expansion\n- --expand-mentions: off by default because mentioned edges have high fan-out\n","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-02T21:33:28.422082Z","created_by":"tayloreernisse","updated_at":"2026-02-06T13:49:15.313047Z","closed_at":"2026-02-06T13:49:15.312993Z","close_reason":"Wired lore timeline command: TimelineArgs with 9 flags, Commands::Timeline variant, handle_timeline handler, VALID_COMMANDS entry, robot-docs manifest with temporal_intelligence workflow","compaction_level":0,"original_size":0,"labels":["cli","gate-3","phase-b"],"dependencies":[{"issue_id":"bd-1nf","depends_on_id":"bd-2f2","type":"blocks","created_at":"2026-03-04T20:00:21Z","created_by":"import"},{"issue_id":"bd-1nf","depends_on_id":"bd-dty","type":"blocks","created_at":"2026-03-04T20:00:21Z","created_by":"import"},{"issue_id":"bd-1nf","depends_on_id":"bd-ike","type":"parent-child","created_at":"2026-03-04T20:00:21Z","created_by":"import"}]} {"id":"bd-1np","title":"[CP1] GitLab types for issues, discussions, notes","description":"## Background\n\nGitLab types define the Rust structs for deserializing GitLab API responses. These types are the foundation for all ingestion work - issues, discussions, and notes must be correctly typed for serde to parse them.\n\n## Approach\n\nAdd types to `src/gitlab/types.rs` with serde derives:\n\n### GitLabIssue\n\n```rust\n#[derive(Debug, Clone, Deserialize)]\npub struct GitLabIssue {\n pub id: i64, // GitLab global ID\n pub iid: i64, // Project-scoped issue number\n pub project_id: i64,\n pub title: String,\n pub description: Option,\n pub state: String, // \"opened\" | \"closed\"\n pub created_at: String, // ISO 8601\n pub updated_at: String, // ISO 8601\n pub closed_at: Option,\n pub author: GitLabAuthor,\n pub labels: Vec, // Array of label names (CP1 canonical)\n pub web_url: String,\n}\n```\n\nNOTE: `labels_details` intentionally NOT modeled - varies across GitLab versions.\n\n### GitLabAuthor\n\n```rust\n#[derive(Debug, Clone, Deserialize)]\npub struct GitLabAuthor {\n pub id: i64,\n pub username: String,\n pub name: String,\n}\n```\n\n### GitLabDiscussion\n\n```rust\n#[derive(Debug, Clone, Deserialize)]\npub struct GitLabDiscussion {\n pub id: String, // String ID like \"6a9c1750b37d...\"\n pub individual_note: bool, // true = standalone comment\n pub notes: Vec,\n}\n```\n\n### GitLabNote\n\n```rust\n#[derive(Debug, Clone, Deserialize)]\npub struct GitLabNote {\n pub id: i64,\n #[serde(rename = \"type\")]\n pub note_type: Option, // \"DiscussionNote\" | \"DiffNote\" | null\n pub body: String,\n pub author: GitLabAuthor,\n pub created_at: String, // ISO 8601\n pub updated_at: String, // ISO 8601\n pub system: bool, // true for system-generated notes\n #[serde(default)]\n pub resolvable: bool,\n #[serde(default)]\n pub resolved: bool,\n pub resolved_by: Option,\n pub resolved_at: Option,\n pub position: Option,\n}\n```\n\n### GitLabNotePosition\n\n```rust\n#[derive(Debug, Clone, Deserialize)]\npub struct GitLabNotePosition {\n pub old_path: Option,\n pub new_path: Option,\n pub old_line: Option,\n pub new_line: Option,\n}\n```\n\n## Acceptance Criteria\n\n- [ ] GitLabIssue deserializes from API response JSON\n- [ ] GitLabAuthor embedded correctly in issue and note\n- [ ] GitLabDiscussion with notes array deserializes\n- [ ] GitLabNote handles null note_type (use Option)\n- [ ] GitLabNote uses #[serde(rename = \"type\")] for reserved keyword\n- [ ] resolvable/resolved default to false via #[serde(default)]\n- [ ] All timestamp fields are String (ISO 8601 parsed elsewhere)\n\n## Files\n\n- src/gitlab/types.rs (edit - add types)\n\n## TDD Loop\n\nRED:\n```rust\n// tests/gitlab_types_tests.rs\n#[test] fn deserializes_gitlab_issue_from_json()\n#[test] fn deserializes_gitlab_discussion_from_json()\n#[test] fn handles_null_note_type()\n#[test] fn handles_missing_resolvable_field()\n#[test] fn deserializes_labels_as_string_array()\n```\n\nGREEN: Add type definitions with serde attributes\n\nVERIFY: `cargo test gitlab_types`\n\n## Edge Cases\n\n- note_type can be null, \"DiscussionNote\", or \"DiffNote\"\n- labels array can be empty\n- description can be null\n- resolved_by/resolved_at can be null\n- position is only present for DiffNotes","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-25T17:02:38.150472Z","created_by":"tayloreernisse","updated_at":"2026-01-25T22:17:08.842965Z","closed_at":"2026-01-25T22:17:08.842895Z","close_reason":"Implemented GitLabAuthor, GitLabIssue, GitLabDiscussion, GitLabNote, GitLabNotePosition types with 10 passing tests","compaction_level":0,"original_size":0} {"id":"bd-1nsl","title":"Epic: Surgical Per-IID Sync","description":"## Background\n\nSurgical Per-IID Sync adds `lore sync --issue --mr -p ` for on-demand sync of specific entities without running the full project-wide pipeline. This is critical for agent workflows: when an agent needs fresh data for a specific issue or MR, waiting for a full sync (minutes) is unacceptable. Surgical sync completes in seconds by fetching only the targeted entities, their discussions, resource events, and dependent data, then scoping doc regeneration and embedding to only the affected documents.\n\n## Architecture\n\nThe pipeline stages mirror full sync but scoped to individual entities:\n\n```\nPREFLIGHT -> TOCTOU CHECK -> INGEST -> DEPENDENTS -> DOCS -> EMBED -> FINALIZE\n```\n\n- **Preflight**: Fetch entity from GitLab API by IID, confirm existence\n- **TOCTOU check**: Compare payload `updated_at` with DB — skip if already current\n- **Ingest**: Upsert entity via existing `process_single_issue`/`process_single_mr`\n- **Dependents**: Inline fetch of discussions, resource events, MR diffs, closes_issues\n- **Docs**: Scoped `regenerate_dirty_documents_for_sources()` — only affected source keys\n- **Embed**: Scoped `embed_documents_by_ids()` — only regenerated document IDs\n- **Finalize**: SyncRunRecorder with surgical mode columns\n\n## Children (Execution Order)\n\n### Foundation (no blockers, can parallelize)\n1. **bd-tiux** — Migration 027: surgical mode columns on sync_runs\n2. **bd-1sc6** — Error variant + pub(crate) visibility changes\n3. **bd-159p** — GitLab client get_by_iid methods\n4. **bd-1lja** — CLI flags + SyncOptions extensions\n\n### Core (blocked by foundation)\n5. **bd-wcja** — SyncResult surgical fields (blocked by bd-3sez)\n6. **bd-arka** — SyncRunRecorder surgical lifecycle (blocked by bd-tiux)\n7. **bd-3sez** — surgical.rs core module + tests (blocked by bd-159p, bd-1sc6)\n8. **bd-hs6j** — Scoped doc regeneration (no blockers)\n9. **bd-1elx** — Scoped embedding (no blockers)\n\n### Orchestration (blocked by core)\n10. **bd-kanh** — Per-entity dependent helpers (blocked by bd-3sez)\n11. **bd-1i4i** — Orchestrator function (blocked by all core beads)\n\n### Wiring + Validation\n12. **bd-3bec** — Wire dispatch in run_sync + robot-docs (blocked by bd-1i4i)\n13. **bd-3jqx** — Integration tests (blocked by bd-1i4i + core beads)\n\n## Completion Criteria\n\n- [ ] `lore sync --issue 7 -p group/project` fetches, ingests, and reports for issue 7 only\n- [ ] `lore sync --mr 101 --mr 102 -p proj` handles multiple MRs\n- [ ] `lore sync --preflight-only --issue 7 -p proj` validates without DB writes\n- [ ] Robot mode JSON includes `surgical_mode`, `surgical_iids`, `entity_results`\n- [ ] TOCTOU: already-current entities are skipped (not re-ingested)\n- [ ] Scoped docs + embed: only affected documents are regenerated and embedded\n- [ ] Cancellation at any stage stops gracefully with partial results\n- [ ] `lore robot-docs` documents all surgical flags and response schemas\n- [ ] All existing full-sync tests pass unchanged\n- [ ] Integration test suite (bd-3jqx) passes","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-02-17T19:11:34.020453Z","created_by":"tayloreernisse","updated_at":"2026-02-18T21:38:02.294242Z","closed_at":"2026-02-18T21:38:02.294190Z","close_reason":"All children shipped. Surgical per-IID sync landed in 9ec1344.","compaction_level":0,"original_size":0,"labels":["surgical-sync"]} diff --git a/.beads/last-touched b/.beads/last-touched index e3315cb..168b9ce 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -bd-1lj5 +bd-1n5q