- Add webhookUrl to NotificationSettingsSchema
- Create clawdbot-webhook-service with fire-and-forget delivery + 1 retry
- Hook into broadcastTaskChange and broadcastChatMessage in broadcast-service
- Support VERITAS_WEBHOOK_URL and VERITAS_WEBHOOK_SECRET env vars
- HMAC-SHA256 payload signing via X-Webhook-Signature header
- Add 16 tests covering config, signing, delivery, retry, and payload formatting
- Update .env.example with new env var documentation
- Fix delete not clearing UI (React Query kept stale data on 404 refetch)
- Fix post-delete send breaking (server recreates task sessions instead of 404)
- Add chat export as markdown (download icon next to trash)
- Auto-focus input on panel open and after each send
- Defensive delete on server (handle already-deleted sessions)
- Chat now generates actual AI responses via Clawdbot gateway WebSocket
- Gateway chat client handles auth, streaming deltas, and final response
- Chat responses broadcast to UI via kanban WebSocket (shared connection)
- Fix double X close button on chat panel (SheetContent built-in + manual)
- Move Chat/Template buttons above tab row to reduce cramping
- Chat/Template buttons now 50/50 full width
- Fix subtask add button height mismatch
- Fix chat send crash (API response type mismatch)
- Remove non-functional agent/model dropdowns from chat panel
- Add model picker (sonnet/opus/haiku) to Agent panel in task detail
- Replace tooltip with inline mode hint text in chat
- Load chat history on panel open (deterministic task session ID)
- Chat stream uses shared WebSocket instead of opening duplicate connection
- Add agent field to CreateTaskForm state with 'auto' default
- Agent dropdown in CreateTaskDialog (Auto + all enabled agents)
- Agent field in TaskMetadataSection (task detail panel) — editable inline
- Pass agent through useTemplateForm to task creation API
- Server: accept agent field in create/update task schemas
- TaskService: persist agent field on task creation
- AgentPanel: pre-select task's assigned agent (priority: manual > pre-assigned > routed > default)
- Sprint and Agent dropdowns now share a row for compact layout
- Add AgentRoutingConfig types with RoutingRule, RoutingMatchCriteria, RoutingResult
- Add DEFAULT_ROUTING_CONFIG with sensible defaults (code/bug/docs/review rules)
- Create AgentRoutingService with resolveAgent() and getFallback() methods
- First-match-wins rule evaluation with type, priority, project, minSubtasks criteria
- Array support for match criteria (e.g., type: ['code', 'bug'])
- Routing API: POST /agents/route, GET/PUT /agents/routing
- Integrate routing into ClawdbotAgentService.startAgent() for 'auto' agent selection
- Add agent field to Task, CreateTaskInput, UpdateTaskInput, TaskSummary schemas
- Settings UI: routing rules section in Agents tab with add/edit/remove/reorder
- Agent Panel: show routing recommendation when starting agent
- 17 unit tests for routing service (all passing)
- Full typecheck clean across shared, server, and web packages
Multiple tasks can each have running timers simultaneously — this supports
multi-agent workflows where different agents track different tasks.
Removed:
- getRunningTimerTask() method (global scan)
- Auto-stop of other tasks' timers in startTimer()
- otherRunningTask UI check that hid the Start button
Kept: per-task guard (can't start a timer on a task that already has one running).
Root cause of all timer UI bugs: the component relied on React Query's
cache propagation chain (mutation → patchTaskInList → useTasks() hook →
re-render) which was unreliable due to structural sharing, concurrent
invalidations from debounced saves, and the multi-layer prop pipeline
(useDebouncedSave → localTask → TaskDetailsTab → TimeTrackingSection).
The fix: TimeTrackingSection now owns its own local state, initialized
from the task prop and updated DIRECTLY from API responses after each
mutation. This guarantees instant UI updates regardless of React Query's
internal state:
- Start timer → API response → setTimeTracking → UI shows Stop immediately
- Stop timer → API response → setTimeTracking → UI shows Start immediately
- Delete entry → API response → setTimeTracking → entry disappears immediately
- Add entry → API response → setTimeTracking → entry appears immediately
The query cache is still patched as a secondary update path (for other
components) and synced from for external changes (WebSocket events,
background refetches), but it's no longer the primary driver.
Also:
- Server timer methods now throw ConflictError (409) / NotFoundError (404)
instead of generic Error (which mapped to 500)
- Delete buttons disabled while a mutation is in flight
- Removed useCallback (unnecessary for onClick handlers)
- JSON fingerprint for cache sync prevents unnecessary effect fires
- Server: startTimer() auto-stops any running timer on another task before
starting the new one. Prevents multiple simultaneous timers.
- Server: updateTask() re-reads from cache inside file lock to prevent
concurrent writes (debounced saves) from clobbering timer state.
- Server: Added WebSocket broadcasts to all timer routes (start/stop/add/delete)
so other clients get real-time updates.
- UI: TimeTrackingSection hides Start button when another task has an
active timer, showing 'Timer active on another task' instead.
- UI: Subscribe directly to query cache for timer state instead of relying
on the debounced-save prop pipeline.
- UI: Added mutatingRef guard to prevent double-clicks on Start/Stop.
Fixes the issue where 3 tasks could have running timers simultaneously.
ManagedListService.create() now accepts an optional 'id' field.
If provided, it uses the explicit ID instead of generating slug-nanoid.
Also adds duplicate ID check to prevent conflicts.
This fixes the corrupted task-type IDs (e.g., 'bug-GdN5rT' instead of 'bug')
that were caused by always appending random suffixes.
Root cause: Each route file created its own TaskService instance via
'new TaskService()', resulting in 13+ separate in-memory caches.
When task-archive.ts archived a task, it removed it from its own cache,
but tasks.ts (which serves GET /api/tasks) still had the task in its
separate cache — so archived tasks continued appearing in the list.
Fix: All route files now use getTaskService() singleton, ensuring a
single shared cache across all endpoints. Service files retain their
own instances for test isolation.
Fixes#22
File-based locking with wx flag doesn't guarantee ordering within the
same Node.js process — concurrent fs.writeFile calls race at the OS
level. Added a per-file Promise chain that serializes lock requests
in FIFO order within the same process. The file lock still provides
cross-process protection.
Fixes flaky withFileLock serialization test (was ~30% failure rate,
now 20/20 passes).