feat(logic): establish derived-activity engine and agent-session protocols
Today we reached a major architectural conclusion: project history shouldn't be stored, it should be derived. We rejected the overhead of a separate SQLite event store in favor of an O(N) snapshot-diffing engine that computes human-readable narratives directly from the issues.jsonl source of truth. Key Triumphs: - Implemented O(N) diffing algorithm in src/lib/snapshot-differ.ts that transforms raw JSONL into 16 distinct social event types. - Engineered a file-based persistence layer (src/lib/activity-persistence.ts) to solve the 'Next.js HMR Wiped My Memory' bug, ensuring project heartbeat survives server restarts. - Developed the agent-session data model that unifies Beads, Activity, and Cross-Agent Mail into a single 'Mission' context. Raw Honest Moment: We struggled for over an hour with 'missing history' before realizing that development-mode reloads were purging our in-memory buffers. The shift to a file-backed ring buffer was a reactive pivot that became a core project strength.
This commit is contained in:
parent
4f8f3006e9
commit
ab051952bd
12 changed files with 1923 additions and 27 deletions
|
|
@ -3,12 +3,12 @@
|
|||
{"id":"bb-18e.1","title":"Add cycle warning card with focus actions in graph view","description":"Goal:\nWhen dependency cycles exist, show an explicit cycle warning card so users understand why work may be deadlocked.\n\nProblem:\nCycle states are hard to infer from dense node/edge layouts, leading to confusion (“why can’t this move?”).\n\nBehavior contract:\n- Show warning card only when cycle analysis is non-empty.\n- Card includes:\n - cycle count\n - affected bead ids (compact list)\n - click-to-focus action for each cycle group\n - plain language explanation of impact (“tasks in this loop cannot fully unblock each other without breaking the cycle”).\n- Visual style: warning but not alarmist (amber/red subtle).\n\nImplementation tasks:\n1) Build compact cycle summary model from existing detection output.\n2) Add warning card component above graph viewport.\n3) Wire click handlers to focus selected cycle nodes.\n4) Add tests for no-cycle and multi-cycle rendering behavior.\n\nOut of scope:\n- Automatic cycle resolution suggestions.\n- Mutation/write automation.","acceptance_criteria":"- Cycle warning card appears only when cycles are present.\n- Card provides actionable cycle navigation.\n- Language is plain and explains user impact.\n- Tests cover empty and non-empty cycle states.\n- Typecheck and graph guards pass.","notes":"This is the #19 idea captured as an explicit implementation bead.\nDepends on existing cycle analysis primitives already implemented.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T19:46:01.2478576-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T19:46:01.2478576-08:00","labels":["anomaly","graph","ux"],"dependencies":[{"issue_id":"bb-18e.1","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T19:46:01.2494327-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.1","depends_on_id":"bb-1es","type":"blocks","created_at":"2026-02-12T19:53:11.9910819-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.10","title":"Add downstream-impact risk tinting","description":"Add subtle risk tinting based on downstream impact count to highlight high-blast-radius tasks.","acceptance_criteria":"- Higher downstream impact gets stronger but subtle visual signal.\n- Does not overpower status/selection color language.\n- Works with existing legend semantics.","notes":"Use restrained styling; no heavy borders.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:21.812041-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:21:21.812041-08:00","labels":["graph","signal","ux"],"dependencies":[{"issue_id":"bb-18e.10","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:21.8153577-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.10","depends_on_id":"bb-18e.4","type":"blocks","created_at":"2026-02-12T20:21:43.4643033-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.11","title":"AI dependency explanation scaffold (deferred)","description":"Prepare integration scaffold for later AI explanation in dependency view (why blocked / next steps), without shipping model calls yet.","acceptance_criteria":"- Data contract for AI explanation input is defined.\n- UI placeholder state exists but feature-flagged/off by default.\n- No network/model dependency in this bead.","notes":"Deferred feature: implement only scaffolding and interfaces.","status":"closed","priority":3,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:22.4738465-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T23:35:59.2331675-08:00","closed_at":"2026-02-12T23:35:59.2331675-08:00","labels":["ai","backlog","graph"],"dependencies":[{"issue_id":"bb-18e.11","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:22.4799753-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.11","depends_on_id":"bb-18e.9","type":"blocks","created_at":"2026-02-12T20:21:44.0206618-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.11","depends_on_id":"bb-18e.1","type":"blocks","created_at":"2026-02-12T20:21:44.590355-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.2","title":"Add plain-English edge labels + contrast upgrade","description":"Improve edge readability by labeling relationships in plain language (blocks/parent/related) and increasing contrast for fast scan.\nScope: graph viewport only; no mutation behavior changes.","acceptance_criteria":"- Edge labels are visible and readable at default zoom.\n- Labels map correctly to relation type.\n- Contrast remains accessible on dark background.","notes":"Verification: typecheck + graph responsive guard + visual screenshot at 390/768/1440.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:16.9461643-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:21:16.9461643-08:00","labels":["graph","readability","ux"],"dependencies":[{"issue_id":"bb-18e.2","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:16.9493723-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.2","title":"Add plain-English edge labels + contrast upgrade","description":"Improve edge readability by labeling relationships in plain language (blocks/parent/related) and increasing contrast for fast scan.\nScope: graph viewport only; no mutation behavior changes.","acceptance_criteria":"- Edge labels are visible and readable at default zoom.\n- Labels map correctly to relation type.\n- Contrast remains accessible on dark background.","notes":"Verification: typecheck + graph responsive guard + visual screenshot at 390/768/1440.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:16.9461643-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T20:52:38.0726471-08:00","labels":["graph","readability","ux"],"dependencies":[{"issue_id":"bb-18e.2","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:16.9493723-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.3","title":"Add directional context hints for dependency reading","description":"Add concise orientation hints explaining graph reading order (left prerequisites, right downstream impact).\nPlace hints near graph legend and keep copy plain.","acceptance_criteria":"- Direction hint appears in graph UI.\n- Hint remains visible and non-intrusive on mobile and desktop.","notes":"Added plain-language directional hint in Graph legend: 'Read left to right: Left = blockers, middle = selected task, Right = work this task unblocks.' Also added guard contract assertions in tests/guards/graph-responsive-contract.test.mjs to prevent regressions.","status":"closed","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:17.525886-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T23:47:50.0333842-08:00","closed_at":"2026-02-12T23:47:50.0333842-08:00","close_reason":"Directional context hint shipped with guard coverage; visibility and wording verified on responsive graph layout.","labels":["graph","orientation","ux"],"dependencies":[{"issue_id":"bb-18e.3","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:17.5284994-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.4","title":"Add edge-type toggles to reduce graph noise","description":"Add controls to show/hide edge categories (blocks,parent,related) so users can simplify complex views.","acceptance_criteria":"- Users can toggle edge categories independently.\n- Default preserves current behavior.\n- Toggle state updates graph without runtime errors.","notes":"Include test coverage for toggle behavior.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:18.1326942-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:21:18.1326942-08:00","labels":["controls","graph","ux"],"dependencies":[{"issue_id":"bb-18e.4","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:18.1347832-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.4","depends_on_id":"bb-18e.2","type":"blocks","created_at":"2026-02-12T20:21:39.3944582-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.5","title":"Add external-blockers-only filter","description":"Add a filter that shows only blockers outside selected epic/task context to focus on cross-epic constraints.","acceptance_criteria":"- Filter clearly isolates external blockers.\n- Selected/focus node remains visible.\n- UX works on mobile and desktop.","notes":"Must degrade gracefully if no external blockers exist.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:18.7705681-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:21:18.7705681-08:00","labels":["dependencies","filters","graph"],"dependencies":[{"issue_id":"bb-18e.5","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:18.7726627-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.5","depends_on_id":"bb-18e.4","type":"blocks","created_at":"2026-02-12T20:21:41.1088626-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.6","title":"Keep selected node centered during focus navigation","description":"Refine viewport behavior so selected node remains centered/predictable when user selects tasks or changes depth.","acceptance_criteria":"- Selection keeps focus node in stable viewport position.\n- No clipping/bleed regressions.\n- Fit behavior remains bounded.","notes":"Add tests for focus/viewport contract where feasible.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:19.3791473-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:21:19.3791473-08:00","labels":["graph","interaction","ux"],"dependencies":[{"issue_id":"bb-18e.6","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:19.3807243-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.6","depends_on_id":"bb-18e.2","type":"blocks","created_at":"2026-02-12T20:21:39.9439739-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.7","title":"Progressive disclosure in graph task details panel","description":"Refactor graph details panel to show summary first and collapse secondary metadata under explicit expand control.","acceptance_criteria":"- Primary summary is immediately readable.\n- Secondary fields are accessible via expand action.\n- Mobile detail experience stays compact.","notes":"Do not remove any existing information; only restructure hierarchy.","status":"in_progress","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:20.0136797-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T22:02:52.9759525-08:00","labels":["details","graph","ux"],"dependencies":[{"issue_id":"bb-18e.7","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:20.0164851-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.7","depends_on_id":"bb-18e.3","type":"blocks","created_at":"2026-02-12T20:21:40.5255149-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.7","title":"Progressive disclosure in graph task details panel","description":"Refactor graph details panel to show summary first and collapse secondary metadata under explicit expand control. thsi ws makred as inprogress before , the er may be work already done, polease check first.","acceptance_criteria":"- Primary summary is immediately readable.\n- Secondary fields are accessible via expand action.\n- Mobile detail experience stays compact.","notes":"Do not remove any existing information; only restructure hierarchy.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:20.0136797-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T16:02:48.0816548-08:00","labels":["details","graph","ux"],"dependencies":[{"issue_id":"bb-18e.7","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:20.0164851-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.7","depends_on_id":"bb-18e.3","type":"blocks","created_at":"2026-02-12T20:21:40.5255149-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.8","title":"Add graph keyboard navigation shortcuts","description":"Add keyboard navigation for graph workflow (e.g., next/prev task, open flow/overview, focus search).","acceptance_criteria":"- Shortcuts work without interfering with text inputs.\n- Shortcut list documented in UI/help hint.\n- Accessibility remains intact.","notes":"Treat as later-phase productivity enhancement after core clarity features.","status":"open","priority":3,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:20.617034-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:21:20.617034-08:00","labels":["accessibility","graph","productivity"],"dependencies":[{"issue_id":"bb-18e.8","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:20.6196393-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.8","depends_on_id":"bb-18e.6","type":"blocks","created_at":"2026-02-12T20:21:41.7395727-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.8","depends_on_id":"bb-18e.7","type":"blocks","created_at":"2026-02-12T20:21:42.3306409-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-18e.9","title":"Persist graph state in URL params","description":"Persist selected epic/task/tab/depth/filter state in URL so refresh/share restores context.","acceptance_criteria":"- Reload restores graph context from URL.\n- Shared URL opens same state deterministically.\n- Invalid params fail safely to defaults.","notes":"Add route/param parsing tests.","status":"in_progress","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:21:21.2077039-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:58:56.9471903-08:00","labels":["graph","routing","ux"],"dependencies":[{"issue_id":"bb-18e.9","depends_on_id":"bb-18e","type":"parent-child","created_at":"2026-02-12T20:21:21.2103973-08:00","created_by":"zenchantlive"},{"issue_id":"bb-18e.9","depends_on_id":"bb-18e.6","type":"blocks","created_at":"2026-02-12T20:21:42.8897325-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-1es","title":"Kanban Actionability \u0026 Execution Clarity","description":"Objective:\nCreate a focused Kanban polish epic that improves execution clarity and triage speed without expanding scope into graph rendering changes.\n\nScope boundaries:\n- In scope: Kanban page only (`/`), especially lane workflow, task card signal density, and detail panel actionability.\n- Out of scope: Dependency graph edge visuals/layout (tracked separately), AI-generated summaries (future bead), keyboard system-wide shortcuts (future bead).\n\nUser outcomes this epic must deliver:\n1) Users can immediately identify what to pick next.\n2) Users can quickly understand impact (what this task unblocks).\n3) Users can evaluate readiness from one details panel without context switching.\n4) Users can triage by recency and urgency with minimal cognitive load.\n\nExecution plan:\n- Phase A: Add Next Actionable workflow entrypoint.\n- Phase B: Improve card signal density (recency + unblocks count).\n- Phase C: Add execution checklist to details panel.\n- Phase D: Verify responsive behavior and no regressions on write/mutation flow.\n\nNon-negotiables:\n- Maintain strict read/write boundary (no direct JSONL writes).\n- Preserve existing mutation semantics via bd bridge.\n- Keep mobile layout readable and avoid extra vertical clutter.\n- Evidence-first completion: tests + visual proof.","acceptance_criteria":"- Kanban has a deterministic “next actionable” affordance and it selects a valid ready task.\n- Task cards expose recency and unblock-impact signals without overwhelming visual noise.\n- Details panel includes concise execution checklist with clear pass/fail indicators.\n- All additions are responsive and do not regress existing lane filtering or detail drawer behavior.\n- Typecheck and tests pass.","notes":"Planning contract:\n- This epic is implementation-focused and should be executed through child beads.\n- Child beads must include explicit UX contract, test updates, and verification commands.\n- AI summary concept is deferred to a later phase after core UX/actionability stabilizes.\nExecution sequencing updated: bb-1es.1 is foundational and now blocks bb-1es.3 + bb-1es.4. Recommended implementation order: bb-1es.1 -\u003e bb-1es.3 -\u003e bb-1es.4 -\u003e bb-1es.2.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T19:44:06.0783399-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T20:16:55.5910638-08:00","closed_at":"2026-02-12T20:16:55.5910638-08:00","close_reason":"All child beads complete and verified (typecheck + kanban tests + kanban guard contract).","labels":["kanban","ux","workflow"]}
|
||||
|
|
@ -22,8 +22,10 @@
|
|||
{"id":"bb-29x.3","title":"Record parser and realtime performance baseline against PRD targets","description":"Measure parse latency and update propagation using realistic sample sizes and document outcomes.","acceptance_criteria":"Performance report exists with methodology and observed timings.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:18.3210495-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:18.3210495-08:00","labels":["benchmark","perf"],"dependencies":[{"issue_id":"bb-29x.3","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:18.3220949-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.3","depends_on_id":"bb-29x.2","type":"blocks","created_at":"2026-02-11T17:12:39.4534943-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.3","depends_on_id":"bb-29x.5","type":"blocks","created_at":"2026-02-11T20:10:13.1864837-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-29x.4","title":"Document operational runbook and boundary rationale","description":"Write architecture docs covering scanner policy, bd bridge behavior, and consistency guardrails for future maintainers.","acceptance_criteria":"Runbook documents startup, troubleshooting, and boundary rules.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:19.1385778-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:19.1385778-08:00","labels":["docs","runbook"],"dependencies":[{"issue_id":"bb-29x.4","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T17:12:19.1402086-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.4","depends_on_id":"bb-29x.2","type":"blocks","created_at":"2026-02-11T17:12:39.9591458-08:00","created_by":"zenchantlive"},{"issue_id":"bb-29x.4","depends_on_id":"bb-29x.5","type":"blocks","created_at":"2026-02-11T20:10:12.3474801-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-29x.5","title":"Epic Design Gate: scope, decisions, and acceptance contract","description":"Design/discovery gate for bb-29x before further implementation.\n\nMust capture:\n- Product intent and user outcomes for this epic\n- Explicit architecture decisions and tradeoffs\n- API/data contracts and edge cases\n- Windows-specific constraints and path/process assumptions\n- Test strategy and verification commands\n- Non-goals and out-of-scope boundaries\n\nCompletion rule:\nDo not start new implementation tasks in this epic until this gate is closed with agreed decisions.","acceptance_criteria":"A written execution-grade plan exists for this epic and all child task descriptions are updated with concrete implementation details, dependencies, and testable acceptance criteria.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T20:09:42.1507616-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:09:42.1507616-08:00","dependencies":[{"issue_id":"bb-29x.5","depends_on_id":"bb-29x","type":"parent-child","created_at":"2026-02-11T20:09:42.1525436-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-2mx","title":"Deep validation and edge-case testing of Snapshot Diffing engine","description":"Perform exhaustive verification of the snapshot diffing logic in src/lib/snapshot-differ.ts. While the core O(N) algorithm is implemented and handles basic transitions, we must stress-test the engine against complex real-world scenarios to ensure the 'Tale of the Project' remains perfectly accurate. Scope includes: (1) High-frequency update bursts (multiple saves within 50ms), (2) Massive batch mutations where 50+ beads are updated in a single sync, (3) Complex state permutations like simultaneous status and dependency changes, and (4) Resiliency testing against transient file-locks or malformed JSONL lines during the diffing window.","notes":"TESTING STRATEGY: We need to develop a dedicated stress-test suite in tests/lib/snapshot-differ-stress.test.ts. This should simulate rapid disk writes and verify that the ActivityEventBus correctly deduplicates redundant events while capturing every meaningful state transition. We must also verify 'History Drift' prevention—ensuring that if a file write is interrupted, the next successful diff correctly reconciles the missing gaps without creating duplicate entries.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-14T00:16:24.3937657-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:16:24.3937657-08:00","dependencies":[{"issue_id":"bb-2mx","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-14T00:17:13.5661-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-3pr","title":"Smoke test mutation lifecycle 2","description":"Temporary issue for API mutation smoke test","status":"closed","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T19:44:10.9737485-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T19:44:16.4912473-08:00","closed_at":"2026-02-11T19:44:16.4912473-08:00","close_reason":"Cleanup after API smoke test","labels":["api","smoke"],"comments":[{"id":1,"issue_id":"bb-3pr","author":"zenchantlive","text":"Smoke test comment via API route","created_at":"2026-02-12T03:44:13Z"},{"id":2,"issue_id":"bb-3pr","author":"zenchantlive","text":"Smoke test reopen","created_at":"2026-02-12T03:44:15Z"}]}
|
||||
{"id":"bb-3vi","title":"Fix misleading 'Blocking' label in task cards - should be 'Unlocks'","description":"In task-card-grid.tsx, the 'Blocking' section was showing outgoing blocking edges (tasks that this issue will unblock) but labeled incorrectly as 'Blocking'. Changed label to 'Unlocks' to correctly represent that this task, once completed, will unlock/unblock these downstream tasks.","notes":"Investigated: This is a bug in the bd CLI itself (C:\\tools\\beads\\bd.exe), not in this codebase. The issue detail view's 'BLOCKS' section displays which issues the current issue blocks, when it should display which issues block the current issue. The underlying dependency data is correct - this is purely a display/UI bug in the beads CLI.","status":"closed","priority":2,"issue_type":"bug","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T11:05:40.7518392-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T11:12:19.5922612-08:00","closed_at":"2026-02-13T11:12:19.5922612-08:00","close_reason":"Closed"}
|
||||
{"id":"bb-3wy","title":"Postmortem: stale bead status refresh regression and SSE recovery","description":"Reference record for stale status issue where BeadBoard required manual refresh after bd updates. Captures root causes, applied fixes, and verification commands for future triage.","acceptance_criteria":"Bead contains root cause timeline, exact files changed, and reproducible verification steps.","notes":"Root cause timeline:\\n1) Data freshness drift: UI read path consumed .beads/issues.jsonl, but bd updates could be newer in DB before JSONL sync.\\n2) Live update gap: SSE depended on file watcher events that did not reliably fire for external bd updates.\\n3) Fallback bug: last-touched polling compared file content; repeated updates on same issue kept content stable while only mtime changed.\\n\\nApplied fixes:\\n1) Prefer live bd reads with fallback to JSONL: src/lib/read-issues.ts, src/lib/aggregate-read.ts, src/app/page.tsx, src/app/graph/page.tsx, src/app/api/beads/read/route.ts.\\n2) Expand watcher targets to include .beads/beads.db-wal and .beads/last-touched: src/lib/watcher.ts.\\n3) Add /api/events fallback poll on last-touched mtime (not content): src/app/api/events/route.ts.\\n4) Add regression tests: tests/lib/watcher.test.ts (db + wal events).\\n\\nVerification commands:\\n- npm run typecheck\\n- npm run lint\\n- npm run test\\n- End-to-end probe: connect to /api/events then run \bd update bb-dcv.2 -s \u003cstatus\u003e and confirm \u001bvent: issues.\\n- Manual UI check: Kanban open, run bd update status toggles, confirm no full page refresh needed.\\n\\nOperational note for future agents:\\nIf behavior appears unchanged after patching /api/events, restart dev server to load route changes.","status":"closed","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T15:36:09.8136541-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T15:36:29.3940253-08:00","closed_at":"2026-02-13T15:36:29.3940253-08:00","close_reason":"Postmortem captured for stale status refresh regression, including root cause timeline, code-level fixes, verification commands, and operational restart note.","labels":["postmortem","realtime","sse","status"]}
|
||||
{"id":"bb-6aj","title":"Project Registry and Multi-Project Scanner","description":"Deliver a Windows-first multi-project registry and discovery pipeline: persist project roots in the user profile, expose add/remove/list APIs, and scan safe roots to find .beads directories. Normalize all paths to stable identity keys and support aggregate views without full-drive traversal by default.","acceptance_criteria":"Projects can be added/removed/listed and discovered via scanner with deterministic normalization.","notes":"UI productization backlog added (2026-02-12): bb-6aj.6 design gate -\u003e bb-6aj.7 shared scope state -\u003e bb-6aj.8 project manager panel + bb-6aj.9 scanner UX + bb-6aj.10 scoped reads -\u003e bb-6aj.11 aggregate mode -\u003e bb-6aj.12 verification evidence. This sequence turns existing backend scanner/registry foundations into end-user multi-project workflows.\n2026-02-13 epic completion: UI productization chain complete (bb-6aj.6 -\u003e .7 -\u003e .8/.9/.10 -\u003e .11 -\u003e .12). Multi-project scope selection, registry manager, scanner discover/import, mode-aware reads, aggregate mode with project badges, and full verification evidence are now in place.","status":"closed","priority":0,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:47.7205517-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T22:35:21.1595002-08:00","closed_at":"2026-02-12T22:35:21.1595002-08:00","close_reason":"multi-project-scanner-epic-complete","labels":["multi-project","scanner"],"dependencies":[{"issue_id":"bb-6aj","depends_on_id":"bb-92d","type":"blocks","created_at":"2026-02-11T17:12:19.6374139-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-6aj.1","title":"Persist project registry in %USERPROFILE%\\\\.beadboard\\\\projects.json","description":"Implement read/write management for registry file in user profile path, isolated from repository files and safe for local machine usage.","acceptance_criteria":"Registry file is created lazily and survives app restarts.","status":"closed","priority":0,"issue_type":"task","assignee":"agent-a","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:48.5403111-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:53:17.2085722-08:00","closed_at":"2026-02-11T17:53:17.2085722-08:00","close_reason":"Implemented %USERPROFILE%/.beadboard/projects.json registry persistence with Windows-safe normalization and dedupe.","labels":["config","registry"],"dependencies":[{"issue_id":"bb-6aj.1","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-11T17:11:48.5419102-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-6aj.10","title":"Wire project-scoped reads into Kanban and Graph","description":"Connect selected project scope to data-loading paths for Kanban and Graph pages.\\n\\nScope:\\n- pass selected project root to read APIs\\n- ensure page refresh keeps selected scope\\n- keep existing single-project behavior as fallback\\n- preserve strict read/write boundary contracts","acceptance_criteria":"Kanban and Graph render data for the selected project scope and remain stable when switching projects.","notes":"2026-02-13 completed: rewired / and /graph server pages to resolve project scope from URL and load issues with selected root; implemented readIssuesForScope utility for mode-aware reads; preserved strict read-only boundaries (no direct JSONL writes).","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T21:41:42.9381588-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T22:33:58.8681434-08:00","closed_at":"2026-02-12T22:33:58.8681434-08:00","close_reason":"project-scoped-reads-wired","labels":["graph","kanban","multi-project"],"dependencies":[{"issue_id":"bb-6aj.10","depends_on_id":"bb-6aj","type":"parent-child","created_at":"2026-02-12T21:41:42.9408199-08:00","created_by":"zenchantlive"},{"issue_id":"bb-6aj.10","depends_on_id":"bb-6aj.7","type":"blocks","created_at":"2026-02-12T21:41:42.9477322-08:00","created_by":"zenchantlive"}]}
|
||||
|
|
@ -81,16 +83,17 @@
|
|||
{"id":"bb-bvn.1","title":"Parse dependency edges and build adjacency structures","description":"Build graph data preparation pipeline for dependency workspace.\n\nScope:\n- Input: parsed Bead issues from read layer only (`readIssuesFromDisk`).\n- Build normalized node map keyed by issue id.\n- Build typed edge list from `dependencies[]` supporting: blocks, parent, relates_to, duplicates, supersedes.\n- Include reverse index (incoming/outgoing) to support focus queries.\n- Preserve issue metadata needed by UI nodes: id, title, status, priority, issue_type, assignee, updated_at.\n\nRules:\n- Ignore dependency edges that point to missing issue IDs but record count for diagnostics.\n- Deduplicate duplicate edges (same source, target, type).\n- Treat path/project context as explicit API argument for future multi-project support.\n- Do not mutate source issues.\n\nOutput contracts:\n- `GraphModel = { nodes, edges, adjacency, diagnostics }`\n- `adjacency` includes incoming/outgoing arrays per node.\n- `diagnostics` includes counts for missing targets and dropped duplicates.\n\nTest plan:\n- Unit tests for edge extraction across all supported types.\n- Unit tests for dedupe and missing-target behavior.\n- Unit tests for adjacency correctness and deterministic ordering.\r\n","acceptance_criteria":"- Graph model contains all valid nodes and typed edges from issue dependencies.\n- Duplicate edges are removed deterministically.\n- Missing-target edges do not crash model generation and are surfaced in diagnostics.\n- Adjacency maps are correct for incoming/outgoing lookups.\n- Unit tests cover all supported dependency types and edge cases.\r\n","notes":"Implemented src/lib/graph.ts GraphModel builder with deterministic node/edge ordering, supported edge-type filtering (blocks/parent/relates_to/duplicates/supersedes), duplicate-edge suppression, missing-target diagnostics, and adjacency incoming/outgoing indexes. Added tests/lib/graph.test.ts covering extraction, dedupe, unsupported/missing handling, and adjacency correctness. Updated package.json test chain to include graph tests.","status":"closed","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:10.0434044-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T09:10:52.6262123-08:00","closed_at":"2026-02-12T09:10:52.6262123-08:00","close_reason":"Completed graph model preparation pipeline with deterministic contracts and full unit coverage; ready for React Flow rendering task bb-bvn.2.","labels":["graph","parser"],"dependencies":[{"issue_id":"bb-bvn.1","depends_on_id":"bb-bvn","type":"parent-child","created_at":"2026-02-11T17:12:10.0449367-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bvn.1","depends_on_id":"bb-bvn.4","type":"blocks","created_at":"2026-02-11T20:10:02.7644711-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-bvn.2","title":"Implement React Flow graph view with pan/zoom/select interactions","description":"Implement deterministic React Flow graph UI (non-chaotic workspace mode).\n\nScope:\n- New graph page/view with React Flow canvas.\n- Deterministic auto-layout (DAG style) for stable mental model:\n - selected node centered in focus mode\n - upstream blockers left, downstream dependents right\n- Use card-like nodes (not bubbles) with minimal status accent.\n- Edge styling by dependency type:\n - blocks: solid\n - parent: thicker muted\n - relates_to: dashed\n - duplicates/supersedes: distinct but subtle styles\n\nInteraction:\n- Click node opens shared detail panel.\n- Controls: hop depth switch (1/2/full), collapse closed, fit-to-selection.\n- Disable freeform drag by default to avoid n8n-like chaos (optional manual toggle can be deferred).\n\nResponsive behavior:\n- Desktop/tablet: full canvas + detail panel split.\n- Mobile: simplified dependency focus mode (selected + immediate blockers/dependents list) instead of dense full canvas.\n\nIntegration:\n- Read-only against graph model from bb-bvn.1.\n- No writeback from graph lane.\n\nTest/verification:\n- Component tests for control toggles and selected-node behavior.\n- Guard test for responsive fallback contract.\n- Playwright screenshots: mobile/tablet/desktop graph view.\r\n","acceptance_criteria":"- Graph renders with deterministic layout and typed edges.\n- Default depth is 2 hops with controls for 1/2/full.\n- Node selection opens detail panel and fit-to-selection works.\n- Mobile shows simplified focus view (no unusable dense canvas).\n- Visual verification screenshots captured for mobile/tablet/desktop.\r\n","notes":"Full visual buff and relationship clarity pass complete. 1) Implemented modern aurora surface theme with refined typography and rhythm. 2) Fixed invisible relationship lines by increasing edge contrast, width, and adding animations for 'blocks' paths. 3) Refined layout to ensure 'Dependency Flow' is fully scrollable and correctly prioritized. 4) Improved mobile UX with a simplified overview and toggleable graph view. 5) Implemented groundwork for bb-bvn.3 (analyzeBlockedChain, detectDependencyCycles) to satisfy tests. Verified via npm run test, typecheck, and captured screenshots in artifacts/.","status":"closed","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:10.8683725-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T18:57:24.4716865-08:00","closed_at":"2026-02-12T18:57:24.4716865-08:00","close_reason":"Implemented React Flow graph workspace with deterministic layout, interaction controls, responsive fallback, and visual verification artifacts; tests/typecheck are green.","labels":["graph","ui"],"dependencies":[{"issue_id":"bb-bvn.2","depends_on_id":"bb-bvn","type":"parent-child","created_at":"2026-02-11T17:12:10.8694189-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bvn.2","depends_on_id":"bb-bvn.1","type":"blocks","created_at":"2026-02-11T17:12:36.8736785-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bvn.2","depends_on_id":"bb-bvn.4","type":"blocks","created_at":"2026-02-11T20:10:04.4783802-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-bvn.3","title":"Add blocked-chain highlighting and cycle anomaly signaling","description":"Add analysis overlays for blocker triage and anomaly visibility.\n\nScope:\n- Compute and highlight blocked chains from selected node.\n- Show concise blocker summary:\n - open blocker count\n - in-progress blocker count\n - first actionable blocker\n- Cycle/anomaly signaling:\n - detect cycles in dependency graph\n - mark involved nodes/edges with warning style and explanation text\n\nUI behavior:\n- \"Show blocking path only\" toggle to reduce noise.\n- Hovering a node/edge highlights direct dependency chain.\n- Keep styling subtle and readable; avoid visual overload.\n\nRules:\n- Analysis is read-only and derived from current graph model.\n- Must not fail hard on malformed dependency data; degrade with warnings.\n\nTest plan:\n- Unit tests for blocked-chain derivation and cycle detection logic.\n- UI tests for toggle behavior and warning visibility.\n- Screenshot verification for normal and anomaly cases.\r\n","acceptance_criteria":"- Selected issue can display clear blocked-chain context.\n- Cycle/anomaly conditions are detected and visibly flagged.\n- Blocking-path-only mode materially reduces graph noise.\n- Analysis features remain performant and do not break base graph rendering.\n- Tests and screenshots verify normal + anomaly paths.\r\n","notes":"Addressed review P1 in detectDependencyCycles: removed early-return DFS behavior that leaked recStack/path state; traversal now always unwinds and collects cycles without contaminating predecessor nodes. Added regression test in tests/lib/graph-view.test.ts: detectDependencyCycles does not mark non-cycle predecessor as cyclic. Verification: node --import tsx --test tests/lib/graph-view.test.ts (pass), npm run typecheck (pass), npm run test (pass).","status":"closed","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:11.687878-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T18:57:24.8694169-08:00","closed_at":"2026-02-12T18:57:24.8694169-08:00","close_reason":"Implemented blocked-chain analysis, blocking-path emphasis, and cycle anomaly signaling with regression coverage; tests/typecheck are green.","labels":["analysis","graph"],"dependencies":[{"issue_id":"bb-bvn.3","depends_on_id":"bb-bvn","type":"parent-child","created_at":"2026-02-11T17:12:11.6890831-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bvn.3","depends_on_id":"bb-bvn.2","type":"blocks","created_at":"2026-02-11T17:12:37.378326-08:00","created_by":"zenchantlive"},{"issue_id":"bb-bvn.3","depends_on_id":"bb-bvn.4","type":"blocks","created_at":"2026-02-11T20:10:03.6326727-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-bvn.4","title":"Epic Design Gate: scope, decisions, and acceptance contract","description":"Design/discovery gate for bb-bvn before further implementation.\n\nMust capture:\n- Product intent and user outcomes for this epic\n- Explicit architecture decisions and tradeoffs\n- API/data contracts and edge cases\n- Windows-specific constraints and path/process assumptions\n- Test strategy and verification commands\n- Non-goals and out-of-scope boundaries\n\nCompletion rule:\nDo not start new implementation tasks in this epic until this gate is closed with agreed decisions.","acceptance_criteria":"A written execution-grade plan exists for this epic and all child task descriptions are updated with concrete implementation details, dependencies, and testable acceptance criteria.","notes":"Graph design gate completed: agreed React Flow deterministic UX, default 2-hop depth controls, mobile simplified fallback, typed edge semantics, and verification contract (tests + screenshots + smoke). Child tasks bb-bvn.1/.2/.3 updated with execution-grade details.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T20:09:40.290642-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:59:12.4823711-08:00","closed_at":"2026-02-11T20:59:12.4823711-08:00","close_reason":"Design gate complete: bb-bvn child tasks now contain concrete scope, contracts, dependencies, and testable acceptance criteria.","dependencies":[{"issue_id":"bb-bvn.4","depends_on_id":"bb-bvn","type":"parent-child","created_at":"2026-02-11T20:09:40.2922349-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv","title":"Agent Communication \u0026 Coordination Patterns","description":"Agents need a standardized way to coordinate (handoffs, help requests, blockers) without breaking flow. We are opting for a **Issue-Centric** communication model (using Comments) rather than an Inbox-Centric model. This epic defines the protocols, patterns, and tool support needed to make that robust.\n\nGoals:\n- Define 'Protocol' for agent-to-agent comments (prefixes, structure).\n- Establish Identity standards (how agents refer to themselves).\n- Ensure CLI support for all protocol actions (commenting, signaling).\n\nDeliverables:\n- RFC-001: Agent Coordination Protocol.\n- Skill: beadboard-driver (teaching the protocol).\n\nThis epic blocks bb-u6f (Agent Sessions) because session attribution relies on the Identity standards defined here.","status":"open","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T21:35:07.1826787-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:36:56.4424829-08:00"}
|
||||
{"id":"bb-dcv.1","title":"Research \u0026 RFC: Agent Skills and Handoff Protocols","description":"Conduct research and draft a 'Request for Comments' (RFC) document that defines the standard operating procedures for agent-to-agent interaction within Beadboard.\n\nKey Questions to Answer:\n1. Identity: How do we consistently identify an agent? (e.g. assignee formats).\n2. Handoff Protocol: Structure of a handoff comment (e.g. [HANDOFF]).\n3. Blocker Signaling: How to raise a flag (e.g. [BLOCKED]).\n4. Parsing: Can/should we have bd parse-comments?\n\nDeliverables:\n- docs/RFC-001-Agent-Coordination.md: Finalized spec.\n- skills/beadboard-driver/SKILL.md (Draft): Prototype skill.\n- Gap Analysis: Missing CLI commands.\n\nAcceptance Criteria:\n- RFC document created and committed.\n- Protocol covers: Identity, Handoff, Blocker, Assignment.\n- Gap analysis lists required code changes.","notes":"Drafted RFC and skill artifacts: docs/RFC-001-Agent-Coordination.md and skills/beadboard-driver/SKILL.md. Includes identity, handoff/blocker/assignment protocol and gap analysis for bb agent CLI.\nSkill deliverable moved to a dedicated later task so RFC scope remains protocol/spec + gap analysis only.","status":"in_progress","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T21:37:32.9086915-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T14:45:05.8683583-08:00","dependencies":[{"issue_id":"bb-dcv.1","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-12T21:37:32.9107758-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.2","title":"Define bb agent CLI contract and storage schema","description":"Write the thin-layer CLI contract for bb agent commands and the on-disk schema under .beadboard/agent. Include command I/O examples and validation rules. This is implementation input, not a refactor.","acceptance_criteria":"Spec includes: register/list/show, send/inbox/read/ack, reserve/release/status; JSON schema and file layout are documented; failure modes are defined.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:41.2806805-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:56:41.2806805-08:00","labels":["agents","cli","design"],"dependencies":[{"issue_id":"bb-dcv.2","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:41.2822506-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.2","depends_on_id":"bb-dcv.1","type":"blocks","created_at":"2026-02-13T12:57:08.7260185-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.3","title":"Final verification and readiness sweep for agent CLI","description":"Run full verification for the thin-layer agent CLI and publish evidence from tests/lint/typecheck plus dependency sanity.","acceptance_criteria":"typecheck/test/lint pass; dependency graph is acyclic and reflects plan; readiness summary posted.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:55.8190789-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:56:55.8190789-08:00","labels":["agents","qa","verification"],"dependencies":[{"issue_id":"bb-dcv.3","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:55.8211858-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.3","depends_on_id":"bb-dcv.5","type":"blocks","created_at":"2026-02-13T12:57:13.0099035-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.3","depends_on_id":"bb-dcv.8","type":"blocks","created_at":"2026-02-13T14:45:12.4489854-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.4","title":"Implement reservation commands with TTL","description":"Implement reserve/release/status commands for work surfaces with TTL expiry and stale ownership handling.","acceptance_criteria":"reserve/release/status work; expired reservations are surfaced/cleared; conflict scenarios are test-covered.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:56.3114764-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:56:56.3114764-08:00","labels":["agents","cli","reservations"],"dependencies":[{"issue_id":"bb-dcv.4","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:56.3130569-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.4","depends_on_id":"bb-dcv.2","type":"blocks","created_at":"2026-02-13T12:57:10.8788059-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.4","depends_on_id":"bb-dcv.7","type":"blocks","created_at":"2026-02-13T12:57:11.4135844-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.5","title":"Integrate bb agent UX with bd claim workflow","description":"Add CLI UX layer and docs so bb agent flows pair cleanly with bd update --claim, without direct JSONL writes.","acceptance_criteria":"Docs include canonical session flow; commands produce operator-friendly output; no direct JSONL writes introduced.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:56.7418732-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:56:56.7418732-08:00","labels":["agents","cli","workflow"],"dependencies":[{"issue_id":"bb-dcv.5","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:56.7434894-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.5","depends_on_id":"bb-dcv.6","type":"blocks","created_at":"2026-02-13T12:57:11.9292114-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.5","depends_on_id":"bb-dcv.4","type":"blocks","created_at":"2026-02-13T12:57:12.4685539-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.6","title":"Implement agent mail commands (send/inbox/read/ack)","description":"Implement file-backed message transport for registered agents with unread/read/acked states and bead-linked thread context.","acceptance_criteria":"send/inbox/read/ack commands work end-to-end; sender/recipient must be registered; message lifecycle is test-covered.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:57.2090515-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:56:57.2090515-08:00","labels":["agents","cli","mail"],"dependencies":[{"issue_id":"bb-dcv.6","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:57.210616-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.6","depends_on_id":"bb-dcv.2","type":"blocks","created_at":"2026-02-13T12:57:09.7811635-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.6","depends_on_id":"bb-dcv.7","type":"blocks","created_at":"2026-02-13T12:57:10.3349432-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.7","title":"Implement agent identity registry commands","description":"Implement bb agent register/list/show with unique-name enforcement and stable metadata files under .beadboard/agent/agents.","acceptance_criteria":"register/list/show commands work; duplicate names fail with clear error; tests cover happy/error paths.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:57.6944409-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T12:56:57.6944409-08:00","labels":["agents","cli","identity"],"dependencies":[{"issue_id":"bb-dcv.7","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:57.6961264-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.7","depends_on_id":"bb-dcv.2","type":"blocks","created_at":"2026-02-13T12:57:09.2534901-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.8","title":"Create beadboard-driver skill from implemented bb agent workflows","description":"Use skill-creator workflow to produce the beadboard-driver skill only after bb agent identity, mail, reservation, and workflow commands are implemented and verified.","acceptance_criteria":"SKILL.md matches implemented CLI behavior; trigger language is explicit; no speculative commands included; quick validation performed.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T14:45:05.4433258-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T14:45:05.4433258-08:00","labels":["agents","docs","skill"],"dependencies":[{"issue_id":"bb-dcv.8","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T14:45:05.4449006-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.8","depends_on_id":"bb-dcv.5","type":"blocks","created_at":"2026-02-13T14:45:11.9133726-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-bvn.4","title":"Epic Design Gate: scope, decisions, and acceptance contract","description":"Design/discovery gate for bb-bvn before further implementation.\n\nMust capture:\n- Product intent and user outcomes for this epic\n- Explicit architecture decisions and tradeoffs\n- API/data contracts and edge cases\n- Windows-specific constraints and path/process assumptions\n- Test strategy and verification commands\n- Non-goals and out-of-scope boundaries\n\nCompletion rule:\nDo not start new implementation tasks in this epic until this gate is closed with agreed decisions.","acceptance_criteria":"A written execution-grade plan exists for this epic and all child task descriptions are updated with concrete implementation details, dependencies, and testable acceptance criteria.","notes":"Graph design gate completed: agreed React Flow deterministic UX, default 2-hop depth controls, mobile simplified fallback, typed edge semantics, and verification contract (tests + screenshots + smoke). Child tasks bb-bvn.1/.2/.3 updated with execution-grade details.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T20:09:40.290642-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:59:12.4823711-08:00","closed_at":"2026-02-11T20:59:12.4823711-08:00","close_reason":"Design gate complete: bb-bvn child tasks now contain concrete scope, contracts, dependencies, and testable acceptance criteria.","dependencies":[{"issue_id":"bb-bvn.4","depends_on_id":"bb-bvn","type":"parent-child","created_at":"2026-02-11T20:09:40.2922349-08:00","created_by":"zenchantlive"}],"comments":[{"id":9,"issue_id":"bb-bvn.4","author":"zenchantlive","text":"yo","created_at":"2026-02-14T06:58:38Z"}]}
|
||||
{"id":"bb-dcv","title":"Agent Communication \u0026 Coordination Patterns","description":"Agents need a standardized way to coordinate (handoffs, help requests, blockers) without breaking flow. We are opting for a **Issue-Centric** communication model (using Comments) rather than an Inbox-Centric model. This epic defines the protocols, patterns, and tool support needed to make that robust.\n\nGoals:\n- Define 'Protocol' for agent-to-agent comments (prefixes, structure).\n- Establish Identity standards (how agents refer to themselves).\n- Ensure CLI support for all protocol actions (commenting, signaling).\n\nDeliverables:\n- RFC-001: Agent Coordination Protocol.\n- Skill: beadboard-driver (teaching the protocol).\n\nThis epic blocks bb-u6f (Agent Sessions) because session attribution relies on the Identity standards defined here.","notes":"All child tasks closed: bb-dcv.1/.2/.3/.4/.5/.6/.7/.8. Agent communication CLI + beadboard-driver skill delivered and fully verified (typecheck/lint/test + skill quick validation).","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T21:35:07.1826787-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T19:02:22.6815392-08:00","closed_at":"2026-02-13T19:02:22.3262564-08:00","close_reason":"Agent Communication \u0026 Coordination Patterns deliverables completed and verified end-to-end."}
|
||||
{"id":"bb-dcv.1","title":"Research \u0026 RFC: Agent Skills and Handoff Protocols","description":"Conduct research and draft a 'Request for Comments' (RFC) document that defines the standard operating procedures for agent-to-agent interaction within Beadboard.\n\nKey Questions to Answer:\n1. Identity: How do we consistently identify an agent? (e.g. assignee formats).\n2. Handoff Protocol: Structure of a handoff comment (e.g. [HANDOFF]).\n3. Blocker Signaling: How to raise a flag (e.g. [BLOCKED]).\n4. Parsing: Can/should we have bd parse-comments?\n\nDeliverables:\n- docs/RFC-001-Agent-Coordination.md: Finalized spec.\n- skills/beadboard-driver/SKILL.md (Draft): Prototype skill.\n- Gap Analysis: Missing CLI commands.\n\nAcceptance Criteria:\n- RFC document created and committed.\n- Protocol covers: Identity, Handoff, Blocker, Assignment.\n- Gap analysis lists required code changes.","notes":"Drafted RFC and skill artifacts: docs/RFC-001-Agent-Coordination.md and skills/beadboard-driver/SKILL.md. Includes identity, handoff/blocker/assignment protocol and gap analysis for bb agent CLI.\nSkill deliverable moved to a dedicated later task so RFC scope remains protocol/spec + gap analysis only.\nRFC finalized with explicit decision: use mcp_agent_mail as reference patterns only (no direct integration). Added command matrix, failure-mode checklist, and rejected alternatives. File: docs/RFC-001-Agent-Coordination.md","status":"closed","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T21:37:32.9086915-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T14:51:44.2117448-08:00","closed_at":"2026-02-13T14:51:44.2117448-08:00","close_reason":"RFC/protocol decision gate completed: identity, message categories+ack policy, reservation TTL model, bd/bb-agent boundary, command matrix, and failure-mode gaps locked for implementation.","dependencies":[{"issue_id":"bb-dcv.1","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-12T21:37:32.9107758-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.2","title":"Define bb agent CLI contract and storage schema","description":"Write the thin-layer CLI contract for bb agent commands and the on-disk schema under .beadboard/agent. Include command I/O examples and validation rules. This is implementation input, not a refactor.","acceptance_criteria":"Spec includes: register/list/show, send/inbox/read/ack, reserve/release/status; JSON schema and file layout are documented; failure modes are defined.","notes":"Completed contract deliverable: docs/plans/2026-02-13-bb-agent-cli-contract.md. Includes command contracts for register/list/show, send/inbox/read/ack, reserve/release/status; validation and error code registry; JSON/file schema; and follow-on test matrix for bb-dcv.7/.6/.4/.5.","status":"closed","priority":1,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:41.2806805-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T15:39:03.0543649-08:00","closed_at":"2026-02-13T15:39:03.0543649-08:00","close_reason":"CLI contract and storage schema finalized and documented for parallel implementation lanes.","labels":["agents","cli","design"],"dependencies":[{"issue_id":"bb-dcv.2","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:41.2822506-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.2","depends_on_id":"bb-dcv.1","type":"blocks","created_at":"2026-02-13T12:57:08.7260185-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.3","title":"Final verification and readiness sweep for agent CLI","description":"Run full verification for the thin-layer agent CLI and publish evidence from tests/lint/typecheck plus dependency sanity.","acceptance_criteria":"typecheck/test/lint pass; dependency graph is acyclic and reflects plan; readiness summary posted.","notes":"Readiness summary: npm run typecheck =\u003e pass; npm run lint =\u003e pass (0 errors, 0 warnings); npm run test =\u003e pass (including tests/skills/beadboard-driver/* and skill-local runner). Dependency sanity: bb-dcv.3 dependencies bb-dcv.5 and bb-dcv.8 are both closed; bb-dcv child chain is acyclic and aligned with planned execution order (.8 before .3).","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:55.8190789-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T19:01:38.6032253-08:00","closed_at":"2026-02-13T19:01:38.6032253-08:00","close_reason":"Final verification sweep complete with full green gates and dependency sanity summary posted.","labels":["agents","qa","verification"],"dependencies":[{"issue_id":"bb-dcv.3","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:55.8211858-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.3","depends_on_id":"bb-dcv.5","type":"blocks","created_at":"2026-02-13T12:57:13.0099035-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.3","depends_on_id":"bb-dcv.8","type":"blocks","created_at":"2026-02-13T14:45:12.4489854-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.4","title":"Implement reservation commands with TTL","description":"Implement reserve/release/status commands for work surfaces with TTL expiry and stale ownership handling.","acceptance_criteria":"reserve/release/status work; expired reservations are surfaced/cleared; conflict scenarios are test-covered.","notes":"Starting bb-dcv.4: implement reserve/release/status with TTL + stale ownership takeover; following brainstorming, TDD, and verification-before-completion skills.\nImplemented reservation commands with TTL in src/lib/agent-reservations.ts and tests in tests/lib/agent-reservations.test.ts. Evidence: focused test pass via node --import tsx --test tests/lib/agent-reservations.test.ts; typecheck pass via npm run typecheck; lint pass via npm run lint; full suite pass via npm run test. Coverage includes reserve/release/status flow, TTL expiry cleanup, stale conflict (RESERVATION_STALE_FOUND), stale takeover (--takeover-stale), and owner-only release (RELEASE_FORBIDDEN). Updated package.json test script to include tests/lib/agent-reservations.test.ts.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:56.3114764-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T16:03:06.6559175-08:00","closed_at":"2026-02-13T16:03:05.7886986-08:00","close_reason":"Completed reservation commands with TTL and contract-aligned test coverage; verification gates passed.","labels":["agents","cli","reservations"],"dependencies":[{"issue_id":"bb-dcv.4","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:56.3130569-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.4","depends_on_id":"bb-dcv.2","type":"blocks","created_at":"2026-02-13T12:57:10.8788059-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.4","depends_on_id":"bb-dcv.7","type":"blocks","created_at":"2026-02-13T12:57:11.4135844-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.5","title":"Integrate bb agent UX with bd claim workflow","description":"Add CLI UX layer and docs so bb agent flows pair cleanly with bd update --claim, without direct JSONL writes.","acceptance_criteria":"Docs include canonical session flow; commands produce operator-friendly output; no direct JSONL writes introduced.","notes":"Created docs/agent-session-flow.md. Implemented tools/bb.ts and bb.ps1. Verified key workflows via test suite. Verified CLI runnable. Validated no-direct-jsonl-write.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:56.7418732-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T16:25:22.8044681-08:00","closed_at":"2026-02-13T16:25:22.8044681-08:00","close_reason":"Docs and CLI wrapper delivered verified","labels":["agents","cli","workflow"],"dependencies":[{"issue_id":"bb-dcv.5","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:56.7434894-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.5","depends_on_id":"bb-dcv.6","type":"blocks","created_at":"2026-02-13T12:57:11.9292114-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.5","depends_on_id":"bb-dcv.4","type":"blocks","created_at":"2026-02-13T12:57:12.4685539-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.6","title":"Implement agent mail commands (send/inbox/read/ack)","description":"Implement file-backed message transport for registered agents with unread/read/acked states and bead-linked thread context.","acceptance_criteria":"send/inbox/read/ack commands work end-to-end; sender/recipient must be registered; message lifecycle is test-covered.","notes":"Implemented agent mail command layer in src/lib/agent-mail.ts with contract behaviors: send/inbox/read/ack, sender/recipient registration checks, message categories and requires_ack policy, unread/read/acked lifecycle, recipient-only ack with ACK_FORBIDDEN, message index + per-recipient inbox storage under %USERPROFILE%/.beadboard/agent/messages. Added tests in tests/lib/agent-mail.test.ts covering unknown sender/recipient, end-to-end send/inbox/read/ack flow, forbidden ack, and category/bead validation errors. Added test inclusion in package.json.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:57.2090515-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T16:05:03.6658756-08:00","closed_at":"2026-02-13T16:05:03.6658756-08:00","close_reason":"Implemented and verified agent mail command handlers (send/inbox/read/ack) with lifecycle and registry validation.","labels":["agents","cli","mail"],"dependencies":[{"issue_id":"bb-dcv.6","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:57.210616-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.6","depends_on_id":"bb-dcv.2","type":"blocks","created_at":"2026-02-13T12:57:09.7811635-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.6","depends_on_id":"bb-dcv.7","type":"blocks","created_at":"2026-02-13T12:57:10.3349432-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.7","title":"Implement agent identity registry commands","description":"Implement bb agent register/list/show with unique-name enforcement and stable metadata files under .beadboard/agent/agents.","acceptance_criteria":"register/list/show commands work; duplicate names fail with clear error; tests cover happy/error paths.","notes":"Delivered src/lib/agent-registry.ts with contract-aligned behavior: agent id validation (regex + length), role validation, duplicate rejection with DUPLICATE_AGENT_ID, force-update path, stable metadata persisted at %USERPROFILE%/.beadboard/agent/agents/\u003cagent_id\u003e.json, list filtering/sorting, and show with AGENT_NOT_FOUND. Added test-first coverage in tests/lib/agent-registry.test.ts (happy path + duplicate error + force-update + list filters/sort + show not found + validation errors). Included suite in package.json test script. Verification evidence: node --import tsx --test tests/lib/agent-registry.test.ts (pass 6/6), npm run typecheck (pass), npm run lint (pass), npm run test (pass full suite). This unblocks bb-dcv.4 and bb-dcv.6 dependency lane.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T12:56:57.6944409-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T15:49:48.0956042-08:00","closed_at":"2026-02-13T15:49:48.0956042-08:00","close_reason":"Implemented and verified agent identity registry command layer (register/list/show).","labels":["agents","cli","identity"],"dependencies":[{"issue_id":"bb-dcv.7","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T12:56:57.6961264-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.7","depends_on_id":"bb-dcv.2","type":"blocks","created_at":"2026-02-13T12:57:09.2534901-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-dcv.8","title":"Create beadboard-driver skill from implemented bb agent workflows","description":"Use skill-creator workflow to produce the beadboard-driver skill only after bb agent identity, mail, reservation, and workflow commands are implemented and verified.","acceptance_criteria":"SKILL.md matches implemented CLI behavior; trigger language is explicit; no speculative commands included; quick validation performed.","notes":"Implemented beadboard-driver skill package with cross-platform script layer and dual test harness. Added skills/beadboard-driver/{SKILL.md,agents/openai.yaml,scripts/*.mjs,references/*.md,tests/*.mjs} plus repo contract tests under tests/skills/beadboard-driver/*.test.ts and wired into npm test. Validation evidence: quick_validate.py skills/beadboard-driver =\u003e pass; node --import tsx --test tests/skills/beadboard-driver/*.test.ts =\u003e pass; node skills/beadboard-driver/tests/run-tests.mjs =\u003e pass; npm run typecheck =\u003e pass; npm run lint =\u003e pass; npm run test =\u003e pass.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T14:45:05.4433258-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T19:01:15.4602809-08:00","closed_at":"2026-02-13T19:01:14.6040163-08:00","close_reason":"Created and validated beadboard-driver skill from implemented bb agent workflows with explicit triggers, no speculative commands, and dual-layer tests.","labels":["agents","docs","skill"],"dependencies":[{"issue_id":"bb-dcv.8","depends_on_id":"bb-dcv","type":"parent-child","created_at":"2026-02-13T14:45:05.4449006-08:00","created_by":"zenchantlive"},{"issue_id":"bb-dcv.8","depends_on_id":"bb-dcv.5","type":"blocks","created_at":"2026-02-13T14:45:11.9133726-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-lvl","title":"Fix stale bead status rendering and refresh propagation in BeadBoard","description":"Triage and fix mismatch where bd shows updated status (e.g., in_progress) but BeadBoard/BV surfaces continue showing stale values or require manual refresh. Investigate DB-\u003eJSONL sync, scope/root selection, API read path, and SSE propagation.","acceptance_criteria":"For a claimed issue, BeadBoard reflects status changes without stale drift; repro and root cause documented; regression checks added.","notes":"Root-cause evidence: bd DB status diverged from .beads/issues.jsonl (bb-dcv.2 in_progress in bd show, open in JSONL before sync). After bd sync in repo root, JSONL updated immediately. Suspected freshness bug from disk-only read path.\nImplemented freshness-path fix: app reads now prefer bd list --json with fallback to disk JSONL. Updated src/lib/read-issues.ts, src/lib/aggregate-read.ts, src/app/page.tsx, src/app/graph/page.tsx, and src/app/api/beads/read/route.ts. Verification: npm run typecheck, npm run lint, npm run test all passed.\nSecond triage fix for live refresh: watcher now includes .beads/beads.db in watched paths, so DB-only updates emit SSE and trigger UI refresh without manual page reload. Updated src/lib/watcher.ts and tests/lib/watcher.test.ts (new beads.db event test). Verification: watcher test pass, typecheck pass, lint pass.\nFurther root cause: events fallback compared .beads/last-touched file CONTENT, but repeated updates on same issue keep content unchanged (bb-dcv.2) while only mtime changes. Updated /api/events fallback poll to compare last-touched mtime instead. Also expanded watcher inputs to include beads.db-wal and last-touched.\nPost-restart verification: end-to-end SSE probe now receives issues events after external bd update (saw_issues_event=true). This confirms refresh path works when server runs updated /api/events logic.","status":"closed","priority":1,"issue_type":"bug","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T15:16:22.8086122-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T15:36:29.9493329-08:00","closed_at":"2026-02-13T15:36:29.9493329-08:00","close_reason":"Status refresh regression resolved: live read freshness + SSE event emission restored; verified via terminal event probe and manual status toggle without page refresh.","labels":["realtime","sse","status"]}
|
||||
{"id":"bb-n7p","title":"Swimlane status model: ready + dependency-derived blocked","notes":"Implemented new swimlane model: removed deferred lane from board usage; added ready lane and dependency-derived blocked lane. Lane rules: closed-\u003eDone; blocked-\u003eBlocked if explicit status blocked OR has active incoming blocker edge; in_progress/review-\u003eIn Progress; otherwise Ready. Added laneToMutationStatus to map board lane writes to bead statuses (ready-\u003eopen). Updated board labels/colors, drag-drop lane source tracking, and controls stat label Open-\u003eReady. TDD: updated tests/lib/kanban.test.ts for ready/blocked semantics. Verification: node --import tsx --test tests/lib/kanban.test.ts (pass), npm run typecheck (pass), npm run test (pass).","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T17:55:04.1851993-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T18:40:08.0620089-08:00","closed_at":"2026-02-12T18:40:08.0620089-08:00","close_reason":"Implemented ready/blocked swimlane model, blocked-tree deep links to lane focus, and verification passed (kanban tests, typecheck, full test suite).","labels":["kanban","status","swimlane"]}
|
||||
{"id":"bb-q1s","title":"UI Bead Editing Across Kanban + Graph","description":"Objective:\nAdd true UI editing for bead fields across both detail panels (Kanban + Graph) using one shared edit core so behavior stays consistent.\n\nWhy:\nWrite-back infrastructure exists, but users currently cannot edit bead content from UI detail panels.\n\nScope:\n- Shared edit validation + mutation adapter.\n- Reusable editor UI block for issue fields.\n- Integration into both Kanban and Graph detail panels.\n- Verification for responsive behavior and mutation safety.\n\nOut of scope:\n- Dependency relation editing.\n- AI content generation.\n- Bulk editing.","acceptance_criteria":"- Users can edit core bead fields from both Kanban and Graph detail panels.\n- Both surfaces use the same validation and update path.\n- Save/cancel/error states are consistent across both surfaces.\n- Typecheck/tests/guards pass and no direct JSONL writes are introduced.","notes":"Execution order enforced through child dependencies.\nExecution order: bb-q1s.1 shared core -\u003e bb-q1s.2 kanban + bb-q1s.3 graph (parallel) -\u003e bb-q1s.4 verification/polish.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:50:12.3431904-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:11:43.1747329-08:00","closed_at":"2026-02-12T21:11:43.1747329-08:00","close_reason":"Shared UI bead editing shipped across Kanban and Graph with verification evidence.","labels":["editing","mutation","ui"]}
|
||||
{"id":"bb-q1s.1","title":"Shared edit core: schema + update adapter + state machine","description":"Build shared edit core used by both detail panels.\n\nIncludes:\n- editable field schema\n- validation rules\n- payload adapter for /api/beads/update\n- form state model: pristine/dirty/saving/error","acceptance_criteria":"- Shared edit core is framework-agnostic and reused by both UIs.\n- Validation covers title/priority/labels/assignee/owner/description.\n- Adapter emits stable update payload.","notes":"Implemented shared edit core in src/lib/issue-editor.ts with draft schema, validation, diff-to-update adapter, label parsing, and edit-state classifier. Added tests in tests/lib/issue-editor.test.ts and expanded mutation adapter to support issueType updates.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-12T20:50:31.668852-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:10:59.9315015-08:00","closed_at":"2026-02-12T21:10:59.9315015-08:00","close_reason":"Shared edit core delivered and validated via unit tests + typecheck.","labels":["editing","lib","shared"],"dependencies":[{"issue_id":"bb-q1s.1","depends_on_id":"bb-q1s","type":"parent-child","created_at":"2026-02-12T20:50:31.6709483-08:00","created_by":"zenchantlive"}]}
|
||||
|
|
@ -109,19 +112,28 @@
|
|||
{"id":"bb-trz.2","title":"Build bead cards with priority/type/labels/assignee/dependency metadata","description":"Design compact cards exposing the most actionable issue metadata while preserving readability at high board density.","acceptance_criteria":"Cards show id, priority, type, labels, assignee, and dependency indicators.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:58.4435327-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8141656-08:00","closed_at":"2026-02-11T17:56:50.8141656-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["cards","kanban"],"dependencies":[{"issue_id":"bb-trz.2","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:58.4450798-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.2","depends_on_id":"bb-trz.1","type":"blocks","created_at":"2026-02-11T17:12:30.7837277-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.3","title":"Implement detail slide-out panel with full issue metadata","description":"Add focused issue detail panel showing description, timestamps, dependencies, and lifecycle fields used by power users.","acceptance_criteria":"Selecting a card opens detail panel with complete issue context.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:11:59.2746013-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8161639-08:00","closed_at":"2026-02-11T17:56:50.8161639-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["details","kanban"],"dependencies":[{"issue_id":"bb-trz.3","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:11:59.2756402-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.3","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.2944-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-trz.4","title":"Add search/filter/stats controls for status/type/priority/labels","description":"Provide fast filtering and at-a-glance counts, including critical issue indicators, for daily planning and triage workflows.","acceptance_criteria":"Search and filters apply consistently across board and counts.","status":"closed","priority":1,"issue_type":"task","assignee":"agent-b","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:00.0927161-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:56:50.8186688-08:00","closed_at":"2026-02-11T17:56:50.8186688-08:00","close_reason":"Tracer bullet 1 Kanban baseline implemented and verified","labels":["filters","stats"],"dependencies":[{"issue_id":"bb-trz.4","depends_on_id":"bb-trz","type":"parent-child","created_at":"2026-02-11T17:12:00.0942721-08:00","created_by":"zenchantlive"},{"issue_id":"bb-trz.4","depends_on_id":"bb-trz.2","type":"blocks","created_at":"2026-02-11T17:12:31.798413-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f","title":"Agent Session Views and Metrics","description":"Provide agent-session accountability views for solo supervision: session list, per-session outcomes, and practical throughput metrics. Focus on operational decisions, not vanity dashboards.","acceptance_criteria":"Session identity is normalized and stable; per-session open/in-progress/closed outcomes are visible; baseline metrics (throughput, completion rate, active span) are correct and explainable; UI uses the same interaction and visual hierarchy conventions established in bb-bvn.","notes":"Product baseline locked (2026-02-12): Agent-session features should optimize for solo supervisor workflows (who changed what, when, and why) with clear per-agent accountability and low-noise summaries.\nExecution order set 2026-02-12: start after bb-xhm baseline event stream patterns are stable; depends on bb-xhm for shared timeline/session event semantics.","status":"open","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:12.5083912-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T12:45:51.8058469-08:00","labels":["agents","sessions"],"dependencies":[{"issue_id":"bb-u6f","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:23.1727361-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f","depends_on_id":"bb-xhm","type":"blocks","created_at":"2026-02-12T12:45:51.3676788-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f","depends_on_id":"bb-dcv","type":"blocks","created_at":"2026-02-12T21:40:13.985575-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.1","title":"Extract and normalize session identity fields from issue data","description":"Define the AgentSession model and implement logic to aggregate individual events and closed tasks into coherent sessions.\n\nFiles:\n- [NEW] src/lib/agent-sessions.ts -- Model types + aggregation logic\n\nData Contract:\ninterface AgentSession {\n id: string; // from closed_by_session or inferred\n startedAt: string; // ISO timestamp\n endedAt: string; // ISO timestamp\n closedIssueIds: string[];\n touchedIssueIds: string[];\n status: 'active' | 'completed';\n}\n\nAggregation Logic:\n1. Group by closed_by_session (hard link). All issues with same session ID belong together.\n2. Infer from ActivityLogs: For non-closed items, checking actor/time matches.\n3. MVP: Focus primarily on closed_by_session grouping.\n\nAcceptance Criteria:\n- AgentSession interface defined and exported.\n- aggregateSessions(issues, events) returns AgentSession[].\n- Correctly groups issues by closed_by_session.\n- Correctly computes start/end from closed_at timestamps.\n- Unit tests for aggregation logic.","acceptance_criteria":"Issues are consistently assigned to session buckets when data exists.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:13.3239834-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:37:59.3265433-08:00","labels":["agents","data"],"dependencies":[{"issue_id":"bb-u6f.1","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:13.3255058-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.1","depends_on_id":"bb-u6f.4","type":"blocks","created_at":"2026-02-11T20:09:55.5193741-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.2","title":"Build session list and detail views for claimed/completed/open outcomes","description":"Present session-level issue outcomes and navigation for operational review and accountability.","acceptance_criteria":"Users can inspect session summaries and drill into individual session issue sets.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:14.1559358-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:14.1559358-08:00","labels":["agents","ui"],"dependencies":[{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:14.157502-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f.1","type":"blocks","created_at":"2026-02-11T17:12:37.9045555-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f.4","type":"blocks","created_at":"2026-02-11T20:09:57.2147927-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.3","title":"Add baseline productivity metrics (completion rate, throughput, active span)","description":"Compute lightweight operational metrics from session issue events and timestamps.","acceptance_criteria":"Metrics are available with documented definitions and caveats.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:15.0144056-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T17:12:15.0144056-08:00","labels":["agents","metrics"],"dependencies":[{"issue_id":"bb-u6f.3","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:15.0155323-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.3","depends_on_id":"bb-u6f.2","type":"blocks","created_at":"2026-02-11T17:12:38.4424336-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.3","depends_on_id":"bb-u6f.4","type":"blocks","created_at":"2026-02-11T20:09:56.3707709-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.4","title":"Epic Design Gate: scope, decisions, and acceptance contract","description":"Design/discovery gate for bb-u6f before further implementation.\n\nMust capture:\n- Product intent and user outcomes for this epic\n- Explicit architecture decisions and tradeoffs\n- API/data contracts and edge cases\n- Windows-specific constraints and path/process assumptions\n- Test strategy and verification commands\n- Non-goals and out-of-scope boundaries\n\nCompletion rule:\nDo not start new implementation tasks in this epic until this gate is closed with agreed decisions.","acceptance_criteria":"A written execution-grade plan exists for this epic and all child task descriptions are updated with concrete implementation details, dependencies, and testable acceptance criteria.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T20:09:41.2150441-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T23:29:55.9362248-08:00","closed_at":"2026-02-12T23:29:55.9362248-08:00","dependencies":[{"issue_id":"bb-u6f.4","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T20:09:41.216603-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-xhm","title":"Timeline and Activity Feed","description":"Build a high-signal timeline view that explains what changed, by whom, and when, without noise.\n\nProduct baseline (2026-02-12): Timeline is secondary to Kanban (not default landing). It should support solo-dev live supervision and focus on actionable event stream clarity rather than exhaustive noise.\n\nArchitecture decisions:\n1. Snapshot-based diffing (not write interception) - keeps read/write boundary clean\n2. Derived events, not stored - ActivityEvent[] computed from JSONL diffs, not persisted separately\n3. In-memory snapshot store - watcher fires on changes, we diff previous vs current in memory\n4. Solo-dev focus - no multi-user attribution beyond created_by/assignee\n\nExecution order: bb-xhm.4 (design gate, DONE) -\u003e bb-xhm.1 (event model) -\u003e bb-xhm.2 (diffing) -\u003e bb-xhm.3 (UI)\n\nDependencies:\n- Depends on bb-bvn for shared UX and component patterns (DONE)\n- Depends on bb-tpc for live file watching and SSE transport (DONE)\n\nNon-goals:\n- No direct JSONL writes; read/write boundary preserved\n- No multi-user collaboration features\n- No persisted event store or event database\n\nAcceptance criteria:\n- Event model (bb-xhm.1) defines ActivityEventKind union with 16 transition types and ActivityEvent interface\n- Snapshot diffing (bb-xhm.2) produces deterministic ActivityEvent[] from JSONL changes with all field comparison rules\n- Timeline UI (bb-xhm.3) at /timeline renders events with date grouping, kind/assignee/project filters, and one-click navigation to issue context\n- All pages share the existing visual design system (dark theme, text-text-* tokens, rounded cards)\n- Tests exist for event model, diffing engine, and UI rendering\n- Typecheck and lint pass across all new files (bun run typecheck \u0026\u0026 bun run lint)","acceptance_criteria":"Timeline events are deterministic from snapshots/diffs; filters (project/actor/event/date) are fast and useful; users can move from event row to issue context in one action; UI follows the same visual system and hierarchy patterns established in bb-bvn.","notes":"Product baseline locked (2026-02-12): Timeline is secondary to Kanban (not default landing). It should support solo-dev live supervision and focus on actionable event stream clarity rather than exhaustive noise.\nExecution order set 2026-02-12: start after bb-bvn visual/interaction contracts are finalized; depends on bb-bvn for shared UX and component patterns.","status":"open","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:05.8525088-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:08:37.3922977-08:00","labels":["activity","timeline"],"dependencies":[{"issue_id":"bb-xhm","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:22.1602338-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm","depends_on_id":"bb-bvn","type":"blocks","created_at":"2026-02-12T12:45:52.624726-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-xhm.1","title":"Define activity event model for created/updated/closed/reopened actions","description":"Create the ActivityEvent TypeScript interface and supporting types that represent issue lifecycle transitions. This is the core data contract consumed by both the snapshot diffing engine (bb-xhm.2) and the timeline UI (bb-xhm.3).\n\nFiles:\n- [NEW] src/lib/activity-events.ts -- Type definitions + factory helpers\n\nData contract:\n- ActivityEventKind union type covering 16 event kinds: created, closed, reopened, status_changed, title_changed, priority_changed, assigned, unassigned, reassigned, labels_changed, dependency_added, dependency_removed, deleted\n- ActivityEvent interface with fields: id (monotonic counter), issueId, issueTitle, kind, at (ISO timestamp), projectRoot, actor (best-effort attribution from assignee/created_by), previousValue, newValue\n- createActivityEvent() factory function enforcing required fields at construction\n\nGrounding in existing types:\n- Builds on BeadIssue from src/lib/types.ts (uses status, assignee, created_by, dependencies, labels fields)\n- ProjectRoot string for multi-project support (matches IssuesChangedEvent convention from realtime.ts)\n\nAcceptance criteria:\n- ActivityEventKind union covers all 16 transition types listed above\n- ActivityEvent interface is exported and importable by bb-xhm.2 and bb-xhm.3\n- Factory function createActivityEvent() enforces required fields at construction time\n- Unit tests cover: creating each event kind, validating required fields, round-trip JSON serialization\n- Typecheck passes (bun run typecheck)","acceptance_criteria":"Event model supports all required timeline activity types.","status":"open","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:06.6781387-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:00:55.2316751-08:00","labels":["model","timeline"],"dependencies":[{"issue_id":"bb-xhm.1","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T17:12:06.6791721-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.1","depends_on_id":"bb-xhm.4","type":"blocks","created_at":"2026-02-11T20:10:05.9709567-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-xhm.2","title":"Implement snapshot diffing for derived timeline events","description":"Build a diffing engine that compares two snapshots of BeadIssue[] (previous and current state of issues.jsonl) and emits an ActivityEvent[] for all detected changes. Integrates with the existing IssuesEventBus to trigger diffs when the file watcher fires.\n\nFiles:\n- [NEW] src/lib/snapshot-differ.ts -- Core diff logic\n- [MODIFY] src/lib/watcher.ts -- Wire differ into onFlush callback\n\nDiff algorithm:\n1. Read current issues.jsonl into Map\u003cid, BeadIssue\u003e\n2. Compare against previous snapshot (also Map\u003cid, BeadIssue\u003e):\n a. For each id in current but not previous: emit 'created'\n b. For each id in previous but not current: emit 'deleted'\n c. For each id in both: compare fields and emit specific events\n3. Store current as new 'previous' snapshot\n4. Return ActivityEvent[]\n\nField comparison rules:\n- status open-\u003eclosed: emit 'closed' (previousValue=old status, newValue='closed')\n- status closed-\u003eopen: emit 'reopened' (previousValue='closed', newValue=new status)\n- status other: emit 'status_changed' (previousValue=old, newValue=new)\n- title: emit 'title_changed' (previousValue=old title, newValue=new title)\n- priority: emit 'priority_changed' (previousValue=old as string, newValue=new as string)\n- assignee null-\u003eX: emit 'assigned' (previousValue=null, newValue=X)\n- assignee X-\u003enull: emit 'unassigned' (previousValue=X, newValue=null)\n- assignee X-\u003eY: emit 'reassigned' (previousValue=X, newValue=Y)\n- labels: emit 'labels_changed' (previousValue=removed labels comma-sep, newValue=added labels comma-sep)\n- dependencies: emit 'dependency_added'/'dependency_removed' (values are dep target strings)\n\nIntegration point:\nIssuesWatchManager.onFlush currently only emits IssuesChangedEvent (file-level). After this bead:\n1. onFlush reads new issues.jsonl\n2. Runs diffSnapshots(previous, current) to get ActivityEvent[]\n3. Emits activity events through a new ActivityEventBus (separate from IssuesEventBus)\n4. Stores current as new previous snapshot\n\nDepends on bb-xhm.1 for ActivityEvent types.\nDepends on bb-tpc.2 for coalesced watcher events.\n\nAcceptance criteria:\n- diffSnapshots(prev, curr, projectRoot) returns ActivityEvent[]\n- All field comparison rules above are implemented\n- Edge cases handled: empty previous (first load = all 'created'), identical snapshots (no events), issue with only updated_at changed (no event)\n- Unit tests: created, deleted, each field change, multiple changes per diff, no-op diff\n- Performance: diff of 200 issues completes in \u003c10ms\n- Typecheck passes (bun run typecheck)","acceptance_criteria":"Diff engine emits deterministic event records for relevant field changes.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:07.5007059-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:01:45.5437274-08:00","labels":["diff","timeline"],"dependencies":[{"issue_id":"bb-xhm.2","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T17:12:07.501756-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-xhm.1","type":"blocks","created_at":"2026-02-11T17:12:35.3430513-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-tpc.2","type":"blocks","created_at":"2026-02-11T17:12:35.8495336-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-xhm.4","type":"blocks","created_at":"2026-02-11T20:10:07.6688195-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-xhm.3","title":"Build timeline UI with date grouping and project/assignee/event filters","description":"Render a reverse-chronological activity feed as a new /timeline page. Date-grouped, filterable, and navigable to issue context in one click.\n\nFiles:\n- [NEW] src/app/timeline/page.tsx -- Server component (reads issues, computes initial snapshot)\n- [NEW] src/components/timeline/timeline-page.tsx -- Client component (SSE subscription, filters, rendering)\n- [NEW] src/components/timeline/timeline-event-row.tsx -- Single event row component\n- [NEW] src/components/timeline/timeline-filters.tsx -- Filter bar component\n\nUI contract:\n1. Date grouping: Events grouped by date headers ('Today', 'Yesterday', 'Feb 10', etc.)\n2. Event row: Shows icon (per event kind), issue ID (monospace), title, actor, relative timestamp\n3. Filters: Project (when multi-project), event kind (checkboxes), assignee, date range\n4. Navigation: Click any event row to navigate to the issue in Workflow Explorer (/graph)\n5. Live updates: Subscribe to ActivityEventBus via SSE for new events, prepend to feed\n6. Design: Follow existing visual system (dark theme, text-text-* tokens, rounded cards, border accents, same header/panel patterns as graph page)\n\nRouting:\n- Add /timeline Next.js route\n- Add link in Workflow Explorer header (next to existing Kanban link)\n\nDepends on bb-xhm.1 for ActivityEvent types.\nDepends on bb-xhm.2 for ActivityEventBus providing live events via SSE.\n\nAcceptance criteria:\n- /timeline page renders with date-grouped event feed\n- Each event kind displays appropriate icon and human-readable description\n- Filter by event kind works (toggle checkboxes)\n- Click on event row navigates to issue context in Workflow Explorer\n- Live SSE updates prepend new events at the top\n- Responsive: usable on mobile (single-column stack)\n- Typecheck and lint pass (bun run typecheck \u0026\u0026 bun run lint)\n- No regressions to existing Kanban or Graph pages","acceptance_criteria":"Timeline view supports grouping and filter combinations with acceptable performance.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:08.3834905-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:08:16.5334224-08:00","labels":["timeline","ui"],"dependencies":[{"issue_id":"bb-xhm.3","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T17:12:08.3851144-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.3","depends_on_id":"bb-xhm.2","type":"blocks","created_at":"2026-02-11T17:12:36.3627477-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.3","depends_on_id":"bb-xhm.4","type":"blocks","created_at":"2026-02-11T20:10:06.8100606-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-xhm.4","title":"Epic Design Gate: scope, decisions, and acceptance contract","description":"Design/discovery gate for bb-xhm before further implementation.\n\nMust capture:\n- Product intent and user outcomes for this epic\n- Explicit architecture decisions and tradeoffs\n- API/data contracts and edge cases\n- Windows-specific constraints and path/process assumptions\n- Test strategy and verification commands\n- Non-goals and out-of-scope boundaries\n\nCompletion rule:\nDo not start new implementation tasks in this epic until this gate is closed with agreed decisions.","acceptance_criteria":"A written execution-grade plan exists for this epic and all child task descriptions are updated with concrete implementation details, dependencies, and testable acceptance criteria.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T20:09:39.3625154-08:00","created_by":"zenchantlive","updated_at":"2026-02-12T21:09:39.2227447-08:00","closed_at":"2026-02-12T21:09:39.2227447-08:00","close_reason":"Design gate complete. All child beads (bb-xhm.1, bb-xhm.2, bb-xhm.3) updated with execution-grade specs: concrete implementation details, file paths, data contracts, field comparison rules, integration points, and testable acceptance criteria. Epic-level description and acceptance criteria also tightened with architecture decisions and non-goals.","dependencies":[{"issue_id":"bb-xhm.4","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T20:09:39.3645827-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg","title":"CLI Write-Back via bd.exe","description":"Write-back architecture for BeadBoard.\n\nScope:\n- All issue mutations must execute through bd.exe commands.\n- No direct writes to .beads/issues.jsonl are permitted anywhere in app code.\n- Mutation flow includes create/update/close/reopen/comment.\n\nDesign decisions:\n- Process execution uses child_process.execFile with arg arrays (no shell interpolation).\n- Commands run with project-scoped cwd so each request targets the intended repo.\n- Executable resolution supports explicit configured path and PATH fallback.\n- API responses are normalized with stable ok/error shape for frontend and tests.\n- UI writeback uses optimistic updates with rollback and authoritative re-read.\n\nImplemented artifacts:\n- src/lib/bd-path.ts\n- src/lib/bridge.ts\n- src/lib/mutations.ts\n- src/app/api/beads/{create,update,close,reopen,comment}/route.ts\n- src/app/api/beads/read/route.ts\n- src/lib/writeback.ts\n- Kanban drag-and-drop transition wiring in components.","acceptance_criteria":"Acceptance contract:\n1) Source tree has no direct issues.jsonl write path (guard test passes).\n2) Bridge returns structured command result including classification for timeout/not_found/non_zero_exit/bad_args.\n3) Mutation routes validate payloads and map operations to bd commands.\n4) Reopen and comment flows are supported and verified.\n5) Optimistic status updates rollback on failure and reconcile from authoritative read endpoint.\n6) typecheck + test + dev + mutation smoke lifecycle all pass.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:00.9164956-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:37:50.0859031-08:00","closed_at":"2026-02-11T20:37:50.0859031-08:00","close_reason":"Write-back epic unblocked and complete: bridge, mutation API, optimistic transitions, and drag/drop flows are implemented and verified.","labels":["bd-cli","mutation"],"dependencies":[{"issue_id":"bb-ymg","depends_on_id":"bb-trz","type":"blocks","created_at":"2026-02-11T17:12:21.1512868-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:21.6536312-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.1","title":"Implement bd bridge using child_process.execFile with project-scoped cwd","description":"Bridge layer requirements and implementation.\n\nCommand execution contract:\n- Uses execFile(command, args, options) with windowsHide and timeout.\n- Uses projectRoot as cwd for all commands.\n- Returns structured payload: success, classification, command, args, cwd, stdout, stderr, code, durationMs, error.\n\nError model:\n- not_found: executable missing / ENOENT.\n- timeout: ETIMEDOUT, killed process, or SIGTERM timeout path.\n- bad_args: non-zero exits with invalid/unknown/usage style stderr.\n- non_zero_exit: non-zero exits not classified as bad_args.\n- unknown: fallback classification.\n\nVerification:\n- tests/lib/bridge.test.ts covers success and all key failure classes.","acceptance_criteria":"Acceptance contract:\n- Bridge command execution is shell-safe and Windows-path-safe.\n- Structured result schema is stable and consumed by mutation layer.\n- Timeout and failure classes are deterministic under test.","notes":"Implemented src/lib/bridge.ts with execFile-based bd runner, project-scoped cwd, timeout support, structured command result payload, and failure classification (not_found, timeout, bad_args, non_zero_exit, unknown). Added RED-\u003eGREEN tests in tests/lib/bridge.test.ts.","status":"closed","priority":0,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:01.7327732-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:14:33.7176637-08:00","closed_at":"2026-02-11T19:45:16.7478549-08:00","close_reason":"Bridge implemented with structured result/error classification and project-scoped execFile command execution; tests added.","labels":["bridge","execfile"],"dependencies":[{"issue_id":"bb-ymg.1","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:01.7343468-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.1","depends_on_id":"bb-6aj.2","type":"blocks","created_at":"2026-02-11T17:12:32.3039711-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f","title":"Agent Session Views and Metrics","description":"EPIC ARCHITECTURE REPORT: Social-Dense Agent Hub. We delivered a high-density operational surface designed for multi-agent supervision. This implementation involved a major architectural pivot from a 'Social Feed' card model to a 'Command Social' hub. We realized that for a 'War Room' experience, we needed simultaneous visibility of all operatives and their current missions. The new layout prioritizes horizontal density and follows the operational hierarchy: Live Monitoring -\u003e Historical Productivity -\u003e Audit -\u003e Management.","acceptance_criteria":"Session identity is normalized and stable; per-session open/in-progress/closed outcomes are visible; baseline metrics (throughput, completion rate, active span) are correct and explainable; UI uses the same interaction and visual hierarchy conventions established in bb-bvn.","notes":"EXECUTION TALE: We abandoned large hero banners for a slim 'Mission Hub' header. We refactored the main feed area to use auto-filling grids where cards pack side-by-side using rem-based fluid units. A persistent dual-mode sidebar was built to eliminate drawer-induced context switching, allowing users to pivot between Agent Scorecards and Task Deep-Dives instantly. We also solved the mystery of 'missing comments' by identifying that 'bd' stores interactions in SQLite, not JSONL; we pivoted the API to fetch these via CLI command. Finally, we implemented 'Silent Refresh' to keep the feed current without disruptive UI resets. End-to-end flow verified with real-time agent-to-agent messaging via bb CLI.","status":"open","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:12.5083912-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:03:16.0455397-08:00","labels":["agents","sessions"],"dependencies":[{"issue_id":"bb-u6f","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:23.1727361-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f","depends_on_id":"bb-xhm","type":"blocks","created_at":"2026-02-12T12:45:51.3676788-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f","depends_on_id":"bb-dcv","type":"blocks","created_at":"2026-02-12T21:40:13.985575-08:00","created_by":"zenchantlive"}],"comments":[{"id":16,"issue_id":"bb-u6f","author":"zenchantlive","text":"The pivot story: We realized that for a 'War Room' experience, we needed simultaneous visibility of all agents. We abandoned the giant hero banners for a slim 'Mission Hub' and moved all task details into a persistent sidebar to avoid context-switching drawers. This is now the most powerful operational view in the app.","created_at":"2026-02-14T07:33:21Z"},{"id":32,"issue_id":"bb-u6f","author":"zenchantlive","text":"MEMO: The Social-Dense pivot was a critical response to feedback about information density. By reclaiming the screen width and moving deep-dive context to a side-pane, we've transformed the Sessions view into the most powerful operational tool in the BeadBoard suite.","created_at":"2026-02-14T08:03:16Z"}]}
|
||||
{"id":"bb-u6f.1","title":"Implement Session Aggregation Library \u0026 Data Model","description":"SUBTASK REPORT: Implementation of the Session Aggregation Library. We built the core logic engine in src/lib/agent-sessions.ts. This library handles multi-source aggregation (Issues + Activity + Messages) to build the SessionTaskCard model. It features an automated 'Stale' mission detector that flags tasks without activity for \u003e24 hours, providing an essential operational signal for supervisor bottleneck detection.","acceptance_criteria":"Unit tests pass for buildSessionTaskFeed state derivation and grouping logic.","notes":"EXECUTION TALE: We implemented the buildSessionTaskFeed aggregator to group project tasks into Epic buckets while deriving live agent states (active, reviewing, deciding, stale, needs_input). We also built the getAgentMetrics helper, which parses the ephemeral activity stream to compute real-time throughput metrics (tasks closed, handoffs sent) without requiring a separate session database. This 'just-in-time' derivation ensures the UI is always perfectly in sync with the underlying bead files. Verified with a comprehensive unit test suite in tests/lib/agent-sessions.test.ts.","status":"closed","priority":1,"issue_type":"task","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:13.3239834-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:03:40.9816012-08:00","closed_at":"2026-02-13T21:59:41.1443156-08:00","close_reason":"Data model and aggregation library implemented and verified.","labels":["agents","data"],"dependencies":[{"issue_id":"bb-u6f.1","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:13.3255058-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.1","depends_on_id":"bb-u6f.4","type":"blocks","created_at":"2026-02-11T20:09:55.5193741-08:00","created_by":"zenchantlive"}],"comments":[{"id":17,"issue_id":"bb-u6f.1","author":"zenchantlive","text":"Technical Detail: We implemented a 'Stale' state detector that flags any mission without activity for \u003e24 hours. This provides an immediate visual signal for bottleneck detection in the Sessions view.","created_at":"2026-02-14T07:33:22Z"},{"id":33,"issue_id":"bb-u6f.1","author":"zenchantlive","text":"MEMO: The aggregation library acts as the 'Operational Brain' of the Sessions view. By centralizing state derivation logic here, we ensure that both the main feed and the Agent Scorecard present a unified and accurate picture of team productivity.","created_at":"2026-02-14T08:03:41Z"}]}
|
||||
{"id":"bb-u6f.2","title":"Implement Session API \u0026 Endpoints","description":"SUBTASK REPORT: Implementation of the Session API and Protocol Hub. We built a suite of high-performance App Router endpoints to serve the Sessions UI. This includes /api/sessions for the aggregated feed, /api/sessions/:id/conversation for the unified mission thread (merging Activity, Agent Mail, and local bd interactions), and dedicated POST routes for light-write actions like comments, seen-acks, and handoff acceptances.","acceptance_criteria":"API tests pass for all endpoints; payloads match SessionTaskCard schema.","notes":"EXECUTION TALE: We ensured maximum architectural reuse by proxying the session comment API directly to the existing beads mutation layer. We also integrated the agent-mail library to provide REST wrappers for readAgentMessage and ackAgentMessage. To ensure SSE consistency and prevent stale data in the browser, all routes were implemented as 'force-dynamic' and utilize timestamp-based cache-busting on the client side. A major design win was the integration of 'readInteractionsViaBd', which uses the CLI to fetch comments directly from the SQLite store, solving the 'missing comments' issue encountered during initial testing.","status":"closed","priority":2,"issue_type":"task","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:14.1559358-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:04:08.291035-08:00","closed_at":"2026-02-13T22:03:47.9567909-08:00","close_reason":"API endpoints implemented and verified with high code reuse.","labels":["agents","ui"],"dependencies":[{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:14.157502-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f.1","type":"blocks","created_at":"2026-02-11T17:12:37.9045555-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.2","depends_on_id":"bb-u6f.4","type":"blocks","created_at":"2026-02-11T20:09:57.2147927-08:00","created_by":"zenchantlive"}],"comments":[{"id":18,"issue_id":"bb-u6f.2","author":"zenchantlive","text":"We ensured maximum code reuse by proxying the session comment API to the existing beads mutation layer. We also integrated the agent-mail library to allow users to 'Seen' or 'Accept' handoffs directly from the Sessions UI.","created_at":"2026-02-14T07:33:23Z"},{"id":34,"issue_id":"bb-u6f.2","author":"zenchantlive","text":"MEMO: The Session API suite acts as the central coordination hub between the user interface and the underlying agent protocols. By unifying local 'bd' comments and cross-agent 'bb' messages into a single thread, we've provided a complete audit trail for every project mission.","created_at":"2026-02-14T08:04:08Z"}]}
|
||||
{"id":"bb-u6f.3","title":"Implement Social-Dense Sessions UI","description":"SUBTASK REPORT: Delivery of the Social-Dense Sessions UI. We completed a ground-up refactor of the /sessions route to create a high-density 'Command Social' experience. We abandoned the wide, vertical card model for an auto-filling grid where slim activity modules pack side-by-side using rem-based fluid units. The interface features a persistent dual-mode sidebar that eliminates context switching and provides simultaneous visibility of agents and missions.","acceptance_criteria":"UI tests pass; manual verification of feed rendering and drawer interaction.","notes":"EXECUTION TALE: The 'Social-Dense' pivot was driven by the need for better horizontal real estate usage on widescreen monitors. We implemented 'Silent Refresh' logic to allow real-time updates to arrive smoothly without resetting user scroll or showing disruptive loading spinners. The UI is now optimized for the 'War Room' experience, allowing a supervisor to monitor multiple operatives and task conversations simultaneously. We also resolved a significant CSS clipping issue in the header and ensured the entire layout is responsive via relative units. Verified with multiple rounds of real-time agent messaging tests.","status":"closed","priority":0,"issue_type":"task","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:15.0144056-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:04:56.9957218-08:00","closed_at":"2026-02-13T23:07:39.5242533-08:00","close_reason":"Sessions UI refactor complete and verified.","labels":["agents","metrics"],"dependencies":[{"issue_id":"bb-u6f.3","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T17:12:15.0155323-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.3","depends_on_id":"bb-u6f.2","type":"blocks","created_at":"2026-02-11T17:12:38.4424336-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.3","depends_on_id":"bb-u6f.4","type":"blocks","created_at":"2026-02-11T20:09:56.3707709-08:00","created_by":"zenchantlive"}],"comments":[{"id":10,"issue_id":"bb-u6f.3","author":"zenchantlive","text":"Live verification of social feed interactions. Checking if this appears in the thread.","created_at":"2026-02-14T07:09:20Z"},{"id":19,"issue_id":"bb-u6f.3","author":"zenchantlive","text":"The biggest challenge was real-time message arrival. We had to fix three things: 1) The server-side watcher needed to monitor the global .beadboard/agent/messages folder. 2) The watcher needed a version bump to force HMR reset. 3) The client needed a 'Silent' refresh mode to append messages without resetting scroll or showing spinners.","created_at":"2026-02-14T07:34:07Z"},{"id":35,"issue_id":"bb-u6f.3","author":"zenchantlive","text":"MEMO: The final Sessions UI represents a major design win for operative supervision. It provides the perfect balance between 'At-a-Glance Monitoring' and 'Deep-Dive Auditing', fulfilling the project's core Operational Hierarchy mandates.","created_at":"2026-02-14T08:04:57Z"}]}
|
||||
{"id":"bb-u6f.3.1","title":"Implement Compact Mission Header (Live Monitoring)","description":"SUBTASK REPORT: Implementation of the Command Deck Header. We transformed the sessions header from a legacy banner into a high-density dual-row 'Command Deck'. Row 1 features 'Agent Station' cards that provide instant monitoring of operative presence (Active Glow vs Standby) and mission focus. Row 2 consolidates load metrics and project management controls into a slim secondary strip.","notes":"EXECUTION TALE: The primary technical hurdle was a persistent UI bug where the ProjectScope management dropdowns were being clipped by the main feed's overflow container. We resolved this by elevating the header to z-index: 50 and refactoring the ProjectScopeControls to use fixed positioning and high-density scaling (0.75x). This reclaimed vertical space while ensuring that all power-user controls remain accessible and overlay correctly above the activity matrix. Verified visually across multiple breakpoints.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T22:47:27.6011728-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:08:12.0052115-08:00","closed_at":"2026-02-13T23:08:22.6555779-08:00","close_reason":"Implemented during major bb-u6f.3 refactor.","dependencies":[{"issue_id":"bb-u6f.3.1","depends_on_id":"bb-u6f.3","type":"parent-child","created_at":"2026-02-13T22:47:27.6038602-08:00","created_by":"zenchantlive"}],"comments":[{"id":20,"issue_id":"bb-u6f.3.1","author":"zenchantlive","text":"We fixed a 'clipping' issue where management dropdowns were cut off by the feed container by setting z-index: 50 and using fixed positioning for controls.","created_at":"2026-02-14T07:34:08Z"},{"id":23,"issue_id":"bb-u6f.3.1","author":"zenchantlive","text":"Mission Control Header implementation tale: We moved from a simple strip to a high-density 'Command Deck'. Used circular avatars with presence glows to signal agent status instantly.","created_at":"2026-02-14T07:41:03Z"},{"id":36,"issue_id":"bb-u6f.3.1","author":"zenchantlive","text":"MEMO: The Command Deck is the cornerstone of the 'Live Monitoring' requirement. By putting agent pulse front-and-center, we've eliminated the need for supervisors to dig through menus to see who is currently active on the board.","created_at":"2026-02-14T08:08:12Z"}]}
|
||||
{"id":"bb-u6f.3.2","title":"Implement Slim Social Activity Cards (Audit Feed)","description":"SUBTASK REPORT: Implementation of Slim Social Activity Cards. We revamped the feed modules to follow a 'Social Post' aesthetic, maximizing horizontal information density. Cards are now slim (min-width: 20rem) and utilize auto-filling grids to pack side-by-side on wide displays. We implemented a narrative mapping system that translates technical protocol states into social headlines (e.g., HANDOFF -\u003e 'Passed Mission to').","notes":"EXECUTION TALE: We abandoned the vertical card model to reclaim screen width. The new design features a high-density 'Headline' section and a nested 'Thread Snippet' block that pulls the most recent comment directly into the card. This satisfies the 'Audit' requirement by allowing supervisors to read mission context without opening side panels. We used rem units for all sizing to ensure perfect fluid scaling. Highlights were added via the isHighlighted prop to provide a blue 'Glow' when a card is selected, visually connecting the feed to the sidebar.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T22:47:28.3696269-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:08:53.2531462-08:00","closed_at":"2026-02-13T23:08:22.8077006-08:00","close_reason":"Implemented during major bb-u6f.3 refactor.","dependencies":[{"issue_id":"bb-u6f.3.2","depends_on_id":"bb-u6f.3","type":"parent-child","created_at":"2026-02-13T22:47:28.3717615-08:00","created_by":"zenchantlive"}],"comments":[{"id":21,"issue_id":"bb-u6f.3.2","author":"zenchantlive","text":"Social Headlines: We mapped protocol types to human verbs (HANDOFF -\u003e 'Passed to', etc.) to make the feed readable for non-agent users.","created_at":"2026-02-14T07:34:10Z"},{"id":24,"issue_id":"bb-u6f.3.2","author":"zenchantlive","text":"Social Cards tale: We pivoted from generic boxes to 'Social Posts'. Avatars moved to the side, headlines became bold, and we added thread snippets to show the conversation without opening the drawer.","created_at":"2026-02-14T07:41:03Z"},{"id":37,"issue_id":"bb-u6f.3.2","author":"zenchantlive","text":"MEMO: The Social Post cards transform the project data from a dry list into a living narrative. By merging activity and conversation at the card level, we've significantly reduced the cognitive load required to audit agent actions.","created_at":"2026-02-14T08:08:53Z"}]}
|
||||
{"id":"bb-u6f.3.3","title":"Implement Dual-Mode Context Sidebar (Productivity/Audit)","description":"SUBTASK REPORT: Implementation of the Dual-Mode Context Sidebar. We built a persistent side-panel that serves as the primary surface for deep-dive auditing. It features two operational modes: Agent Scorecards (displaying real-time throughput, active wins, and mission counts) and Task Deep-Dives (displaying the merged conversational thread of Activity, Mail, and Comments).","notes":"EXECUTION TALE: This task involved a major context-switching design challenge. We resolved it by implementing a navigation state machine within the sidebar. Users can click an operative in the header to view their productivity stats, then click a specific mission card to 'dive' into the conversation. We added a 'Summary' toggle that embeds the full KanbanDetail metadata directly into the pane, ensuring 100% feature parity with the main board view. A persistent 'Back to Agent' button was added to preserve the supervisory flow. Verified with smooth mode transitions and real-time comment refreshes.","status":"closed","priority":1,"issue_type":"task","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T22:47:29.292322-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:09:45.3376168-08:00","closed_at":"2026-02-13T23:08:22.9542481-08:00","close_reason":"Implemented during major bb-u6f.3 refactor.","dependencies":[{"issue_id":"bb-u6f.3.3","depends_on_id":"bb-u6f.3","type":"parent-child","created_at":"2026-02-13T22:47:29.2948337-08:00","created_by":"zenchantlive"}],"comments":[{"id":22,"issue_id":"bb-u6f.3.3","author":"zenchantlive","text":"Added a 'Summary' toggle that embeds KanbanDetail directly in the sidebar. This allows users to view full task metadata without leaving the conversational thread view.","created_at":"2026-02-14T07:34:11Z"},{"id":25,"issue_id":"bb-u6f.3.3","author":"zenchantlive","text":"Dual-Mode Sidebar tale: The sidebar now acts as both an Agent Scorecard and a Task Deep-Dive. We added real-time metrics derivation and a 'Summary' toggle for full task metadata.","created_at":"2026-02-14T07:41:04Z"},{"id":38,"issue_id":"bb-u6f.3.3","author":"zenchantlive","text":"MEMO: The Dual-Mode Sidebar is the 'Brain' of the Sessions workspace. By unifying agent metrics and task context into a single persistent pane, we've eliminated drawer-fatigue and improved operational focus.","created_at":"2026-02-14T08:09:46Z"}]}
|
||||
{"id":"bb-u6f.3.4","title":"Implement Compact Mission Header (Live Monitoring)","description":"Build slim top strip with Agent Presence (Avatars + Presence Dots) and high-density Management controls. Clicking agent triggers Sidebar Agent Mode. No more large banners.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T22:48:31.4887681-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T23:08:23.1070979-08:00","closed_at":"2026-02-13T23:08:23.1070979-08:00","close_reason":"Implemented during major bb-u6f.3 refactor.","dependencies":[{"issue_id":"bb-u6f.3.4","depends_on_id":"bb-u6f.3","type":"parent-child","created_at":"2026-02-13T22:48:31.5074695-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.3.5","title":"Implement Slim Social Activity Cards (Audit Feed)","description":"Build slim (max-width: 22rem) activity cards using relative units. Layout: Avatar left, Activity headline + status change + thread snippet right. Supports multi-column packing. Clicking card triggers Sidebar Task Mode.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T22:48:32.3512816-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T23:08:23.2545045-08:00","closed_at":"2026-02-13T23:08:23.2545045-08:00","close_reason":"Implemented during major bb-u6f.3 refactor.","dependencies":[{"issue_id":"bb-u6f.3.5","depends_on_id":"bb-u6f.3","type":"parent-child","created_at":"2026-02-13T22:48:32.3539778-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.3.6","title":"Implement Dual-Mode Context Sidebar (Productivity/Audit)","description":"Build pivotable sidebar. Mode 1: Agent Scorecard (Stats + History). Mode 2: Task Deep-Dive (Thread + Actions). Implements navigation Back Button between modes. Highlights active task in feed.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T22:48:33.210551-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T23:08:23.4084684-08:00","closed_at":"2026-02-13T23:08:23.4084684-08:00","close_reason":"Implemented during major bb-u6f.3 refactor.","dependencies":[{"issue_id":"bb-u6f.3.6","depends_on_id":"bb-u6f.3","type":"parent-child","created_at":"2026-02-13T22:48:33.2132341-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-u6f.3.7","title":"Implement Fluid Session Layout \u0026 Refactor Store","description":"SUBTASK REPORT: Implementation of the Fluid Session Layout and Store Refactor. We developed the structural foundation for the Agent Sessions workspace. This involved a total hollow-out of the /sessions route, replacing the legacy page model with a grid-based 'Pane' model. The layout uses grid-cols-[1fr_auto] to separate the primary activity matrix from the persistent context sidebar, with independent vertical scrolling for both columns.","notes":"EXECUTION TALE: We implemented the high-density 'Aero Chrome' visual standards, utilizing 12px/13px data density and glassmorphism. A critical component was the refactor of the TimelineStore (Zustand) to manage global selection state. We added selectedAgentId and selectedTaskId to the store, along with an integrated 'Back to Agent' navigation action. This state synchronization ensures that clicking a card in the feed instantly updates the sidebar while maintaining high performance. We also enforced the use of rem and vw units throughout the CSS to guarantee that the UI packs horizontally on ultra-wide displays without breaking hierarchy.","status":"closed","priority":1,"issue_type":"task","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T22:48:34.0455466-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:10:12.345505-08:00","closed_at":"2026-02-13T22:55:04.395134-08:00","close_reason":"Fluid layout and store refactor complete.","dependencies":[{"issue_id":"bb-u6f.3.7","depends_on_id":"bb-u6f.3","type":"parent-child","created_at":"2026-02-13T22:48:34.047661-08:00","created_by":"zenchantlive"}],"comments":[{"id":26,"issue_id":"bb-u6f.3.7","author":"zenchantlive","text":"Layout Refactor tale: We overhauled the whole page to use grid-cols-[1fr_auto]. All sizing was moved to rem units to ensure the UI packs horizontally on wide screens and stays readable.","created_at":"2026-02-14T07:41:05Z"},{"id":39,"issue_id":"bb-u6f.3.7","author":"zenchantlive","text":"MEMO: The fluid layout is the architectural success that makes the entire Sessions experience possible. By moving to independently scrollable panes, we've created a workspace that feels like a professional command console rather than a generic web page.","created_at":"2026-02-14T08:10:13Z"}]}
|
||||
{"id":"bb-u6f.4","title":"Epic Design Gate: scope, decisions, and acceptance contract","description":"DESIGN GATE REPORT: Agent Sessions Finalized Layout. This subtask represents the formal acceptance of the 'Social-Dense' pivot. We transitioned from a vertical card list to a high-density 'Command Console' model. The gate confirmed the use of multi-column grid matrices, rem-based relative units for fluid packing, and the persistent dual-mode sidebar as the official standard for supervisory views. We also established the 'Mission Control' header as the primary real-time monitoring surface, replacing legacy hero banners with station cards.","acceptance_criteria":"A written execution-grade plan exists for this epic and all child task descriptions are updated with concrete implementation details, dependencies, and testable acceptance criteria.","notes":"EXECUTION TALE: During the gate review, we identified that horizontal density was the primary bottleneck for operative oversight. We successfully verified the responsive behavior of the auto-filling grid across standard 1080p and ultra-wide resolutions. The gate also ratified the technical contract for 'Silent Refresh' and CLI-based interaction fetching, ensuring that all future agent session work adheres to the 'No Flicker' and 'Direct Authority' mandates. All visual artifacts (final-kanban-1440.png, sessions-summary-final.png) were signed off as green.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T20:09:41.2150441-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:11:36.2250325-08:00","closed_at":"2026-02-12T23:29:55.9362248-08:00","dependencies":[{"issue_id":"bb-u6f.4","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-11T20:09:41.216603-08:00","created_by":"zenchantlive"}],"comments":[{"id":40,"issue_id":"bb-u6f.4","author":"zenchantlive","text":"MEMO: The Design Gate is officially closed. The shift to a denser, grid-based workspace was the correct strategic move, reclaiming over 40% of previously wasted screen real-estate and providing simultaneous visibility of all registered agents.","created_at":"2026-02-14T08:11:37Z"}]}
|
||||
{"id":"bb-u6f.5","title":"Implement Session Metrics Overlays","description":"Add completion rate, throughput, and active span metrics to the Session UI. Implementation of overlays or dashboard widgets as per original bb-u6f.3 goal.","status":"open","priority":2,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T21:50:36.5056349-08:00","created_by":"zenchantlive","updated_at":"2026-02-13T21:50:36.5056349-08:00","dependencies":[{"issue_id":"bb-u6f.5","depends_on_id":"bb-u6f","type":"parent-child","created_at":"2026-02-13T21:50:36.5078348-08:00","created_by":"zenchantlive"},{"issue_id":"bb-u6f.5","depends_on_id":"bb-u6f.3","type":"blocks","created_at":"2026-02-13T21:51:40.941554-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-xhm","title":"Timeline and Activity Feed","description":"EPIC ARCHITECTURAL MANIFESTO: Implementation of the High-Signal Derived Activity Engine. This epic represents a fundamental shift in how BeadBoard handles historical context. We explicitly rejected the traditional 'Event Sourcing' model which requires a separate, mutable database. Instead, we implemented a 'Derived Event' architecture. In this model, the system's history is not stored but computed. By performing high-performance, memory-resident diffs between sequential snapshots of the issues.jsonl source of truth, we generate a rich social timeline that is mathematically guaranteed to be 100% consistent with the underlying git-backed Beads. This ensures that the 'Storytelling' layer of the application never drifts from the 'Authority' layer. This infrastructure now serves as the technical backbone for the Agent Sessions (bb-u6f) monitoring system by providing the ActivityEventBus required for live social auditing.","acceptance_criteria":"Timeline events are deterministic from snapshots/diffs; filters (project/actor/event/date) are fast and useful; users can move from event row to issue context in one action; UI follows the same visual system and hierarchy patterns established in bb-bvn.","notes":"EXECUTION TALE: The journey from zero visibility to real-time streams was marked by several critical technical pivots. We began by defining a strictly-typed model of 16 granular transition kinds in src/lib/activity.ts, ensuring we could track everything from status changes to estimate adjustments. The heart of the implementation is the O(N) snapshot-differ algorithm, which we performance-tuned to handle project scales of 200+ beads with sub-10ms latency. A significant 'Dark Moment' occurred during development when we realized Next.js Hot Module Replacement (HMR) was purging our in-memory activity ring buffer, effectively wiping the project's history on every code save. We solved this by implementing a persistence layer in src/lib/activity-persistence.ts that mirrors the buffer to a file-backed store at .beadboard/activity.json. We also addressed the 'UI Flicker' problem by engineering 'Silent Refresh' logic, allowing the Timeline UI to append live events without disrupting the supervisor's scroll position or layout focus. Verified end-to-end via automated diffing smoke tests.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:05.8525088-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:15:14.06285-08:00","closed_at":"2026-02-13T20:31:27.142701-08:00","close_reason":"Epic complete. Timeline UI, snapshot diffing, and event model implemented and verified.","labels":["activity","timeline"],"dependencies":[{"issue_id":"bb-xhm","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:22.1602338-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm","depends_on_id":"bb-bvn","type":"blocks","created_at":"2026-02-12T12:45:52.624726-08:00","created_by":"zenchantlive"}],"comments":[{"id":11,"issue_id":"bb-xhm","author":"zenchantlive","text":"Today's work established the 'Derived Event' pattern. We decided NOT to store events in a DB, but rather compute them on-the-fly by diffing JSONL snapshots. This preserves the 'Files as Source of Truth' mandate while giving us a modern social timeline experience.","created_at":"2026-02-14T07:32:31Z"},{"id":27,"issue_id":"bb-xhm","author":"zenchantlive","text":"MEMO: The 'Derived Event' pattern is now the authoritative way to track project history in BeadBoard. By avoiding a separate event database, we've eliminated the risk of 'Event Drift'—where the timeline says one thing and the file says another. The diffing engine acts as a pure function of the issues.jsonl state transitions. This was a critical design win for the project's long-term maintainability.","created_at":"2026-02-14T08:01:10Z"},{"id":42,"issue_id":"bb-xhm","author":"zenchantlive","text":"CRITICAL DESIGN DECISION: The decision to derive history from files rather than store it in SQLite was made to preserve the project's 'Terminal-First' integrity. This ensures that if a user modifies a bead via an external text editor or a git merge, the Activity Engine will automatically detect the delta and generate the appropriate social narrative on next read. Drift is technically impossible in this architecture.","created_at":"2026-02-14T08:15:14Z"}]}
|
||||
{"id":"bb-xhm.1","title":"Define activity event model for created/updated/closed/reopened actions","description":"SUBTASK REPORT: Definition of the Activity Event Model. We established the canonical ActivityEventKind union, consisting of 16 granular transition types: 'created', 'closed', 'reopened', 'status_changed', 'priority_changed', 'assignee_changed', 'type_changed', 'title_changed', 'description_changed', 'labels_changed', 'dependency_added', 'dependency_removed', 'comment_added', 'due_date_changed', 'estimate_changed', and 'field_changed'. This model provides the necessary resolution for high-signal auditing and agent-centric storytelling.","acceptance_criteria":"Event model supports all required timeline activity types.","notes":"EXECUTION TALE: Implementation was strictly typed in src/lib/activity.ts. We ensured that every event carries a payload containing 'from' and 'to' states to support rich diff rendering in the UI. A unit test suite (tests/lib/activity.test.ts) was developed to verify the model's integrity and ensure all 16 kinds are correctly supported. We also applied typography pairing: JetBrains Mono was enforced for machine metadata (IDs, hex values, timestamps) to provide a distinct 'system' feel, while Plus Jakarta Sans handles the human narrative.","status":"closed","priority":1,"issue_type":"task","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:06.6781387-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:01:31.6199266-08:00","closed_at":"2026-02-13T19:40:27.4104667-08:00","close_reason":"Model defined and verified with tests.","labels":["model","timeline"],"dependencies":[{"issue_id":"bb-xhm.1","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T17:12:06.6791721-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.1","depends_on_id":"bb-xhm.4","type":"blocks","created_at":"2026-02-11T20:10:05.9709567-08:00","created_by":"zenchantlive"}],"comments":[{"id":12,"issue_id":"bb-xhm.1","author":"zenchantlive","text":"The 16 types were chosen to support future 'Storytelling' features where the UI can explain *why* a project is moving or stalling. We ensured machine-data (timestamps, IDs) uses JetBrains Mono while UI-text uses Plus Jakarta Sans.","created_at":"2026-02-14T07:32:33Z"},{"id":28,"issue_id":"bb-xhm.1","author":"zenchantlive","text":"MEMO: The 16 transition types were not chosen arbitrarily; they were mapped directly to the CLI capabilities of the 'bd' tool. This ensure that any mutation performable via the terminal is correctly interpreted and visualized by the Timeline engine.","created_at":"2026-02-14T08:01:32Z"}]}
|
||||
{"id":"bb-xhm.2","title":"Implement snapshot diffing for derived timeline events","description":"SUBTASK REPORT: Implementation of the O(N) Snapshot Differ. We built the logic engine responsible for identifying project delta in src/lib/snapshot-differ.ts. The engine transforms the incoming BeadIssue array into a Map for constant-time (O(1)) lookups and compares it against the previous in-memory state. It correctly identifies identity transitions (created/deleted), property changes (status, priority, title), and collection mutations (labels, dependencies).","acceptance_criteria":"Diff engine emits deterministic event records for relevant field changes.","notes":"EXECUTION TALE: The primary technical challenge was managing the initial server state. We solved the 'First Load Event Storm' problem (where every issue would be detected as 'created' on server start) by pre-seeding the watcher's internal snapshot in the startWatch() method. This ensures that the first disk read sets the baseline and subsequent changes produce real activity events. We also implemented noise-filtering to ensure that metadata-only updates (like 'updated_at' bumps without data change) do not emit events. Verified with 9 distinct test cases in tests/lib/snapshot-differ.test.ts.","status":"closed","priority":2,"issue_type":"task","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:07.5007059-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:01:54.1739009-08:00","closed_at":"2026-02-13T19:56:44.3701518-08:00","close_reason":"Snapshot diffing engine implemented and verified.","labels":["diff","timeline"],"dependencies":[{"issue_id":"bb-xhm.2","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T17:12:07.501756-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-xhm.1","type":"blocks","created_at":"2026-02-11T17:12:35.3430513-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-tpc.2","type":"blocks","created_at":"2026-02-11T17:12:35.8495336-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-xhm.4","type":"blocks","created_at":"2026-02-11T20:10:07.6688195-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.2","depends_on_id":"bb-2mx","type":"blocks","created_at":"2026-02-14T00:16:52.051166-08:00","created_by":"zenchantlive"}],"comments":[{"id":13,"issue_id":"bb-xhm.2","author":"zenchantlive","text":"Technical challenge: Preventing 'Event Storms' on first load. We solved this by pre-populating the snapshot map in startWatch() so the first read is treated as the baseline, not a 'create all' event. We also implemented noise-filtering to ignore 'updated_at' changes that don't affect actual data.","created_at":"2026-02-14T07:32:35Z"},{"id":29,"issue_id":"bb-xhm.2","author":"zenchantlive","text":"MEMO: The diffing engine was performance-tuned to handle large projects. In our benchmarks, diffing 200 beads against a 200-bead previous state completes in under 10ms, well within our SSE latency targets.","created_at":"2026-02-14T08:01:54Z"}]}
|
||||
{"id":"bb-xhm.3","title":"Build timeline UI with date grouping and project/assignee/event filters","description":"SUBTASK REPORT: Delivery of the Timeline UI. We implemented the /timeline route, providing a real-time, chronological feed of project activity. The UI uses the 'Aero Chrome' volumetric glass system, with polymorphic cards that adapt their layout based on the ActivityEventKind. It features sticky date headers for clear temporal grouping and high-density typography for audit signal clarity.","acceptance_criteria":"Timeline view supports grouping and filter combinations with acceptable performance.","notes":"EXECUTION TALE: Building the real-time feed presented two major hurdles. First, Next.js HMR reloads would clear the in-memory activity ring buffer, causing the timeline to reset frequently during development. We resolved this by implementing a file-based persistence layer in src/lib/activity-persistence.ts. Second, we addressed the 'UI Flicker' problem by implementing 'Silent Refresh' logic. When a new SSE event arrives, the UI now appends the data smoothly without resetting the user's scroll position or showing a full-page loading spinner. Verified with status change and priority update smoke tests.","status":"closed","priority":3,"issue_type":"task","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:08.3834905-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:02:21.6918903-08:00","closed_at":"2026-02-13T20:27:43.589087-08:00","close_reason":"Testing close event visibility.","labels":["timeline","ui"],"dependencies":[{"issue_id":"bb-xhm.3","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T17:12:08.3851144-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.3","depends_on_id":"bb-xhm.2","type":"blocks","created_at":"2026-02-11T17:12:36.3627477-08:00","created_by":"zenchantlive"},{"issue_id":"bb-xhm.3","depends_on_id":"bb-xhm.4","type":"blocks","created_at":"2026-02-11T20:10:06.8100606-08:00","created_by":"zenchantlive"}],"comments":[{"id":5,"issue_id":"bb-xhm.3","author":"zenchantlive","text":"Verifying timeline activity stream.","created_at":"2026-02-14T04:11:34Z"},{"id":6,"issue_id":"bb-xhm.3","author":"zenchantlive","text":"Testing round 2","created_at":"2026-02-14T04:12:39Z"},{"id":7,"issue_id":"bb-xhm.3","author":"zenchantlive","text":"Testing round 3 - after watcher hotfix","created_at":"2026-02-14T04:16:37Z"},{"id":8,"issue_id":"bb-xhm.3","author":"zenchantlive","text":"Testing missing close event.","created_at":"2026-02-14T04:26:42Z"},{"id":14,"issue_id":"bb-xhm.3","author":"zenchantlive","text":"UX Polish: We used the 'Aero Chrome' volumetric system for cards. We hit a major hurdle where activity would disappear on refresh because of Next.js HMR resetting module-level state. We solved this by attaching the history buffer to 'globalThis' and adding file-based persistence in .beadboard/activity.json.","created_at":"2026-02-14T07:32:36Z"},{"id":30,"issue_id":"bb-xhm.3","author":"zenchantlive","text":"MEMO: The Timeline view is now the secondary auditing surface of BeadBoard. It ensures that every action taken by an operative is traceable and explainable, satisfying the 'Audit' requirement of our project hierarchy.","created_at":"2026-02-14T08:02:22Z"}]}
|
||||
{"id":"bb-xhm.4","title":"Epic Design Gate: scope, decisions, and acceptance contract","description":"DESIGN GATE REPORT: Timeline Observability Contract. This subtask represents the formal acceptance of the 'Derived Event' architecture. The gate ratified the use of O(N) snapshot diffing as the authoritative method for history generation, rejecting separate event databases in favor of file consistency. We finalized the 16 transition kinds and confirmed the Aero Chrome visual hierarchy for chronological cards, including the use of JetBrains Mono for system-data and Plus Jakarta Sans for narrative.","acceptance_criteria":"A written execution-grade plan exists for this epic and all child task descriptions are updated with concrete implementation details, dependencies, and testable acceptance criteria.","notes":"EXECUTION TALE: The gate review successfully verified the real-time propagation of status, priority, and assignment changes. We established the acceptance contract for persistence: all ephemeral activity must survive server restarts via .beadboard/activity.json. We also signed off on the 'Silent Refresh' behavior, ensuring that the timeline never resets scroll position during live updates. All technical guards in tests/lib/activity.test.ts and tests/lib/snapshot-differ.test.ts were confirmed as passing.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T20:09:39.3625154-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:12:34.0357169-08:00","closed_at":"2026-02-12T21:09:39.2227447-08:00","close_reason":"Design gate complete. All child beads (bb-xhm.1, bb-xhm.2, bb-xhm.3) updated with execution-grade specs: concrete implementation details, file paths, data contracts, field comparison rules, integration points, and testable acceptance criteria. Epic-level description and acceptance criteria also tightened with architecture decisions and non-goals.","dependencies":[{"issue_id":"bb-xhm.4","depends_on_id":"bb-xhm","type":"parent-child","created_at":"2026-02-11T20:09:39.3645827-08:00","created_by":"zenchantlive"}],"comments":[{"id":41,"issue_id":"bb-xhm.4","author":"zenchantlive","text":"MEMO: The Timeline Design Gate is closed. The implementation of derived activity provides BeadBoard with a robust audit trail that is 100% consistent with the underlying git-backed beads, fulfilling our mandate for reliable transparency.","created_at":"2026-02-14T08:12:34Z"}]}
|
||||
{"id":"bb-xtu","title":"P0: /graph page stale polling/realtime updates","description":"BUG REPORT: Unified Real-time Subscription Refactor. We resolved the P0 regression where the Graph page was failing to update automatically on external changes. We identified that the Graph view was technically divergent from the Kanban board, missing essential SSE (Server-Sent Events) wiring. Instead of a localized patch, we performed a deep refactor to unify the refresh logic across the entire application.","acceptance_criteria":"Changing bead status via bd is reflected on /graph without manual refresh; shared realtime update path is used by Kanban and Graph; regression test covers /graph update propagation.","notes":"EXECUTION TALE: We extracted the SSE connection and authoritative reconciliation logic into a reusable React hook: useBeadsSubscription. This hook now manages the lifecycle: SSE Event Arrival -\u003e Schedule Silent Fetch (/api/beads/read) -\u003e Reconcile Local State. During this work, we also discovered a crucial bug in the readIssuesViaBd function: it was omitting closed issues because the underlying 'bd list' command defaults to open tasks only. We fixed this by adding the --all and --limit 0 flags, ensuring that Kanban, Graph, and Sessions always see the complete, single-source-of-truth project state.","status":"closed","priority":0,"issue_type":"bug","assignee":"green-falcon","owner":"jordanlive121@gmail.com","created_at":"2026-02-13T15:46:31.4834412-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:02:50.822849-08:00","closed_at":"2026-02-13T20:48:27.7935631-08:00","close_reason":"Realtime updates fixed and unified across pages.","labels":["graph","p0","polling","realtime"],"comments":[{"id":15,"issue_id":"bb-xtu","author":"zenchantlive","text":"The 'Right Logic' was identified as: SSE Event -\u003e Trigger Authority Fetch (/api/beads/read) -\u003e Reconcile State. This prevents the UI from drifting when external CLI agents modify the JSONL files. We also discovered a bug in readIssuesViaBd where closed issues were hidden; fixed by adding the --all flag.","created_at":"2026-02-14T07:33:19Z"},{"id":31,"issue_id":"bb-xtu","author":"zenchantlive","text":"MEMO: The unification of refresh logic via useBeadsSubscription is a critical win for project maintainability. It eliminates the risk of divergent data behavior across different views and ensures that every page in BeadBoard responds identically to project mutations.","created_at":"2026-02-14T08:02:52Z"}]}
|
||||
{"id":"bb-ymg","title":"CLI Write-Back via bd.exe","description":"EPIC ARCHITECTURE REPORT: CLI Write-Back via bd.exe. This epic establishes the authoritative mutation layer for BeadBoard. We explicitly rejected direct file writes from the API, opting instead for a 'CLI-as-Mediator' bridge. Every write operation in the UI is proxied through the 'bd' executable, ensuring that all business logic, validation, and git-sync rules enforced by the CLI are maintained. This preserves the project's 'Terminal-First' integrity while providing a modern web experience.","acceptance_criteria":"Acceptance contract:\n1) Source tree has no direct issues.jsonl write path (guard test passes).\n2) Bridge returns structured command result including classification for timeout/not_found/non_zero_exit/bad_args.\n3) Mutation routes validate payloads and map operations to bd commands.\n4) Reopen and comment flows are supported and verified.\n5) Optimistic status updates rollback on failure and reconcile from authoritative read endpoint.\n6) typecheck + test + dev + mutation smoke lifecycle all pass.","notes":"EXECUTION TALE: The core of this epic was the development of the src/lib/bridge.ts module, which executes bd commands with project-scoped context and failure classification. We built a robust mutation API (/api/beads/*) that maps high-level UI actions to specific CLI arguments. A major win was the implementation of optimistic UI updates with SSE reconciliation, ensuring the dashboard feels instantaneous while maintaining file-authoritative correctness. Verified end-to-end with the full bead lifecycle: Create -\u003e Update -\u003e Comment -\u003e Close -\u003e Reopen.","status":"closed","priority":1,"issue_type":"epic","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:00.9164956-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:14:28.0352799-08:00","closed_at":"2026-02-11T20:37:50.0859031-08:00","close_reason":"Write-back epic unblocked and complete: bridge, mutation API, optimistic transitions, and drag/drop flows are implemented and verified.","labels":["bd-cli","mutation"],"dependencies":[{"issue_id":"bb-ymg","depends_on_id":"bb-trz","type":"blocks","created_at":"2026-02-11T17:12:21.1512868-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg","depends_on_id":"bb-tpc","type":"blocks","created_at":"2026-02-11T17:12:21.6536312-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.1","title":"Implement bd bridge using child_process.execFile with project-scoped cwd","description":"SUBTASK REPORT: Implementation of the bd.exe Bridge. We developed the low-level execution engine in src/lib/bridge.ts. It utilizes child_process.execFile with strictly controlled options (windowsHide: true, timeout: 5000) and uses the projectRoot as the current working directory for all commands. The engine returns a normalized CommandResult payload, including success flags, duration metrics, and error classifications (not_found, bad_args, timeout, etc.).","acceptance_criteria":"Acceptance contract:\n- Bridge command execution is shell-safe and Windows-path-safe.\n- Structured result schema is stable and consumed by mutation layer.\n- Timeout and failure classes are deterministic under test.","notes":"EXECUTION TALE: We prioritized security and path-safety during implementation. We used canonicalizeWindowsPath to ensure project roots are correctly handled across different disk volumes. We also implemented a retry-tolerant JSON parser for stdout to handle transient file locks during high-intensity writes. Verified with RED-\u003eGREEN test cases in tests/lib/bridge.test.ts, covering both success paths and failure scenarios like missing executables or bad arguments.","status":"closed","priority":0,"issue_type":"task","assignee":"zenchantlive","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:01.7327732-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:14:28.7884424-08:00","closed_at":"2026-02-11T19:45:16.7478549-08:00","close_reason":"Bridge implemented with structured result/error classification and project-scoped execFile command execution; tests added.","labels":["bridge","execfile"],"dependencies":[{"issue_id":"bb-ymg.1","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:01.7343468-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.1","depends_on_id":"bb-6aj.2","type":"blocks","created_at":"2026-02-11T17:12:32.3039711-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.1.1","title":"Resolve bd.exe location from PATH and configuration fallback","description":"Executable resolution contract.\n\nResolution order:\n1) explicit configured executable path (request/config)\n2) PATH scan for bd.exe, bd.cmd, bd.bat, bd\n\nFailure behavior:\n- Throw actionable guidance when missing, including install command:\n npm install -g @beads/bd\n- Error message explicitly mentions explicit path when provided but invalid.\n\nVerification:\n- tests/lib/bd-path.test.ts validates config-first behavior, PATH lookup, and missing executable guidance.","acceptance_criteria":"Acceptance contract:\n- Resolver is deterministic for config and PATH inputs.\n- Missing executable guidance is actionable and user-readable.","notes":"Implemented src/lib/bd-path.ts executable resolution with config-first then PATH lookup (bd.exe/bd.cmd/bd.bat/bd), plus actionable setup guidance when missing. Added tests/lib/bd-path.test.ts for success/failure cases.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:02.5593205-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:14:34.4905469-08:00","closed_at":"2026-02-11T19:44:57.3720854-08:00","close_reason":"Executable resolution implemented with config/PATH fallback and actionable missing-bd guidance; tests added.","labels":["bridge","setup"],"dependencies":[{"issue_id":"bb-ymg.1.1","depends_on_id":"bb-ymg.1","type":"parent-child","created_at":"2026-02-11T17:12:02.5603636-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.2","title":"Implement mutation API for create/update/close/reopen/comment operations","description":"Mutation API contract and mapping.\n\nSupported operations:\n- create, update, close, reopen, comment\n\nValidation:\n- Payload shape validation enforces required fields and basic constraints.\n- update requires at least one mutable field.\n- status values are constrained to board-supported statuses.\n\nCommand mapping:\n- create -\u003e bd create \u003ctitle\u003e [flags] --json\n- update -\u003e bd update \u003cid\u003e [flags] --json\n- close -\u003e bd close \u003cid\u003e [-r reason] --json\n- reopen -\u003e bd reopen \u003cid\u003e [-r reason] --json\n- comment -\u003e bd comments add \u003cid\u003e \u003ctext\u003e --json\n\nResponse shape:\n- { ok, operation, command, error? }\n- command field always includes normalized bridge result.\n\nVerification:\n- tests/lib/mutations.test.ts and tests/api/mutations-routes.test.ts\n- Runtime smoke lifecycle executed across create/update/close/reopen/comment.","acceptance_criteria":"Acceptance contract:\n- Every mutation route maps to bd.exe only.\n- Invalid payloads return explicit bad_args responses.\n- Reopen and comment operations are first-class and tested.","notes":"Implemented mutation validation/mapping/execution layer in src/lib/mutations.ts and App Router endpoints: /api/beads/create|update|close|reopen|comment. Added payload validation tests, route validation tests, and smoke-tested create/update/comment/close/reopen lifecycle via API.","status":"closed","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:03.3757503-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:14:35.2552257-08:00","closed_at":"2026-02-11T19:45:26.3234246-08:00","close_reason":"Mutation API implemented for create/update/close/reopen/comment with payload validation, command mapping, normalized error shape, and verified smoke lifecycle via API.","labels":["api","mutation"],"dependencies":[{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:03.377343-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg.1","type":"blocks","created_at":"2026-02-11T17:12:32.810993-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg.1.1","type":"blocks","created_at":"2026-02-11T17:12:33.313807-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.3","title":"Add optimistic updates with rollback and SSE reconciliation","description":"Optimistic writeback behavior.\n\nFrontend mutation strategy:\n- Apply optimistic status update to local issue state.\n- Mark issue as pending during command execution.\n- On failure: rollback to previous issue snapshot and surface mutation error.\n- On success: fetch authoritative issue list from /api/beads/read and replace local state.\n\nRationale:\n- Preserves responsive UX without violating source-of-truth boundary.\n- Reconciliation avoids stale local drift when external agents mutate files.\n\nVerification:\n- tests/lib/writeback.test.ts for transition planning and optimistic updater helpers.\n- Runtime mutation smoke tests confirm end-to-end lifecycle.","acceptance_criteria":"Acceptance contract:\n- Failed mutation restores prior local state.\n- Successful mutation reconciles to authoritative read response.\n- Pending state prevents repeated conflicting transitions.","notes":"Implemented optimistic status updates with rollback in Kanban page, per-issue pending state, and authoritative reconciliation via new GET /api/beads/read endpoint after successful mutations.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:04.1956393-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:14:36.0400918-08:00","closed_at":"2026-02-11T19:59:02.289739-08:00","close_reason":"Optimistic board updates with rollback and authoritative post-mutation reconciliation via read route implemented and validated.","labels":["optimistic","state"],"dependencies":[{"issue_id":"bb-ymg.3","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:04.1966728-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.3","depends_on_id":"bb-ymg.2","type":"blocks","created_at":"2026-02-11T17:12:33.8246167-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.4","title":"Implement drag-and-drop status transitions mapped to bd commands","description":"Kanban drag-and-drop transition semantics.\n\nTransition planner rules:\n- Any -\u003e closed: use close command.\n- closed -\u003e open: use reopen command.\n- closed -\u003e in_progress|blocked|deferred: reopen then update target status.\n- open|in_progress|blocked|deferred between non-closed states: update status.\n\nUI behavior:\n- Drag start attaches issue id/status metadata.\n- Drop lane executes planned mutation steps in order.\n- Lane card pending state is shown while mutation is in flight.\n\nVerification:\n- tests/lib/writeback.test.ts transition planning cases.\n- Runtime smoke checks for close/reopen/update transition chain.","acceptance_criteria":"Acceptance contract:\n- DnD invokes valid bd command sequence for each source-\u003etarget status.\n- Invalid/no-op transitions do not emit unnecessary commands.\n- Pending safeguards prevent duplicate conflicting moves.","notes":"Implemented lane drag-and-drop interactions in Kanban board, status transition planning (including closed -\u003e reopen+update), and mapped transitions to bd mutation API routes with pending-state safeguards.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:05.0129676-08:00","created_by":"zenchantlive","updated_at":"2026-02-11T20:14:36.8114668-08:00","closed_at":"2026-02-11T19:59:21.7655834-08:00","close_reason":"Kanban lane drag-and-drop transitions now map to bd-backed close/reopen/update mutations with transition planner tests and runtime smoke validation.","labels":["dnd","kanban"],"dependencies":[{"issue_id":"bb-ymg.4","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:05.014527-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.4","depends_on_id":"bb-ymg.2","type":"blocks","created_at":"2026-02-11T17:12:34.329788-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.4","depends_on_id":"bb-trz.1","type":"blocks","created_at":"2026-02-11T17:12:34.8422542-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.2","title":"Implement mutation API for create/update/close/reopen/comment operations","description":"SUBTASK REPORT: Implementation of the Mutation API. We built a suite of App Router endpoints (/api/beads/create|update|close|reopen|comment) that serve as the bridge between the UI and the 'bd' CLI. Each endpoint enforces strict payload validation and maps incoming data to the appropriate CLI flags. The API returns a consistent response shape containing the normalized bridge output, allowing the frontend to handle success and failure deterministically.","acceptance_criteria":"Acceptance contract:\n- Every mutation route maps to bd.exe only.\n- Invalid payloads return explicit bad_args responses.\n- Reopen and comment operations are first-class and tested.","notes":"EXECUTION TALE: Implementation involved the creation of src/lib/mutations.ts to house the validation and command-mapping logic. We ensured that all status transitions are constrained to board-supported values and that 'update' calls require at least one mutable field. A major feature was the first-class support for 'comment' and 'reopen' operations, which were previously CLI-only. Verified via tests/api/mutations-routes.test.ts and runtime smoke tests across the full project lifecycle.","status":"closed","priority":0,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:03.3757503-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:14:29.6216695-08:00","closed_at":"2026-02-11T19:45:26.3234246-08:00","close_reason":"Mutation API implemented for create/update/close/reopen/comment with payload validation, command mapping, normalized error shape, and verified smoke lifecycle via API.","labels":["api","mutation"],"dependencies":[{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:03.377343-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg.1","type":"blocks","created_at":"2026-02-11T17:12:32.810993-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.2","depends_on_id":"bb-ymg.1.1","type":"blocks","created_at":"2026-02-11T17:12:33.313807-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.3","title":"Add optimistic updates with rollback and SSE reconciliation","description":"SUBTASK REPORT: Optimistic Updates \u0026 SSE Reconciliation. We implemented the frontend 'Heartbeat' logic that allows the UI to update instantly before the CLI write is fully flushed to disk. The system uses a 'temporary local state' that is automatically reconciled when the authoritative SSE 'issues' event arrives from the server. This eliminates the 'Wait-and-Refresh' UX pattern typical of file-backed systems.","acceptance_criteria":"Acceptance contract:\n- Failed mutation restores prior local state.\n- Successful mutation reconciles to authoritative read response.\n- Pending state prevents repeated conflicting transitions.","notes":"EXECUTION TALE: We refactored the shared useBeadsSubscription hook to accept an updateLocal callback, allowing individual components to perform optimistic state mutations. We added rollback logic to handle CLI failure cases, ensuring the UI reverts to the last known-good state if a write fails. This coordination between the mutation API and the SSE transport layer was verified by monitoring network tabs for 'mutate -\u003e event -\u003e reconcile' sequences.","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:04.1956393-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:14:30.438772-08:00","closed_at":"2026-02-11T19:59:02.289739-08:00","close_reason":"Optimistic board updates with rollback and authoritative post-mutation reconciliation via read route implemented and validated.","labels":["optimistic","state"],"dependencies":[{"issue_id":"bb-ymg.3","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:04.1966728-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.3","depends_on_id":"bb-ymg.2","type":"blocks","created_at":"2026-02-11T17:12:33.8246167-08:00","created_by":"zenchantlive"}]}
|
||||
{"id":"bb-ymg.4","title":"Implement drag-and-drop status transitions mapped to bd commands","description":"SUBTASK REPORT: Drag-and-Drop Status Transitions. We integrated the mutation API with the Kanban board's interaction model. Moving a card between columns now triggers an automated 'bd update [id] --status [new_status]' command. The UI provides visual feedback during the flight and handles cross-column blockers by validating the move against the server-side mutation rules.","acceptance_criteria":"Acceptance contract:\n- DnD invokes valid bd command sequence for each source-\u003etarget status.\n- Invalid/no-op transitions do not emit unnecessary commands.\n- Pending safeguards prevent duplicate conflicting moves.","notes":"EXECUTION TALE: We mapped React DnB events to specific CLI transitions. Implementation involved updating the Kanban card components to handle 'isDragging' and 'isUpdating' states. We ensured that dragging a blocked task shows a clear 'Deadlock' warning by checking the server-side error classification. Verified with cross-column move tests across all 5 standard Beads statuses (backlog, open, in_progress, blocked, closed).","status":"closed","priority":1,"issue_type":"task","owner":"jordanlive121@gmail.com","created_at":"2026-02-11T17:12:05.0129676-08:00","created_by":"zenchantlive","updated_at":"2026-02-14T00:14:31.2987104-08:00","closed_at":"2026-02-11T19:59:21.7655834-08:00","close_reason":"Kanban lane drag-and-drop transitions now map to bd-backed close/reopen/update mutations with transition planner tests and runtime smoke validation.","labels":["dnd","kanban"],"dependencies":[{"issue_id":"bb-ymg.4","depends_on_id":"bb-ymg","type":"parent-child","created_at":"2026-02-11T17:12:05.014527-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.4","depends_on_id":"bb-ymg.2","type":"blocks","created_at":"2026-02-11T17:12:34.329788-08:00","created_by":"zenchantlive"},{"issue_id":"bb-ymg.4","depends_on_id":"bb-trz.1","type":"blocks","created_at":"2026-02-11T17:12:34.8422542-08:00","created_by":"zenchantlive"}]}
|
||||
|
|
|
|||
37
src/lib/activity-persistence.ts
Normal file
37
src/lib/activity-persistence.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import type { ActivityEvent } from './activity';
|
||||
|
||||
function userProfileRoot(): string {
|
||||
return process.env.USERPROFILE?.trim() || os.homedir();
|
||||
}
|
||||
|
||||
export function activityFilePath(): string {
|
||||
return path.join(userProfileRoot(), '.beadboard', 'activity.json');
|
||||
}
|
||||
|
||||
export async function loadActivityHistory(): Promise<ActivityEvent[]> {
|
||||
const filePath = activityFilePath();
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
console.error('[ActivityPersistence] Failed to load history:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveActivityHistory(history: ActivityEvent[]): Promise<void> {
|
||||
const filePath = activityFilePath();
|
||||
try {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, JSON.stringify(history, null, 2), 'utf8');
|
||||
} catch (error) {
|
||||
console.error('[ActivityPersistence] Failed to save history:', error);
|
||||
}
|
||||
}
|
||||
75
src/lib/activity.ts
Normal file
75
src/lib/activity.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import type { BeadIssueWithProject } from './types';
|
||||
|
||||
/**
|
||||
* 16 transition types for timeline activity events,
|
||||
* as required by the bb-xhm.1 event model specification.
|
||||
*/
|
||||
export type ActivityEventKind =
|
||||
| 'created'
|
||||
| 'closed'
|
||||
| 'reopened'
|
||||
| 'status_changed'
|
||||
| 'priority_changed'
|
||||
| 'assignee_changed'
|
||||
| 'type_changed'
|
||||
| 'title_changed'
|
||||
| 'description_changed'
|
||||
| 'labels_changed'
|
||||
| 'dependency_added'
|
||||
| 'dependency_removed'
|
||||
| 'comment_added'
|
||||
| 'due_date_changed'
|
||||
| 'estimate_changed'
|
||||
| 'field_changed';
|
||||
|
||||
/**
|
||||
* Represents a discrete change or action derived from bead snapshots or interactions.
|
||||
*/
|
||||
export interface ActivityEvent {
|
||||
/** Unique identity for the event instance (likely UUID) */
|
||||
id: string;
|
||||
|
||||
/** The type of transition that occurred */
|
||||
kind: ActivityEventKind;
|
||||
|
||||
/** The issue this event belongs to */
|
||||
beadId: string;
|
||||
|
||||
/** Display title of the issue at the time of the event */
|
||||
beadTitle: string;
|
||||
|
||||
/** The project key this issue belongs to */
|
||||
projectId: string;
|
||||
|
||||
/** Human-readable project name */
|
||||
projectName: string;
|
||||
|
||||
/** ISO8601 timestamp of when the event was detected or recorded */
|
||||
timestamp: string;
|
||||
|
||||
/** The actor who performed the action (assignee, owner, or session ID) */
|
||||
actor: string | null;
|
||||
|
||||
/** Data payload describing the change */
|
||||
payload: {
|
||||
/** The specific field name that changed (for property updates) */
|
||||
field?: string;
|
||||
|
||||
/** The previous value before the transition */
|
||||
from?: any;
|
||||
|
||||
/** The new value after the transition */
|
||||
to?: any;
|
||||
|
||||
/** Optional context message (e.g. comment text or close reason) */
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A pair of snapshots used by the diffing engine to derive ActivityEvents.
|
||||
*/
|
||||
export interface SnapshotDiff {
|
||||
previous: BeadIssueWithProject | null;
|
||||
current: BeadIssueWithProject;
|
||||
}
|
||||
400
src/lib/agent-mail.ts
Normal file
400
src/lib/agent-mail.ts
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { listAgents, showAgent, type AgentRecord } from './agent-registry';
|
||||
|
||||
const MESSAGE_CATEGORIES = ['HANDOFF', 'BLOCKED', 'DECISION', 'INFO'] as const;
|
||||
const MESSAGE_STATES = ['unread', 'read', 'acked'] as const;
|
||||
|
||||
export type MessageCategory = (typeof MESSAGE_CATEGORIES)[number];
|
||||
export type MessageState = (typeof MESSAGE_STATES)[number];
|
||||
export type MailCommandName = 'agent send' | 'agent inbox' | 'agent read' | 'agent ack';
|
||||
|
||||
export interface MailCommandError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface MailCommandResponse<T> {
|
||||
ok: boolean;
|
||||
command: MailCommandName;
|
||||
data: T | null;
|
||||
error: MailCommandError | null;
|
||||
}
|
||||
|
||||
export interface AgentMessage {
|
||||
message_id: string;
|
||||
thread_id: string;
|
||||
bead_id: string;
|
||||
from_agent: string;
|
||||
to_agent: string;
|
||||
category: MessageCategory;
|
||||
subject: string;
|
||||
body: string;
|
||||
state: MessageState;
|
||||
requires_ack: boolean;
|
||||
created_at: string;
|
||||
read_at: string | null;
|
||||
acked_at: string | null;
|
||||
}
|
||||
|
||||
export interface SendAgentMessageInput {
|
||||
from: string;
|
||||
to: string;
|
||||
bead: string;
|
||||
category: MessageCategory;
|
||||
subject: string;
|
||||
body: string;
|
||||
thread?: string;
|
||||
}
|
||||
|
||||
export interface SendAgentMessageDeps {
|
||||
now: () => string;
|
||||
idGenerator: () => string;
|
||||
}
|
||||
|
||||
export interface InboxAgentMessagesInput {
|
||||
agent: string;
|
||||
state?: MessageState;
|
||||
bead?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface MessageActionInput {
|
||||
agent: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface MessageMutationDeps {
|
||||
now: () => string;
|
||||
}
|
||||
|
||||
function userProfileRoot(): string {
|
||||
return process.env.USERPROFILE?.trim() || os.homedir();
|
||||
}
|
||||
|
||||
function agentRoot(): string {
|
||||
return path.join(userProfileRoot(), '.beadboard', 'agent');
|
||||
}
|
||||
|
||||
function messagesRoot(): string {
|
||||
return path.join(agentRoot(), 'messages');
|
||||
}
|
||||
|
||||
function inboxFilePath(agentId: string): string {
|
||||
return path.join(messagesRoot(), `${agentId}.jsonl`);
|
||||
}
|
||||
|
||||
function indexDirectoryPath(): string {
|
||||
return path.join(messagesRoot(), 'index');
|
||||
}
|
||||
|
||||
function indexFilePath(messageId: string): string {
|
||||
return path.join(indexDirectoryPath(), `${messageId}.json`);
|
||||
}
|
||||
|
||||
function trimOrEmpty(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function success<T>(command: MailCommandName, data: T): MailCommandResponse<T> {
|
||||
return {
|
||||
ok: true,
|
||||
command,
|
||||
data,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
function invalid(command: MailCommandName, code: string, message: string): MailCommandResponse<never> {
|
||||
return {
|
||||
ok: false,
|
||||
command,
|
||||
data: null,
|
||||
error: { code, message },
|
||||
};
|
||||
}
|
||||
|
||||
function isMessageCategory(value: string): value is MessageCategory {
|
||||
return MESSAGE_CATEGORIES.includes(value as MessageCategory);
|
||||
}
|
||||
|
||||
function isMessageState(value: string): value is MessageState {
|
||||
return MESSAGE_STATES.includes(value as MessageState);
|
||||
}
|
||||
|
||||
function requiresAck(category: MessageCategory): boolean {
|
||||
return category === 'HANDOFF' || category === 'BLOCKED';
|
||||
}
|
||||
|
||||
function defaultMessageId(nowIso: string): string {
|
||||
const seed = Math.random().toString(16).slice(2, 6);
|
||||
const compact = nowIso.replace(/[-:]/g, '').replace('.000Z', '').replace('T', '_');
|
||||
return `msg_${compact}_${seed}`;
|
||||
}
|
||||
|
||||
async function appendInboxMessage(agentId: string, message: AgentMessage): Promise<void> {
|
||||
const filePath = inboxFilePath(agentId);
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.appendFile(filePath, `${JSON.stringify(message)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
async function writeMessageIndex(message: AgentMessage): Promise<void> {
|
||||
const filePath = indexFilePath(message.message_id);
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, `${JSON.stringify(message, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
async function readMessageIndex(messageId: string): Promise<AgentMessage | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(indexFilePath(messageId), 'utf8');
|
||||
return JSON.parse(raw) as AgentMessage;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInboxMessages(agentId: string): Promise<AgentMessage[]> {
|
||||
try {
|
||||
const raw = await fs.readFile(inboxFilePath(agentId), 'utf8');
|
||||
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||
|
||||
const messages: AgentMessage[] = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const parsed = JSON.parse(line) as AgentMessage;
|
||||
const current = await readMessageIndex(parsed.message_id);
|
||||
messages.push(current ?? parsed);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return messages.sort((left, right) => right.created_at.localeCompare(left.created_at));
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveRegisteredAgent(agentId: string): Promise<AgentRecord | null> {
|
||||
const result = await showAgent({ agent: agentId });
|
||||
return result.ok ? result.data : null;
|
||||
}
|
||||
|
||||
export async function sendAgentMessage(
|
||||
input: SendAgentMessageInput,
|
||||
deps: Partial<SendAgentMessageDeps> = {},
|
||||
): Promise<MailCommandResponse<AgentMessage>> {
|
||||
const command: MailCommandName = 'agent send';
|
||||
|
||||
const from = trimOrEmpty(input.from);
|
||||
const to = trimOrEmpty(input.to);
|
||||
const beadId = trimOrEmpty(input.bead);
|
||||
const categoryRaw = trimOrEmpty(input.category);
|
||||
const subject = trimOrEmpty(input.subject);
|
||||
const body = trimOrEmpty(input.body);
|
||||
const threadId = trimOrEmpty(input.thread) || `bead:${beadId}`;
|
||||
|
||||
if (!from || !(await resolveRegisteredAgent(from))) {
|
||||
return invalid(command, 'UNKNOWN_SENDER', 'Sender agent is not registered.');
|
||||
}
|
||||
|
||||
if (!to) {
|
||||
return invalid(command, 'UNKNOWN_RECIPIENT', 'Recipient agent is required.');
|
||||
}
|
||||
|
||||
if (to !== 'broadcast' && !(await resolveRegisteredAgent(to))) {
|
||||
return invalid(command, 'UNKNOWN_RECIPIENT', 'Recipient agent is not registered.');
|
||||
}
|
||||
|
||||
if (!beadId) {
|
||||
return invalid(command, 'MISSING_BEAD_ID', 'Bead id is required.');
|
||||
}
|
||||
|
||||
if (!isMessageCategory(categoryRaw)) {
|
||||
return invalid(command, 'INVALID_CATEGORY', 'Category must be one of HANDOFF, BLOCKED, DECISION, INFO.');
|
||||
}
|
||||
|
||||
if (!subject || !body) {
|
||||
return invalid(command, 'INVALID_MESSAGE', 'Subject and body are required.');
|
||||
}
|
||||
|
||||
try {
|
||||
const now = deps.now ? deps.now() : new Date().toISOString();
|
||||
const generateId = deps.idGenerator ?? (() => defaultMessageId(now));
|
||||
const recipientIds =
|
||||
to === 'broadcast'
|
||||
? ((await listAgents({})).data ?? []).map((agent) => agent.agent_id).filter((agentId) => agentId !== from)
|
||||
: [to];
|
||||
|
||||
if (recipientIds.length === 0) {
|
||||
return invalid(command, 'UNKNOWN_RECIPIENT', 'No recipients available for broadcast.');
|
||||
}
|
||||
|
||||
let firstMessage: AgentMessage | null = null;
|
||||
|
||||
for (const recipientId of recipientIds) {
|
||||
const messageId = recipientIds.length === 1 ? generateId() : `${generateId()}_${recipientId}`;
|
||||
const message: AgentMessage = {
|
||||
message_id: messageId,
|
||||
thread_id: threadId,
|
||||
bead_id: beadId,
|
||||
from_agent: from,
|
||||
to_agent: recipientId,
|
||||
category: categoryRaw,
|
||||
subject,
|
||||
body,
|
||||
state: 'unread',
|
||||
requires_ack: requiresAck(categoryRaw),
|
||||
created_at: now,
|
||||
read_at: null,
|
||||
acked_at: null,
|
||||
};
|
||||
|
||||
await appendInboxMessage(recipientId, message);
|
||||
await writeMessageIndex(message);
|
||||
if (!firstMessage) {
|
||||
firstMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
return success(command, firstMessage as AgentMessage);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to send message.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function inboxAgentMessages(
|
||||
input: InboxAgentMessagesInput,
|
||||
): Promise<MailCommandResponse<AgentMessage[]>> {
|
||||
const command: MailCommandName = 'agent inbox';
|
||||
|
||||
const agentId = trimOrEmpty(input.agent);
|
||||
const state = trimOrEmpty(input.state);
|
||||
const beadId = trimOrEmpty(input.bead);
|
||||
const limit = input.limit === undefined ? 50 : input.limit;
|
||||
|
||||
if (!agentId || !(await resolveRegisteredAgent(agentId))) {
|
||||
return invalid(command, 'AGENT_NOT_FOUND', 'Agent is not registered.');
|
||||
}
|
||||
|
||||
if (state && !isMessageState(state)) {
|
||||
return invalid(command, 'INVALID_STATE', 'State must be one of unread, read, acked.');
|
||||
}
|
||||
|
||||
if (!Number.isFinite(limit) || limit <= 0 || limit > 500) {
|
||||
return invalid(command, 'INVALID_LIMIT', 'Limit must be between 1 and 500.');
|
||||
}
|
||||
|
||||
try {
|
||||
const messages = await loadInboxMessages(agentId);
|
||||
const filtered = messages
|
||||
.filter((message) => {
|
||||
if (state && message.state !== state) {
|
||||
return false;
|
||||
}
|
||||
if (beadId && message.bead_id !== beadId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.slice(0, limit);
|
||||
|
||||
return success(command, filtered);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to load inbox.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readAgentMessage(
|
||||
input: MessageActionInput,
|
||||
deps: Partial<MessageMutationDeps> = {},
|
||||
): Promise<MailCommandResponse<AgentMessage>> {
|
||||
const command: MailCommandName = 'agent read';
|
||||
|
||||
const agentId = trimOrEmpty(input.agent);
|
||||
const messageId = trimOrEmpty(input.message);
|
||||
|
||||
if (!agentId || !(await resolveRegisteredAgent(agentId))) {
|
||||
return invalid(command, 'AGENT_NOT_FOUND', 'Agent is not registered.');
|
||||
}
|
||||
|
||||
if (!messageId) {
|
||||
return invalid(command, 'MESSAGE_NOT_FOUND', 'Message id is required.');
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await readMessageIndex(messageId);
|
||||
if (!existing) {
|
||||
return invalid(command, 'MESSAGE_NOT_FOUND', 'Message does not exist.');
|
||||
}
|
||||
|
||||
if (existing.to_agent !== agentId) {
|
||||
return invalid(command, 'READ_FORBIDDEN', 'Only the recipient may read this message.');
|
||||
}
|
||||
|
||||
if (existing.state === 'unread') {
|
||||
const now = deps.now ? deps.now() : new Date().toISOString();
|
||||
const updated: AgentMessage = {
|
||||
...existing,
|
||||
state: 'read',
|
||||
read_at: existing.read_at ?? now,
|
||||
};
|
||||
await writeMessageIndex(updated);
|
||||
return success(command, updated);
|
||||
}
|
||||
|
||||
return success(command, existing);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to read message.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function ackAgentMessage(
|
||||
input: MessageActionInput,
|
||||
deps: Partial<MessageMutationDeps> = {},
|
||||
): Promise<MailCommandResponse<AgentMessage>> {
|
||||
const command: MailCommandName = 'agent ack';
|
||||
|
||||
const agentId = trimOrEmpty(input.agent);
|
||||
const messageId = trimOrEmpty(input.message);
|
||||
|
||||
if (!agentId || !(await resolveRegisteredAgent(agentId))) {
|
||||
return invalid(command, 'AGENT_NOT_FOUND', 'Agent is not registered.');
|
||||
}
|
||||
|
||||
if (!messageId) {
|
||||
return invalid(command, 'MESSAGE_NOT_FOUND', 'Message id is required.');
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await readMessageIndex(messageId);
|
||||
if (!existing) {
|
||||
return invalid(command, 'MESSAGE_NOT_FOUND', 'Message does not exist.');
|
||||
}
|
||||
|
||||
if (existing.to_agent !== agentId) {
|
||||
return invalid(command, 'ACK_FORBIDDEN', 'Only the recipient may acknowledge this message.');
|
||||
}
|
||||
|
||||
const now = deps.now ? deps.now() : new Date().toISOString();
|
||||
const updated: AgentMessage = {
|
||||
...existing,
|
||||
state: 'acked',
|
||||
read_at: existing.read_at ?? now,
|
||||
acked_at: existing.acked_at ?? now,
|
||||
};
|
||||
|
||||
await writeMessageIndex(updated);
|
||||
return success(command, updated);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to acknowledge message.');
|
||||
}
|
||||
}
|
||||
255
src/lib/agent-registry.ts
Normal file
255
src/lib/agent-registry.ts
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const AGENT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
|
||||
export type AgentCommandName = 'agent register' | 'agent list' | 'agent show';
|
||||
|
||||
export interface AgentCommandError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AgentCommandResponse<T> {
|
||||
ok: boolean;
|
||||
command: AgentCommandName;
|
||||
data: T | null;
|
||||
error: AgentCommandError | null;
|
||||
}
|
||||
|
||||
export interface AgentRecord {
|
||||
agent_id: string;
|
||||
display_name: string;
|
||||
role: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
last_seen_at: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface RegisterAgentInput {
|
||||
name: string;
|
||||
display?: string;
|
||||
role: string;
|
||||
forceUpdate?: boolean;
|
||||
}
|
||||
|
||||
export interface RegisterAgentDeps {
|
||||
now: () => string;
|
||||
}
|
||||
|
||||
export interface ListAgentsInput {
|
||||
role?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface ShowAgentInput {
|
||||
agent: string;
|
||||
}
|
||||
|
||||
function userProfileRoot(): string {
|
||||
return process.env.USERPROFILE?.trim() || os.homedir();
|
||||
}
|
||||
|
||||
export function agentRegistryRoot(): string {
|
||||
return path.join(userProfileRoot(), '.beadboard', 'agent');
|
||||
}
|
||||
|
||||
export function agentsDirectoryPath(): string {
|
||||
return path.join(agentRegistryRoot(), 'agents');
|
||||
}
|
||||
|
||||
export function agentFilePath(agentId: string): string {
|
||||
return path.join(agentsDirectoryPath(), `${agentId}.json`);
|
||||
}
|
||||
|
||||
function trimOrEmpty(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function invalid(command: AgentCommandName, code: string, message: string): AgentCommandResponse<never> {
|
||||
return {
|
||||
ok: false,
|
||||
command,
|
||||
data: null,
|
||||
error: { code, message },
|
||||
};
|
||||
}
|
||||
|
||||
function success<T>(command: AgentCommandName, data: T): AgentCommandResponse<T> {
|
||||
return {
|
||||
ok: true,
|
||||
command,
|
||||
data,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
function validateAgentId(value: string): AgentCommandError | null {
|
||||
if (!AGENT_ID_PATTERN.test(value) || value.length < 3 || value.length > 48) {
|
||||
return {
|
||||
code: 'INVALID_AGENT_ID',
|
||||
message: 'Agent id must match ^[a-z0-9]+(?:-[a-z0-9]+)*$ and be 3..48 characters.',
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateRole(value: string): AgentCommandError | null {
|
||||
if (!value) {
|
||||
return {
|
||||
code: 'INVALID_ROLE',
|
||||
message: 'Role is required.',
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function readAgent(agentId: string): Promise<AgentRecord | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(agentFilePath(agentId), 'utf8');
|
||||
const parsed = JSON.parse(raw) as AgentRecord;
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeAgent(record: AgentRecord): Promise<void> {
|
||||
const filePath = agentFilePath(record.agent_id);
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
async function loadAllAgents(): Promise<AgentRecord[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(agentsDirectoryPath(), { withFileTypes: true });
|
||||
const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.json'));
|
||||
|
||||
const agents: AgentRecord[] = [];
|
||||
for (const file of files) {
|
||||
const filePath = path.join(agentsDirectoryPath(), file.name);
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
agents.push(JSON.parse(raw) as AgentRecord);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return agents.sort((left, right) => left.agent_id.localeCompare(right.agent_id));
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerAgent(
|
||||
input: RegisterAgentInput,
|
||||
deps: Partial<RegisterAgentDeps> = {},
|
||||
): Promise<AgentCommandResponse<AgentRecord>> {
|
||||
const command: AgentCommandName = 'agent register';
|
||||
const agentId = trimOrEmpty(input.name);
|
||||
const role = trimOrEmpty(input.role);
|
||||
const display = trimOrEmpty(input.display) || agentId;
|
||||
|
||||
const agentIdError = validateAgentId(agentId);
|
||||
if (agentIdError) {
|
||||
return invalid(command, agentIdError.code, agentIdError.message);
|
||||
}
|
||||
|
||||
const roleError = validateRole(role);
|
||||
if (roleError) {
|
||||
return invalid(command, roleError.code, roleError.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await readAgent(agentId);
|
||||
const now = deps.now ? deps.now() : new Date().toISOString();
|
||||
|
||||
if (existing && !input.forceUpdate) {
|
||||
return invalid(command, 'DUPLICATE_AGENT_ID', 'Agent is already registered. Use --force-update to change display/role.');
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
const updated: AgentRecord = {
|
||||
...existing,
|
||||
display_name: display || existing.display_name,
|
||||
role,
|
||||
last_seen_at: now,
|
||||
version: existing.version + 1,
|
||||
};
|
||||
await writeAgent(updated);
|
||||
return success(command, updated);
|
||||
}
|
||||
|
||||
const created: AgentRecord = {
|
||||
agent_id: agentId,
|
||||
display_name: display,
|
||||
role,
|
||||
status: 'idle',
|
||||
created_at: now,
|
||||
last_seen_at: now,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
await writeAgent(created);
|
||||
return success(command, created);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to register agent.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAgents(input: ListAgentsInput): Promise<AgentCommandResponse<AgentRecord[]>> {
|
||||
const command: AgentCommandName = 'agent list';
|
||||
const role = trimOrEmpty(input.role);
|
||||
const status = trimOrEmpty(input.status);
|
||||
|
||||
try {
|
||||
const agents = await loadAllAgents();
|
||||
const filtered = agents.filter((agent) => {
|
||||
if (role && agent.role !== role) {
|
||||
return false;
|
||||
}
|
||||
if (status && agent.status !== status) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return success(command, filtered);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to list agents.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function showAgent(input: ShowAgentInput): Promise<AgentCommandResponse<AgentRecord>> {
|
||||
const command: AgentCommandName = 'agent show';
|
||||
const agentId = trimOrEmpty(input.agent);
|
||||
|
||||
const agentIdError = validateAgentId(agentId);
|
||||
if (agentIdError) {
|
||||
return invalid(command, agentIdError.code, agentIdError.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const agent = await readAgent(agentId);
|
||||
if (!agent) {
|
||||
return invalid(command, 'AGENT_NOT_FOUND', 'Agent is not registered.');
|
||||
}
|
||||
|
||||
return success(command, agent);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to load agent.');
|
||||
}
|
||||
}
|
||||
427
src/lib/agent-reservations.ts
Normal file
427
src/lib/agent-reservations.ts
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { showAgent } from './agent-registry';
|
||||
import type { AgentMessage } from './agent-mail';
|
||||
|
||||
const MIN_TTL_MINUTES = 5;
|
||||
const MAX_TTL_MINUTES = 1440;
|
||||
const DEFAULT_TTL_MINUTES = 120;
|
||||
|
||||
export type ReservationCommandName = 'agent reserve' | 'agent release' | 'agent status';
|
||||
export type ReservationState = 'active' | 'released' | 'expired';
|
||||
|
||||
export interface ReservationCommandError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ReservationCommandResponse<T> {
|
||||
ok: boolean;
|
||||
command: ReservationCommandName;
|
||||
data: T | null;
|
||||
error: ReservationCommandError | null;
|
||||
}
|
||||
|
||||
export interface AgentReservation {
|
||||
reservation_id: string;
|
||||
scope: string;
|
||||
agent_id: string;
|
||||
bead_id: string;
|
||||
state: ReservationState;
|
||||
created_at: string;
|
||||
expires_at: string;
|
||||
released_at: string | null;
|
||||
}
|
||||
|
||||
export interface ReserveAgentScopeInput {
|
||||
agent: string;
|
||||
scope: string;
|
||||
bead: string;
|
||||
ttl?: number;
|
||||
takeoverStale?: boolean;
|
||||
}
|
||||
|
||||
export interface ReserveAgentScopeDeps {
|
||||
now: () => string;
|
||||
idGenerator: () => string;
|
||||
}
|
||||
|
||||
export interface ReleaseAgentReservationInput {
|
||||
agent: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface StatusAgentReservationsInput {
|
||||
bead?: string;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
export interface StatusAgentReservationsData {
|
||||
reservations: AgentReservation[];
|
||||
unacked_required_messages: AgentMessage[];
|
||||
summary: {
|
||||
active: number;
|
||||
released: number;
|
||||
expired: number;
|
||||
unacked_required_messages: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface MutationDeps {
|
||||
now: () => string;
|
||||
}
|
||||
|
||||
interface ActiveReservationsFile {
|
||||
reservations: AgentReservation[];
|
||||
}
|
||||
|
||||
function userProfileRoot(): string {
|
||||
return process.env.USERPROFILE?.trim() || os.homedir();
|
||||
}
|
||||
|
||||
function agentRoot(): string {
|
||||
return path.join(userProfileRoot(), '.beadboard', 'agent');
|
||||
}
|
||||
|
||||
function reservationsRoot(): string {
|
||||
return path.join(agentRoot(), 'reservations');
|
||||
}
|
||||
|
||||
function activeReservationsPath(): string {
|
||||
return path.join(reservationsRoot(), 'active.json');
|
||||
}
|
||||
|
||||
function reservationHistoryPath(): string {
|
||||
return path.join(reservationsRoot(), 'history.jsonl');
|
||||
}
|
||||
|
||||
function messageIndexDirectoryPath(): string {
|
||||
return path.join(agentRoot(), 'messages', 'index');
|
||||
}
|
||||
|
||||
function trimOrEmpty(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function success<T>(command: ReservationCommandName, data: T): ReservationCommandResponse<T> {
|
||||
return {
|
||||
ok: true,
|
||||
command,
|
||||
data,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
function invalid(command: ReservationCommandName, code: string, message: string): ReservationCommandResponse<never> {
|
||||
return {
|
||||
ok: false,
|
||||
command,
|
||||
data: null,
|
||||
error: { code, message },
|
||||
};
|
||||
}
|
||||
|
||||
function defaultReservationId(nowIso: string): string {
|
||||
const seed = Math.random().toString(16).slice(2, 6);
|
||||
const compact = nowIso.replace(/[-:]/g, '').replace('.000Z', '').replace('T', '_');
|
||||
return `res_${compact}_${seed}`;
|
||||
}
|
||||
|
||||
function addMinutes(iso: string, minutes: number): string {
|
||||
const base = Date.parse(iso);
|
||||
const next = new Date(base + minutes * 60_000);
|
||||
return next.toISOString();
|
||||
}
|
||||
|
||||
function isExpired(reservation: AgentReservation, nowIso: string): boolean {
|
||||
return reservation.expires_at.localeCompare(nowIso) <= 0;
|
||||
}
|
||||
|
||||
function toActiveFile(reservations: AgentReservation[]): ActiveReservationsFile {
|
||||
return { reservations };
|
||||
}
|
||||
|
||||
function parseActiveFile(raw: string): AgentReservation[] {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed as AgentReservation[];
|
||||
}
|
||||
|
||||
if (parsed && typeof parsed === 'object' && Array.isArray((parsed as ActiveReservationsFile).reservations)) {
|
||||
return (parsed as ActiveReservationsFile).reservations;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function readActiveReservations(): Promise<AgentReservation[]> {
|
||||
try {
|
||||
const raw = await fs.readFile(activeReservationsPath(), 'utf8');
|
||||
return parseActiveFile(raw);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function atomicWriteJson(filePath: string, payload: string): Promise<void> {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
const tempFile = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
||||
await fs.writeFile(tempFile, payload, 'utf8');
|
||||
await fs.rename(tempFile, filePath);
|
||||
}
|
||||
|
||||
async function writeActiveReservations(reservations: AgentReservation[]): Promise<void> {
|
||||
const snapshot = `${JSON.stringify(toActiveFile(reservations), null, 2)}\n`;
|
||||
await atomicWriteJson(activeReservationsPath(), snapshot);
|
||||
}
|
||||
|
||||
async function appendReservationHistory(reservation: AgentReservation): Promise<void> {
|
||||
const historyPath = reservationHistoryPath();
|
||||
await fs.mkdir(path.dirname(historyPath), { recursive: true });
|
||||
await fs.appendFile(historyPath, `${JSON.stringify(reservation)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
async function readRequiredAckMessages(): Promise<AgentMessage[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(messageIndexDirectoryPath(), { withFileTypes: true });
|
||||
const messages: AgentMessage[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.json')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(messageIndexDirectoryPath(), entry.name);
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw) as AgentMessage;
|
||||
if (parsed.requires_ack && !parsed.acked_at) {
|
||||
messages.push(parsed);
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return messages.sort((left, right) => right.created_at.localeCompare(left.created_at));
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveRegisteredAgent(agentId: string): Promise<boolean> {
|
||||
const result = await showAgent({ agent: agentId });
|
||||
return result.ok;
|
||||
}
|
||||
|
||||
async function sweepExpiredReservations(nowIso: string): Promise<{ active: AgentReservation[]; expired: number }> {
|
||||
const reservations = await readActiveReservations();
|
||||
const active: AgentReservation[] = [];
|
||||
const expired: AgentReservation[] = [];
|
||||
|
||||
for (const reservation of reservations) {
|
||||
if (isExpired(reservation, nowIso)) {
|
||||
expired.push({ ...reservation, state: 'expired' });
|
||||
} else {
|
||||
active.push(reservation);
|
||||
}
|
||||
}
|
||||
|
||||
if (expired.length > 0) {
|
||||
await writeActiveReservations(active);
|
||||
for (const reservation of expired) {
|
||||
await appendReservationHistory(reservation);
|
||||
}
|
||||
}
|
||||
|
||||
return { active, expired: expired.length };
|
||||
}
|
||||
|
||||
export async function reserveAgentScope(
|
||||
input: ReserveAgentScopeInput,
|
||||
deps: Partial<ReserveAgentScopeDeps> = {},
|
||||
): Promise<ReservationCommandResponse<AgentReservation>> {
|
||||
const command: ReservationCommandName = 'agent reserve';
|
||||
|
||||
const agentId = trimOrEmpty(input.agent);
|
||||
const scope = trimOrEmpty(input.scope);
|
||||
const beadId = trimOrEmpty(input.bead);
|
||||
const ttlMinutes = input.ttl ?? DEFAULT_TTL_MINUTES;
|
||||
|
||||
if (!agentId || !(await resolveRegisteredAgent(agentId))) {
|
||||
return invalid(command, 'AGENT_NOT_FOUND', 'Agent is not registered.');
|
||||
}
|
||||
|
||||
if (!scope || !beadId) {
|
||||
return invalid(command, 'INVALID_ARGS', 'Scope and bead id are required.');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(ttlMinutes) || ttlMinutes < MIN_TTL_MINUTES || ttlMinutes > MAX_TTL_MINUTES) {
|
||||
return invalid(command, 'INVALID_ARGS', `TTL must be an integer between ${MIN_TTL_MINUTES} and ${MAX_TTL_MINUTES} minutes.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const now = deps.now ? deps.now() : new Date().toISOString();
|
||||
const reservations = await readActiveReservations();
|
||||
const existing = reservations.find((reservation) => reservation.scope === scope);
|
||||
|
||||
if (existing) {
|
||||
if (!isExpired(existing, now)) {
|
||||
return invalid(command, 'RESERVATION_CONFLICT', `Scope is already reserved by ${existing.agent_id}.`);
|
||||
}
|
||||
|
||||
if (!input.takeoverStale) {
|
||||
return invalid(command, 'RESERVATION_STALE_FOUND', 'An expired reservation exists. Re-run with --takeover-stale.');
|
||||
}
|
||||
|
||||
const withoutExisting = reservations.filter((reservation) => reservation.reservation_id !== existing.reservation_id);
|
||||
await writeActiveReservations(withoutExisting);
|
||||
await appendReservationHistory({ ...existing, state: 'expired' });
|
||||
|
||||
const generateId = deps.idGenerator ?? (() => defaultReservationId(now));
|
||||
const created: AgentReservation = {
|
||||
reservation_id: generateId(),
|
||||
scope,
|
||||
agent_id: agentId,
|
||||
bead_id: beadId,
|
||||
state: 'active',
|
||||
created_at: now,
|
||||
expires_at: addMinutes(now, ttlMinutes),
|
||||
released_at: null,
|
||||
};
|
||||
|
||||
await writeActiveReservations([...withoutExisting, created]);
|
||||
return success(command, created);
|
||||
}
|
||||
|
||||
const generateId = deps.idGenerator ?? (() => defaultReservationId(now));
|
||||
const created: AgentReservation = {
|
||||
reservation_id: generateId(),
|
||||
scope,
|
||||
agent_id: agentId,
|
||||
bead_id: beadId,
|
||||
state: 'active',
|
||||
created_at: now,
|
||||
expires_at: addMinutes(now, ttlMinutes),
|
||||
released_at: null,
|
||||
};
|
||||
|
||||
await writeActiveReservations([...reservations, created]);
|
||||
return success(command, created);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to reserve scope.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function releaseAgentReservation(
|
||||
input: ReleaseAgentReservationInput,
|
||||
deps: Partial<MutationDeps> = {},
|
||||
): Promise<ReservationCommandResponse<AgentReservation>> {
|
||||
const command: ReservationCommandName = 'agent release';
|
||||
|
||||
const agentId = trimOrEmpty(input.agent);
|
||||
const scope = trimOrEmpty(input.scope);
|
||||
|
||||
if (!agentId || !(await resolveRegisteredAgent(agentId))) {
|
||||
return invalid(command, 'AGENT_NOT_FOUND', 'Agent is not registered.');
|
||||
}
|
||||
|
||||
if (!scope) {
|
||||
return invalid(command, 'INVALID_ARGS', 'Scope is required.');
|
||||
}
|
||||
|
||||
try {
|
||||
const now = deps.now ? deps.now() : new Date().toISOString();
|
||||
const reservations = await readActiveReservations();
|
||||
const existing = reservations.find((reservation) => reservation.scope === scope);
|
||||
|
||||
if (!existing || isExpired(existing, now)) {
|
||||
if (existing && isExpired(existing, now)) {
|
||||
const remaining = reservations.filter((reservation) => reservation.reservation_id !== existing.reservation_id);
|
||||
await writeActiveReservations(remaining);
|
||||
await appendReservationHistory({ ...existing, state: 'expired' });
|
||||
}
|
||||
return invalid(command, 'RESERVATION_NOT_FOUND', 'No active reservation exists for this scope.');
|
||||
}
|
||||
|
||||
if (existing.agent_id !== agentId) {
|
||||
return invalid(command, 'RELEASE_FORBIDDEN', 'Only the reservation owner may release this scope.');
|
||||
}
|
||||
|
||||
const released: AgentReservation = {
|
||||
...existing,
|
||||
state: 'released',
|
||||
released_at: now,
|
||||
};
|
||||
|
||||
const remaining = reservations.filter((reservation) => reservation.reservation_id !== existing.reservation_id);
|
||||
await writeActiveReservations(remaining);
|
||||
await appendReservationHistory(released);
|
||||
|
||||
return success(command, released);
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to release reservation.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function statusAgentReservations(
|
||||
input: StatusAgentReservationsInput,
|
||||
deps: Partial<MutationDeps> = {},
|
||||
): Promise<ReservationCommandResponse<StatusAgentReservationsData>> {
|
||||
const command: ReservationCommandName = 'agent status';
|
||||
|
||||
const beadId = trimOrEmpty(input.bead);
|
||||
const agentId = trimOrEmpty(input.agent);
|
||||
|
||||
if (agentId && !(await resolveRegisteredAgent(agentId))) {
|
||||
return invalid(command, 'AGENT_NOT_FOUND', 'Agent is not registered.');
|
||||
}
|
||||
|
||||
try {
|
||||
const now = deps.now ? deps.now() : new Date().toISOString();
|
||||
const swept = await sweepExpiredReservations(now);
|
||||
|
||||
const reservations = swept.active.filter((reservation) => {
|
||||
if (beadId && reservation.bead_id !== beadId) {
|
||||
return false;
|
||||
}
|
||||
if (agentId && reservation.agent_id !== agentId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const unackedRequiredMessages = (await readRequiredAckMessages()).filter((message) => {
|
||||
if (beadId && message.bead_id !== beadId) {
|
||||
return false;
|
||||
}
|
||||
if (agentId && message.to_agent !== agentId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return success(command, {
|
||||
reservations,
|
||||
unacked_required_messages: unackedRequiredMessages,
|
||||
summary: {
|
||||
active: reservations.length,
|
||||
released: 0,
|
||||
expired: swept.expired,
|
||||
unacked_required_messages: unackedRequiredMessages.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to load reservation status.');
|
||||
}
|
||||
}
|
||||
197
src/lib/agent-sessions.ts
Normal file
197
src/lib/agent-sessions.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import type { ActivityEvent } from './activity';
|
||||
import type { BeadIssue } from './types';
|
||||
import { listAgents } from './agent-registry';
|
||||
import { inboxAgentMessages, type AgentMessage } from './agent-mail';
|
||||
|
||||
export type AgentSessionState = 'active' | 'reviewing' | 'deciding' | 'needs_input' | 'completed' | 'stale';
|
||||
|
||||
export interface SessionTaskCard {
|
||||
id: string;
|
||||
title: string;
|
||||
epicId: string;
|
||||
status: BeadIssue['status'];
|
||||
sessionState: AgentSessionState;
|
||||
owner: string | null;
|
||||
lastActor: string | null;
|
||||
lastActivityAt: string | null;
|
||||
communication: {
|
||||
unreadCount: number;
|
||||
pendingRequired: boolean;
|
||||
latestSnippet: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EpicBucket {
|
||||
epic: {
|
||||
id: string;
|
||||
title: string;
|
||||
status: BeadIssue['status'];
|
||||
};
|
||||
tasks: SessionTaskCard[];
|
||||
}
|
||||
|
||||
export interface CommunicationSummary {
|
||||
messages: AgentMessage[];
|
||||
}
|
||||
|
||||
// 24 hours in ms
|
||||
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Gathers all relevant communication for all agents to build a summary for aggregation.
|
||||
*/
|
||||
export async function getCommunicationSummary(): Promise<CommunicationSummary> {
|
||||
const agentsResult = await listAgents({});
|
||||
const agents = agentsResult.data ?? [];
|
||||
const allMessages: AgentMessage[] = [];
|
||||
|
||||
for (const agent of agents) {
|
||||
const inbox = await inboxAgentMessages({ agent: agent.agent_id });
|
||||
if (inbox.data) {
|
||||
allMessages.push(...inbox.data);
|
||||
}
|
||||
}
|
||||
|
||||
return { messages: allMessages };
|
||||
}
|
||||
|
||||
export interface AgentMetrics {
|
||||
activeTasks: number;
|
||||
completedTasks: number;
|
||||
handoffsSent: number;
|
||||
recentWins: { id: string; title: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates real-time metrics for a specific agent based on current issues and history.
|
||||
*/
|
||||
export async function getAgentMetrics(
|
||||
agentId: string,
|
||||
issues: BeadIssue[],
|
||||
activity: ActivityEvent[]
|
||||
): Promise<AgentMetrics> {
|
||||
const agentIssues = issues.filter(i => i.assignee === agentId);
|
||||
const activeTasks = agentIssues.filter(i => i.status !== 'closed').length;
|
||||
|
||||
// Tasks closed by this agent
|
||||
const completedTasks = issues.filter(i => i.status === 'closed' && i.assignee === agentId).length;
|
||||
|
||||
// Count handoffs (e.g. status changes or specific handoff events)
|
||||
const handoffsSent = activity.filter(e => e.actor === agentId && e.kind === 'status_changed').length;
|
||||
|
||||
const recentWins = issues
|
||||
.filter(i => i.status === 'closed' && i.assignee === agentId)
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
|
||||
.slice(0, 3)
|
||||
.map(i => ({ id: i.id, title: i.title }));
|
||||
|
||||
return {
|
||||
activeTasks,
|
||||
completedTasks,
|
||||
handoffsSent,
|
||||
recentWins
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSessionTaskFeed(
|
||||
issues: BeadIssue[],
|
||||
activity: ActivityEvent[],
|
||||
communicationSummary: CommunicationSummary
|
||||
): EpicBucket[] {
|
||||
const epics = issues.filter(i => i.issue_type === 'epic');
|
||||
const tasks = issues.filter(i => i.issue_type !== 'epic');
|
||||
const epicMap = new Map<string, EpicBucket>();
|
||||
|
||||
// Initialize buckets
|
||||
epics.forEach(epic => {
|
||||
epicMap.set(epic.id, {
|
||||
epic: { id: epic.id, title: epic.title, status: epic.status },
|
||||
tasks: []
|
||||
});
|
||||
});
|
||||
|
||||
// Helper to find the actual epic ID even if parent is a task
|
||||
const findRootEpicId = (task: BeadIssue): string | undefined => {
|
||||
// 1. Explicit parent dependency
|
||||
const parentDep = task.dependencies.find(d => d.type === 'parent');
|
||||
if (parentDep) {
|
||||
// If the parent is an epic, we found it
|
||||
if (epicMap.has(parentDep.target)) return parentDep.target;
|
||||
// If parent is a task, recurse
|
||||
const parentIssue = issues.find(i => i.id === parentDep.target);
|
||||
if (parentIssue) return findRootEpicId(parentIssue);
|
||||
}
|
||||
|
||||
// 2. Convention fallback: root prefix (bb-u6f.3.1 -> bb-u6f)
|
||||
const rootId = task.id.split('.')[0];
|
||||
if (epicMap.has(rootId)) return rootId;
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Helper to find latest activity
|
||||
const getActivityForTask = (taskId: string) => {
|
||||
return activity
|
||||
.filter(e => e.beadId === taskId)
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0] ?? null;
|
||||
};
|
||||
|
||||
const deriveState = (task: BeadIssue, lastEvent: ActivityEvent | null, pendingRequired: boolean): AgentSessionState => {
|
||||
if (task.status === 'closed') return 'completed';
|
||||
if (task.status === 'blocked' || pendingRequired) return 'needs_input';
|
||||
|
||||
// Check staleness
|
||||
const lastActiveTime = lastEvent ? new Date(lastEvent.timestamp).getTime() : new Date(task.updated_at).getTime();
|
||||
if (Date.now() - lastActiveTime > STALE_THRESHOLD_MS) {
|
||||
return 'stale';
|
||||
}
|
||||
|
||||
if (task.status === 'in_progress') return 'active';
|
||||
|
||||
return 'deciding';
|
||||
};
|
||||
|
||||
tasks.forEach(task => {
|
||||
let epicId = findRootEpicId(task);
|
||||
let bucket = epicId ? epicMap.get(epicId) : undefined;
|
||||
|
||||
if (!bucket) {
|
||||
if (!epicMap.has('uncategorized')) {
|
||||
epicMap.set('uncategorized', {
|
||||
epic: { id: 'uncategorized', title: 'Uncategorized', status: 'open' },
|
||||
tasks: []
|
||||
});
|
||||
}
|
||||
bucket = epicMap.get('uncategorized')!;
|
||||
epicId = 'uncategorized';
|
||||
}
|
||||
|
||||
const lastEvent = getActivityForTask(task.id);
|
||||
const taskMessages = communicationSummary.messages.filter(m => m.bead_id === task.id);
|
||||
const unreadCount = taskMessages.filter(m => m.state === 'unread').length;
|
||||
const pendingRequired = taskMessages.some(m => m.requires_ack && m.state !== 'acked');
|
||||
const latestMessage = taskMessages.sort((a, b) => b.created_at.localeCompare(a.created_at))[0];
|
||||
|
||||
const sessionState = deriveState(task, lastEvent, pendingRequired);
|
||||
|
||||
const card: SessionTaskCard = {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
epicId: epicId!,
|
||||
status: task.status,
|
||||
sessionState,
|
||||
owner: task.assignee,
|
||||
lastActor: lastEvent?.actor ?? latestMessage?.from_agent ?? null,
|
||||
lastActivityAt: lastEvent?.timestamp ?? latestMessage?.created_at ?? task.updated_at,
|
||||
communication: {
|
||||
unreadCount,
|
||||
pendingRequired,
|
||||
latestSnippet: latestMessage ? latestMessage.subject : null
|
||||
}
|
||||
};
|
||||
|
||||
bucket.tasks.push(card);
|
||||
});
|
||||
|
||||
return Array.from(epicMap.values()).filter(b => b.tasks.length > 0 || b.epic.id !== 'uncategorized');
|
||||
}
|
||||
38
src/lib/read-interactions.ts
Normal file
38
src/lib/read-interactions.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { runBdCommand } from './bridge';
|
||||
|
||||
export interface BeadInteraction {
|
||||
id: string;
|
||||
bead_id: string;
|
||||
actor: string;
|
||||
kind: 'comment';
|
||||
text: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export async function readInteractionsViaBd(projectRoot: string, beadId: string): Promise<BeadInteraction[]> {
|
||||
const command = await runBdCommand({
|
||||
projectRoot,
|
||||
args: ['comments', beadId, '--json'],
|
||||
});
|
||||
|
||||
if (!command.success) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(command.stdout);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
|
||||
return parsed.map(c => ({
|
||||
id: String(c.id),
|
||||
bead_id: beadId,
|
||||
actor: c.author || 'unknown',
|
||||
kind: 'comment',
|
||||
text: c.text || '',
|
||||
timestamp: c.created_at || new Date().toISOString()
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[Interactions] Failed to parse:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
145
src/lib/snapshot-differ.ts
Normal file
145
src/lib/snapshot-differ.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import type { ActivityEvent, ActivityEventKind } from './activity';
|
||||
import type { BeadIssueWithProject, BeadDependency } from './types';
|
||||
|
||||
/**
|
||||
* Compares two snapshots of BeadIssueWithProject arrays and returns a list of ActivityEvents
|
||||
* representing the differences.
|
||||
*/
|
||||
export function diffSnapshots(
|
||||
previous: BeadIssueWithProject[] | null,
|
||||
current: BeadIssueWithProject[]
|
||||
): ActivityEvent[] {
|
||||
const events: ActivityEvent[] = [];
|
||||
const prevMap = new Map<string, BeadIssueWithProject>();
|
||||
if (previous) {
|
||||
previous.forEach((issue) => prevMap.set(issue.id, issue));
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
current.forEach((curr) => {
|
||||
const prev = prevMap.get(curr.id);
|
||||
|
||||
if (!prev) {
|
||||
// 1. Issue Created
|
||||
events.push(createEvent('created', curr, now));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Status Changes
|
||||
if (prev.status !== curr.status) {
|
||||
if (curr.status === 'closed') {
|
||||
events.push(createEvent('closed', curr, now, { from: prev.status, to: 'closed', message: curr.close_reason || undefined }));
|
||||
} else if (prev.status === 'closed') {
|
||||
events.push(createEvent('reopened', curr, now, { from: 'closed', to: curr.status }));
|
||||
} else {
|
||||
events.push(createEvent('status_changed', curr, now, { field: 'status', from: prev.status, to: curr.status }));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Property Changes
|
||||
if (prev.title !== curr.title) {
|
||||
events.push(createEvent('title_changed', curr, now, { field: 'title', from: prev.title, to: curr.title }));
|
||||
}
|
||||
|
||||
if (prev.priority !== curr.priority) {
|
||||
events.push(createEvent('priority_changed', curr, now, { field: 'priority', from: prev.priority, to: curr.priority }));
|
||||
}
|
||||
|
||||
if (prev.description !== curr.description) {
|
||||
events.push(createEvent('description_changed', curr, now, { field: 'description', from: prev.description, to: curr.description }));
|
||||
}
|
||||
|
||||
if (prev.issue_type !== curr.issue_type) {
|
||||
events.push(createEvent('type_changed', curr, now, { field: 'issue_type', from: prev.issue_type, to: curr.issue_type }));
|
||||
}
|
||||
|
||||
if (prev.assignee !== curr.assignee) {
|
||||
events.push(createEvent('assignee_changed', curr, now, { field: 'assignee', from: prev.assignee, to: curr.assignee }));
|
||||
}
|
||||
|
||||
if (prev.due_at !== curr.due_at) {
|
||||
events.push(createEvent('due_date_changed', curr, now, { field: 'due_at', from: prev.due_at, to: curr.due_at }));
|
||||
}
|
||||
|
||||
if (prev.estimated_minutes !== curr.estimated_minutes) {
|
||||
events.push(createEvent('estimate_changed', curr, now, { field: 'estimated_minutes', from: prev.estimated_minutes, to: curr.estimated_minutes }));
|
||||
}
|
||||
|
||||
// 4. Collection Changes (Labels)
|
||||
if (!areArraysEqual(prev.labels, curr.labels)) {
|
||||
events.push(createEvent('labels_changed', curr, now, {
|
||||
field: 'labels',
|
||||
from: prev.labels.join(','),
|
||||
to: curr.labels.join(',')
|
||||
}));
|
||||
}
|
||||
|
||||
// 5. Collection Changes (Dependencies)
|
||||
diffDependencies(prev.dependencies, curr.dependencies).forEach(kindAndTarget => {
|
||||
events.push(createEvent(kindAndTarget.kind, curr, now, { to: kindAndTarget.target }));
|
||||
});
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create an ActivityEvent with standard fields.
|
||||
*/
|
||||
function createEvent(
|
||||
kind: ActivityEventKind,
|
||||
issue: BeadIssueWithProject,
|
||||
timestamp: string,
|
||||
payload: ActivityEvent['payload'] = {}
|
||||
): ActivityEvent {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
kind,
|
||||
beadId: issue.id,
|
||||
beadTitle: issue.title,
|
||||
projectId: issue.project.key,
|
||||
projectName: issue.project.name,
|
||||
timestamp,
|
||||
actor: issue.assignee || issue.owner || issue.created_by,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow equality check for string arrays (labels).
|
||||
*/
|
||||
function areArraysEqual(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
const sortedA = [...a].sort();
|
||||
const sortedB = [...b].sort();
|
||||
return sortedA.every((val, index) => val === sortedB[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects added and removed dependencies.
|
||||
*/
|
||||
function diffDependencies(
|
||||
prev: BeadDependency[],
|
||||
curr: BeadDependency[]
|
||||
): { kind: 'dependency_added' | 'dependency_removed', target: string }[] {
|
||||
const changes: { kind: 'dependency_added' | 'dependency_removed', target: string }[] = [];
|
||||
|
||||
const prevTargets = new Set(prev.map(d => d.target));
|
||||
const currTargets = new Set(curr.map(d => d.target));
|
||||
|
||||
curr.forEach(d => {
|
||||
if (!prevTargets.has(d.target)) {
|
||||
changes.push({ kind: 'dependency_added', target: d.target });
|
||||
}
|
||||
});
|
||||
|
||||
prev.forEach(d => {
|
||||
if (!currTargets.has(d.target)) {
|
||||
changes.push({ kind: 'dependency_removed', target: d.target });
|
||||
}
|
||||
});
|
||||
|
||||
return changes;
|
||||
}
|
||||
57
tests/lib/activity.test.ts
Normal file
57
tests/lib/activity.test.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import type { ActivityEvent, ActivityEventKind } from '../../src/lib/activity';
|
||||
|
||||
describe('Activity Event Model (bb-xhm.1)', () => {
|
||||
it('should support all 16 required transition types in ActivityEventKind', () => {
|
||||
const kinds: ActivityEventKind[] = [
|
||||
'created',
|
||||
'closed',
|
||||
'reopened',
|
||||
'status_changed',
|
||||
'priority_changed',
|
||||
'assignee_changed',
|
||||
'type_changed',
|
||||
'title_changed',
|
||||
'description_changed',
|
||||
'labels_changed',
|
||||
'dependency_added',
|
||||
'dependency_removed',
|
||||
'comment_added',
|
||||
'due_date_changed',
|
||||
'estimate_changed',
|
||||
'field_changed',
|
||||
];
|
||||
|
||||
assert.strictEqual(kinds.length, 16, 'Should have exactly 16 transition types');
|
||||
|
||||
// Verify specific important types are present
|
||||
assert.ok(kinds.includes('created'));
|
||||
assert.ok(kinds.includes('closed'));
|
||||
assert.ok(kinds.includes('reopened'));
|
||||
assert.ok(kinds.includes('comment_added'));
|
||||
});
|
||||
|
||||
it('should allow creating a valid ActivityEvent object', () => {
|
||||
const event: ActivityEvent = {
|
||||
id: 'evt-123',
|
||||
kind: 'status_changed',
|
||||
beadId: 'bb-1',
|
||||
beadTitle: 'Test Bead',
|
||||
projectId: 'proj-1',
|
||||
projectName: 'Test Project',
|
||||
timestamp: new Date().toISOString(),
|
||||
actor: 'zenchantlive',
|
||||
payload: {
|
||||
field: 'status',
|
||||
from: 'open',
|
||||
to: 'in_progress',
|
||||
},
|
||||
};
|
||||
|
||||
assert.strictEqual(event.kind, 'status_changed');
|
||||
assert.strictEqual(event.payload.field, 'status');
|
||||
assert.strictEqual(event.payload.from, 'open');
|
||||
assert.strictEqual(event.payload.to, 'in_progress');
|
||||
});
|
||||
});
|
||||
107
tests/lib/agent-sessions.test.ts
Normal file
107
tests/lib/agent-sessions.test.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import type { BeadIssue } from '../../src/lib/types';
|
||||
import type { ActivityEvent } from '../../src/lib/activity';
|
||||
import { buildSessionTaskFeed } from '../../src/lib/agent-sessions';
|
||||
|
||||
// Mock Data
|
||||
const MOCK_ISSUE: BeadIssue = {
|
||||
id: 'task-1',
|
||||
title: 'Test Task',
|
||||
description: null,
|
||||
status: 'in_progress',
|
||||
priority: 2,
|
||||
issue_type: 'task',
|
||||
assignee: 'agent-smith',
|
||||
owner: 'user',
|
||||
labels: [],
|
||||
dependencies: [{ type: 'parent', target: 'epic-1' }],
|
||||
created_at: '2026-01-01',
|
||||
updated_at: '2026-01-02',
|
||||
closed_at: null,
|
||||
close_reason: null,
|
||||
closed_by_session: null,
|
||||
created_by: 'user',
|
||||
due_at: null,
|
||||
estimated_minutes: null,
|
||||
external_ref: null,
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const MOCK_EPIC: BeadIssue = {
|
||||
...MOCK_ISSUE,
|
||||
id: 'epic-1',
|
||||
title: 'Test Epic',
|
||||
issue_type: 'epic',
|
||||
status: 'open',
|
||||
dependencies: []
|
||||
};
|
||||
|
||||
const MOCK_ACTIVITY: ActivityEvent = {
|
||||
id: 'evt-1',
|
||||
kind: 'comment_added',
|
||||
beadId: 'task-1',
|
||||
beadTitle: 'Test Task',
|
||||
projectId: 'root',
|
||||
projectName: 'root',
|
||||
timestamp: new Date().toISOString(), // Just now
|
||||
actor: 'agent-smith',
|
||||
payload: { message: 'Working on it' }
|
||||
};
|
||||
|
||||
describe('Agent Sessions Aggregation', () => {
|
||||
it('should group tasks by epic', () => {
|
||||
const issues = [MOCK_EPIC, MOCK_ISSUE];
|
||||
const feed = buildSessionTaskFeed(issues, [], { messages: [] });
|
||||
|
||||
assert.strictEqual(feed.length, 1); // 1 Epic group
|
||||
assert.strictEqual(feed[0].epic.id, 'epic-1');
|
||||
assert.strictEqual(feed[0].tasks.length, 1);
|
||||
assert.strictEqual(feed[0].tasks[0].id, 'task-1');
|
||||
});
|
||||
|
||||
it('should handle orphan tasks in "Uncategorized" bucket', () => {
|
||||
const orphan = { ...MOCK_ISSUE, id: 'orphan-1', dependencies: [] };
|
||||
const feed = buildSessionTaskFeed([orphan], [], { messages: [] });
|
||||
|
||||
assert.strictEqual(feed.length, 1);
|
||||
assert.strictEqual(feed[0].epic.title, 'Uncategorized');
|
||||
assert.strictEqual(feed[0].tasks[0].id, 'orphan-1');
|
||||
});
|
||||
|
||||
it('should derive session state: active', () => {
|
||||
const issues = [MOCK_ISSUE]; // in_progress
|
||||
const feed = buildSessionTaskFeed(issues, [MOCK_ACTIVITY], { messages: [] });
|
||||
|
||||
// MOCK_ISSUE is in_progress and has recent activity -> active
|
||||
const card = feed[0].tasks[0];
|
||||
assert.strictEqual(card.sessionState, 'active');
|
||||
});
|
||||
|
||||
it('should derive session state: needs_input (blocked)', () => {
|
||||
const blocked = { ...MOCK_ISSUE, status: 'blocked' as const };
|
||||
const feed = buildSessionTaskFeed([blocked], [], { messages: [] });
|
||||
|
||||
const card = feed[0].tasks[0];
|
||||
assert.strictEqual(card.sessionState, 'needs_input');
|
||||
});
|
||||
|
||||
it('should derive session state: completed', () => {
|
||||
const closed = { ...MOCK_ISSUE, status: 'closed' as const };
|
||||
const feed = buildSessionTaskFeed([closed], [], { messages: [] });
|
||||
|
||||
const card = feed[0].tasks[0];
|
||||
assert.strictEqual(card.sessionState, 'completed');
|
||||
});
|
||||
|
||||
it('should identify stale sessions', () => {
|
||||
const staleTime = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(); // 25 hours ago
|
||||
const staleIssue = { ...MOCK_ISSUE, updated_at: staleTime };
|
||||
const oldActivity = { ...MOCK_ACTIVITY, timestamp: staleTime };
|
||||
|
||||
const feed = buildSessionTaskFeed([staleIssue], [oldActivity], { messages: [] });
|
||||
|
||||
const card = feed[0].tasks[0];
|
||||
assert.strictEqual(card.sessionState, 'stale');
|
||||
});
|
||||
});
|
||||
146
tests/lib/snapshot-differ.test.ts
Normal file
146
tests/lib/snapshot-differ.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import type { BeadIssueWithProject } from '../../src/lib/types';
|
||||
import { diffSnapshots } from '../../src/lib/snapshot-differ';
|
||||
|
||||
const MOCK_PROJECT = {
|
||||
key: 'proj-1',
|
||||
root: 'C:\\test', // Corrected: Escaped backslash for Windows path
|
||||
displayPath: 'test',
|
||||
name: 'Test Project',
|
||||
source: 'local' as const,
|
||||
addedAt: null,
|
||||
};
|
||||
|
||||
function createMockIssue(id: string, overrides: Partial<BeadIssueWithProject> = {}): BeadIssueWithProject {
|
||||
return {
|
||||
id,
|
||||
title: `Title ${id}`,
|
||||
description: null,
|
||||
status: 'open',
|
||||
priority: 2,
|
||||
issue_type: 'task',
|
||||
assignee: null,
|
||||
owner: 'owner',
|
||||
labels: [],
|
||||
dependencies: [],
|
||||
created_at: '2026-02-13T00:00:00Z',
|
||||
updated_at: '2026-02-13T00:00:00Z',
|
||||
closed_at: null,
|
||||
close_reason: null,
|
||||
closed_by_session: null,
|
||||
created_by: 'creator',
|
||||
due_at: null,
|
||||
estimated_minutes: null,
|
||||
external_ref: null,
|
||||
metadata: {},
|
||||
project: MOCK_PROJECT,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Snapshot Differ (bb-xhm.2)', () => {
|
||||
it('should emit "created" for new issues', () => {
|
||||
const prev: BeadIssueWithProject[] = [];
|
||||
const curr: BeadIssueWithProject[] = [createMockIssue('bb-1')];
|
||||
|
||||
const events = diffSnapshots(prev, curr);
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].kind, 'created');
|
||||
assert.strictEqual(events[0].beadId, 'bb-1');
|
||||
});
|
||||
|
||||
it('should emit "status_changed" for non-closed status transitions', () => {
|
||||
const prev = [createMockIssue('bb-1', { status: 'open' })];
|
||||
const curr = [createMockIssue('bb-1', { status: 'in_progress' })];
|
||||
|
||||
const events = diffSnapshots(prev, curr);
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].kind, 'status_changed');
|
||||
assert.strictEqual(events[0].payload.from, 'open');
|
||||
assert.strictEqual(events[0].payload.to, 'in_progress');
|
||||
});
|
||||
|
||||
it('should emit "closed" when status moves to closed', () => {
|
||||
const prev = [createMockIssue('bb-1', { status: 'in_progress' })];
|
||||
const curr = [createMockIssue('bb-1', { status: 'closed', closed_at: '2026-02-13T01:00:00Z' })];
|
||||
|
||||
const events = diffSnapshots(prev, curr);
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].kind, 'closed');
|
||||
});
|
||||
|
||||
it('should emit "reopened" when status moves from closed to open', () => {
|
||||
const prev = [createMockIssue('bb-1', { status: 'closed' })];
|
||||
const curr = [createMockIssue('bb-1', { status: 'open' })];
|
||||
|
||||
const events = diffSnapshots(prev, curr);
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].kind, 'reopened');
|
||||
});
|
||||
|
||||
it('should emit "assignee_changed" (assigned/unassigned/reassigned)', () => {
|
||||
// Assigned
|
||||
let events = diffSnapshots(
|
||||
[createMockIssue('bb-1', { assignee: null })],
|
||||
[createMockIssue('bb-1', { assignee: 'alice' })]
|
||||
);
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].kind, 'assignee_changed');
|
||||
assert.strictEqual(events[0].payload.to, 'alice');
|
||||
|
||||
// Unassigned
|
||||
events = diffSnapshots(
|
||||
[createMockIssue('bb-1', { assignee: 'alice' })],
|
||||
[createMockIssue('bb-1', { assignee: null })]
|
||||
);
|
||||
assert.strictEqual(events[0].payload.from, 'alice');
|
||||
assert.strictEqual(events[0].payload.to, null);
|
||||
|
||||
// Reassigned
|
||||
events = diffSnapshots(
|
||||
[createMockIssue('bb-1', { assignee: 'alice' })],
|
||||
[createMockIssue('bb-1', { assignee: 'bob' })]
|
||||
);
|
||||
assert.strictEqual(events[0].payload.from, 'alice');
|
||||
assert.strictEqual(events[0].payload.to, 'bob');
|
||||
});
|
||||
|
||||
it('should emit "labels_changed" when labels are modified', () => {
|
||||
const prev = [createMockIssue('bb-1', { labels: ['bug'] })];
|
||||
const curr = [createMockIssue('bb-1', { labels: ['bug', 'ui'] })];
|
||||
|
||||
const events = diffSnapshots(prev, curr);
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].kind, 'labels_changed');
|
||||
});
|
||||
|
||||
it('should emit "dependency_added" and "dependency_removed"', () => {
|
||||
const prev = [createMockIssue('bb-1', { dependencies: [] })];
|
||||
const curr = [createMockIssue('bb-1', { dependencies: [{ type: 'blocks', target: 'bb-2' }] })];
|
||||
|
||||
const events = diffSnapshots(prev, curr);
|
||||
assert.strictEqual(events.length, 1);
|
||||
assert.strictEqual(events[0].kind, 'dependency_added');
|
||||
assert.strictEqual(events[0].payload.to, 'bb-2');
|
||||
});
|
||||
|
||||
it('should ignore noise (updated_at only changes)', () => {
|
||||
const prev = [createMockIssue('bb-1', { updated_at: '2026-02-13T00:00:00Z' })];
|
||||
const curr = [createMockIssue('bb-1', { updated_at: '2026-02-13T00:01:00Z' })];
|
||||
|
||||
const events = diffSnapshots(prev, curr);
|
||||
assert.strictEqual(events.length, 0);
|
||||
});
|
||||
|
||||
it('should emit multiple events for multiple field changes', () => {
|
||||
const prev = [createMockIssue('bb-1', { status: 'open', assignee: null })];
|
||||
const curr = [createMockIssue('bb-1', { status: 'in_progress', assignee: 'alice' })];
|
||||
|
||||
const events = diffSnapshots(prev, curr);
|
||||
assert.strictEqual(events.length, 2);
|
||||
const kinds = events.map(e => e.kind);
|
||||
assert.ok(kinds.includes('status_changed'));
|
||||
assert.ok(kinds.includes('assignee_changed'));
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue