From 2a923f3a73daa6aebd61694d4c8470c7c4ccbe91 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Mon, 24 Nov 2025 21:02:39 -0500 Subject: task: migrate storage from jsonl to sqlite Removes .tasks/tasks.jsonl and replaces it with a local SQLite database (.tasks/tasks.db). Adds --db flag to CLI. Removes sync command. Amp-Thread-ID: https://ampcode.com/threads/T-ac41b9b6-d117-46de-9e4f-842887a22f1d Co-authored-by: Amp --- .tasks/tasks.jsonl | 235 ------------------- Omni/Bild/Deps/Haskell.nix | 1 + Omni/Task.hs | 41 ++-- Omni/Task/Core.hs | 558 +++++++++++++++++++-------------------------- 4 files changed, 254 insertions(+), 581 deletions(-) delete mode 100644 .tasks/tasks.jsonl diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl deleted file mode 100644 index 7dc530a..0000000 --- a/.tasks/tasks.jsonl +++ /dev/null @@ -1,235 +0,0 @@ -{"taskCreatedAt":"2025-11-08T20:03:50.230851965Z","taskDependencies":[],"taskDescription":null,"taskId":"t-a1b2c3","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Show help text when task invoked without args","taskType":"WorkTask","taskUpdatedAt":"2025-11-08T20:06:02.605878048Z"} -{"taskCreatedAt":"2025-11-08T20:03:53.429072631Z","taskDependencies":[],"taskDescription":null,"taskId":"t-d4e5f6","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Move dev instructions from README.md to AGENTS.md","taskType":"WorkTask","taskUpdatedAt":"2025-11-08T20:06:22.732392229Z"} -{"taskCreatedAt":"2025-11-08T20:06:27.395834401Z","taskDependencies":[],"taskDescription":null,"taskId":"t-g7h8i9","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Task ids should be shorter. Use the sqids package in haskell to generate ids","taskType":"WorkTask","taskUpdatedAt":"2025-11-08T21:00:37.311865046Z"} -{"taskCreatedAt":"2025-11-08T20:09:35.590622249Z","taskDependencies":[],"taskDescription":null,"taskId":"t-j0k1l2","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Tasks should have an optional namespace associated with them. Namespaces are first class citizens in this monorepo","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:14:05.775741617Z"} -{"taskCreatedAt":"2025-11-08T20:10:09.944217463Z","taskDependencies":[],"taskDescription":null,"taskId":"t-m3n4o5","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"There should be a command to list all projects.","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:44:57.393279815Z"} -{"taskCreatedAt":"2025-11-08T20:20:38.785442739Z","taskDependencies":[],"taskDescription":null,"taskId":"t-p6q7r8","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Instruct agents too use git-branchless and a patch based workflow rather than traditional git commands if and when they need to record things in git.","taskType":"WorkTask","taskUpdatedAt":"2025-11-08T21:09:06.854871964Z"} -{"taskCreatedAt":"2025-11-08T20:22:20.116289616Z","taskDependencies":[],"taskDescription":null,"taskId":"t-s9t0u1","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"instruct agents to include tests with all new features and bug fixes","taskType":"WorkTask","taskUpdatedAt":"2025-11-08T21:24:54.004658966Z"} -{"taskCreatedAt":"2025-11-08T20:45:12.764939794Z","taskDependencies":[],"taskDescription":null,"taskId":"t-v2w3x4","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"instruct agents to run 'bild --test' and 'lint' for whatever namespace(s) they are working on after completing a task and fix any reported errors","taskType":"WorkTask","taskUpdatedAt":"2025-11-08T21:25:10.756670871Z"} -{"taskCreatedAt":"2025-11-08T20:48:43.183226361Z","taskDependencies":[],"taskDescription":null,"taskId":"t-y5z6a7","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"The script Omni/Ide/typecheck.sh needs to support Haskell type checking in a similar fashion as how Omni/Ide/repl.sh is able to handle multiple languages","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T19:39:09.64405169Z"} -{"taskCreatedAt":"2025-11-08T21:00:27.020241869Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1ky7gj2","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test shorter IDs","taskType":"WorkTask","taskUpdatedAt":"2025-11-08T21:04:00.990704969Z"} -{"taskCreatedAt":"2025-11-08T21:00:29.901677247Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1kyjmjn","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Another test task","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:13:51.934598506Z"} -{"taskCreatedAt":"2025-11-08T21:11:41.013924674Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1lhjhgs","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove the old aider config in .aider* files and directories. Aider stinks and we will use amp going forward","taskType":"WorkTask","taskUpdatedAt":"2025-11-08T21:28:34.875747622Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.468930038Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ppxwsu","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Task Manager Improvements","taskType":"Epic","taskUpdatedAt":"2025-11-09T13:05:06.468930038Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.718797697Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ppyzt2","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement child ID generation (t-abc123.1)","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T00:45:25.831872782Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.746734115Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ppz6jc","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add child_counters storage","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:09:49.499868491Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.774903465Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ppze3x","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update createTask to auto-generate child IDs","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T23:06:53.123460583Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.802295008Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ppzlbl","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement task tree visualization command","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:47:12.411364105Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.829842253Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ppzsm4","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement task stats command","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T19:05:37.772094417Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.85771202Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ppzzba","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement epic progress tracking","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T19:19:05.482575703Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.88583862Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ppzgvf","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add filtering by type and parent (list improvements)","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:17:51.373969453Z"} -{"taskCreatedAt":"2025-11-09T13:05:18.344932105Z","taskDependencies":[],"taskDescription":null,"taskId":"t-pqllxk","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement epic and task types","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:18.406381682Z"} -{"taskCreatedAt":"2025-11-09T13:05:18.445111257Z","taskDependencies":[],"taskDescription":null,"taskId":"t-pqmc17","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add enhanced dependency types (blocks, discovered-from, related)","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:18.50495798Z"} -{"taskCreatedAt":"2025-11-09T13:05:18.543055749Z","taskDependencies":[],"taskDescription":null,"taskId":"t-pqmbus","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Protect production database from tests","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:18.602787251Z"} -{"taskCreatedAt":"2025-11-09T13:05:18.64074361Z","taskDependencies":[],"taskDescription":null,"taskId":"t-pqn0uu","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add migration support for old task format","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:18.703048004Z"} -{"taskCreatedAt":"2025-11-09T14:22:32.038937583Z","taskDependencies":[],"taskDescription":null,"taskId":"t-uumhrq","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Investigate and implement prettier tree drawing with box characters","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T18:40:33.764590135Z"} -{"taskCreatedAt":"2025-11-09T16:48:40.260201423Z","taskDependencies":[],"taskDescription":null,"taskId":"t-143kql2","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"PodcastItLater: Path to Paid Product","taskType":"Epic","taskUpdatedAt":"2025-11-09T16:48:40.260201423Z"} -{"taskCreatedAt":"2025-11-09T16:48:47.076581674Z","taskDependencies":[],"taskDescription":null,"taskId":"t-144drae","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Adopt Bootstrap CSS for UI improvements","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T17:00:05.424532832Z"} -{"taskCreatedAt":"2025-11-09T16:48:47.237113366Z","taskDependencies":[],"taskDescription":null,"taskId":"t-144e7lf","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add Stripe integration for billing","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T23:04:23.856763018Z"} -{"taskCreatedAt":"2025-11-09T16:48:47.388960509Z","taskDependencies":[],"taskDescription":null,"taskId":"t-144ekr1","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement usage tracking and limits","taskType":"WorkTask","taskUpdatedAt":"2025-11-19T03:27:25.707745105Z"} -{"taskCreatedAt":"2025-11-09T16:48:47.589181852Z","taskDependencies":[],"taskDescription":null,"taskId":"t-144fawn","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add email notifications (transactional)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T01:35:54.519545888Z"} -{"taskCreatedAt":"2025-11-09T16:48:47.737218185Z","taskDependencies":[],"taskDescription":null,"taskId":"t-144gds4","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Migrate from SQLite to PostgreSQL","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T01:35:54.70061831Z"} -{"taskCreatedAt":"2025-11-09T16:48:47.887102357Z","taskDependencies":[],"taskDescription":null,"taskId":"t-144gqry","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Create basic admin dashboard","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T13:50:19.733612558Z"} -{"taskCreatedAt":"2025-11-09T16:48:48.072927212Z","taskDependencies":[],"taskDescription":null,"taskId":"t-144hcmj","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Complete comprehensive test suite","taskType":"Epic","taskUpdatedAt":"2025-11-09T16:48:48.072927212Z"} -{"taskCreatedAt":"2025-11-09T17:48:34.522286485Z","taskDependencies":[],"taskDescription":null,"taskId":"t-17z0069","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix Recent Episodes refresh to prepend instead of reload (interrupts audio playback)","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T19:42:22.105902786Z"} -{"taskCreatedAt":"2025-11-09T22:19:27.303689497Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1piv0zf","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement billing page UI component with pricing and upgrade options","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T23:04:20.974801117Z"} -{"taskCreatedAt":"2025-11-09T22:38:46.235799803Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1qzlmb4","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add a 'task show ' command that prints out a long, easy to read (for humans) version of the task. Include dependencies and all information fields in the output","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T16:37:18.61969283Z"} -{"taskCreatedAt":"2025-11-09T22:56:18.897655607Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1s8adc0","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Make PodcastItLater UI mobile-friendly and responsive","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T23:09:16.712244322Z"} -{"taskCreatedAt":"2025-11-10T01:32:42.893029428Z","taskDependencies":[],"taskDescription":null,"taskId":"t-64tkb5","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add dark mode support to PodcastItLater UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-10T02:16:39.202726983Z"} -{"taskCreatedAt":"2025-11-13T16:32:05.496080694Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12yqukr","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Simplify billing to single paid plan","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:37:49.407332883Z"} -{"taskCreatedAt":"2025-11-13T16:32:16.514172804Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12zb93b","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Separate navbar into user navbar and callout box for plan info","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:34:44.480359383Z"} -{"taskCreatedAt":"2025-11-13T16:32:16.718245548Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12zc095","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Make 'Upgrade Now' button go directly to Stripe checkout (not /billing page)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:34:44.530482584Z"} -{"taskCreatedAt":"2025-11-13T16:32:16.899253732Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12zclez","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add plan details to callout box (unlimited articles, $12/month)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:34:44.579475578Z"} -{"taskCreatedAt":"2025-11-13T16:32:17.077566618Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12zdvcb","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Delete /billing page and all related code (billing_page, BillingPage component)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:36:41.708746244Z"} -{"taskCreatedAt":"2025-11-13T16:32:17.264388472Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12zeidq","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update billing_checkout to use 'paid' tier instead of 'pro'","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:36:41.758424911Z"} -{"taskCreatedAt":"2025-11-13T16:32:17.411379982Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12zeusg","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update success/cancel URLs to redirect to / instead of /billing","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:36:41.808119038Z"} -{"taskCreatedAt":"2025-11-13T16:32:17.557115348Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12zfwnf","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove 'Billing' button from navbar (paid users will use Stripe portal link in callout)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:34:44.628587871Z"} -{"taskCreatedAt":"2025-11-13T16:32:17.738052991Z","taskDependencies":[],"taskDescription":null,"taskId":"t-12zghrb","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test the complete flow and verify all changes","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T16:37:49.356932049Z"} -{"taskCreatedAt":"2025-11-13T19:38:08.01779309Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1f9rizd","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-1vipjyg","taskPriority":"P2","taskStatus":"Done","taskTitle":"Account Management Page","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T13:50:19.815116309Z"} -{"taskCreatedAt":"2025-11-13T19:38:08.176692694Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1f9snu7","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-1vipjyg","taskPriority":"P2","taskStatus":"Done","taskTitle":"Queue Status Improvements","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T13:50:19.89665814Z"} -{"taskCreatedAt":"2025-11-13T19:38:08.37344762Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1f9td4u","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-1vipjyg","taskPriority":"P2","taskStatus":"Done","taskTitle":"Navbar Styling Cleanup","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T13:50:19.977778598Z"} -{"taskCreatedAt":"2025-11-13T19:38:32.95559213Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbym1m","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove BLE001 noqa for bare Exception catches - use specific exceptions","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:43:29.049855419Z"} -{"taskCreatedAt":"2025-11-13T19:38:33.139120541Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbz7lv","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix PLR0913 violations - refactor functions with too many parameters","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:44:09.820023426Z"} -{"taskCreatedAt":"2025-11-13T19:38:33.309222802Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbzq1v","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Extract format_duration utility to shared UI or Core module (used only in Web.py)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:45:49.402934404Z"} -{"taskCreatedAt":"2025-11-13T19:38:33.491331064Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbabod","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Extract extract_og_metadata and send_magic_link to Core module for reusability","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:46:04.679290775Z"} -{"taskCreatedAt":"2025-11-13T19:38:33.674140035Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbbmxa","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Review and fix type: ignore comments - improve type safety","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:47:09.583640045Z"} -{"taskCreatedAt":"2025-11-13T19:38:33.85804778Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbc8nq","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove PLR2004 magic number - use constant for month check","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:47:45.120428021Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.035597081Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbcszd","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement cancel subscription functionality","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:22:51.709672316Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.194926176Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbdyr2","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement delete account functionality","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:14:24.645486426Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.384489707Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbelkv","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement change email address functionality","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:14:24.726951592Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.561871604Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbf5tv","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add logout button to account page","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:22:51.65796855Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.777721397Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbg02x","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Replace Coming Soon placeholder with full account management UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:22:51.606196024Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.962196629Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbgm2m","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add remove button to queue status items","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:20:10.941908917Z"} -{"taskCreatedAt":"2025-11-13T19:38:35.119686179Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbhr0w","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove button classes from navbar links (make them regular nav links)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:18:17.185088389Z"} -{"taskCreatedAt":"2025-11-13T19:38:35.311151364Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbieof","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove 'Logged in as' text from navbar","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:18:17.23552934Z"} -{"taskCreatedAt":"2025-11-13T19:38:35.476139354Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbivjl","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Left-align navbar links instead of right-aligned buttons","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:18:17.285578917Z"} -{"taskCreatedAt":"2025-11-13T19:38:35.65125955Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fbjfic","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove logout button from navbar (will be in account page)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:18:17.336546723Z"} -{"taskCreatedAt":"2025-11-13T19:54:08.34625259Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1gcr9rv","taskNamespace":"Omni/Bild.nix","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add ruff to the developer environment, the 'env' attribute in Bild.nix","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:04:43.292235852Z"} -{"taskCreatedAt":"2025-11-13T20:02:50.914482516Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1gmdnjk","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix dev mode banner styling and pre-fill login email","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:03:45.644107089Z"} -{"taskCreatedAt":"2025-11-13T21:01:35.331063546Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1kcjtuu","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix Stripe portal error handling and account page padding","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T21:03:19.701792229Z"} -{"taskCreatedAt":"2025-11-14T18:19:16.584321849Z","taskDependencies":[],"taskDescription":null,"taskId":"t-19zf6a8","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Parallel Target Builds - Epic","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:03:02.525200039Z"} -{"taskCreatedAt":"2025-11-14T18:19:33.701736325Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a0ovbs","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add mapConcurrentlyBounded helper using QSemN","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:20:20.979870628Z"} -{"taskCreatedAt":"2025-11-14T18:19:37.810028305Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a16ame","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Refactor build function to extract buildTarget worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:20:58.231039244Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.688391211Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1ddsb","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Replace forM with mapConcurrentlyBounded in build","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:20:58.290149792Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.716079624Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1dl5c","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test basic parallel builds without UI changes","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:31:57.019839638Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.744631636Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1dsvi","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Research ansi-terminal and design LineManager API","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:32:29.399532791Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.772108017Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1dzes","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Create Omni/Log/Concurrent.hs module with LineManager","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:33:02.794492847Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.800202144Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1dgy0","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement line reservation and release logic","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:33:02.855747669Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.82813327Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1doev","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement concurrent line update with ANSI codes","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:33:02.915807677Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.857123437Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1dvm5","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add terminal capability detection","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:33:02.975985146Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.886073324Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1e3j1","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Thread LineManager through build/nixBuild functions","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:38:03.516198105Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.914626247Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1eajy","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Create runWithLineManager and logsToLine functions","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T18:55:54.836022471Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.94320795Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1eiay","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test parallel builds with ANSI multi-line output","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:01:40.850177474Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.971879353Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1epcz","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add fallback for dumb terminals","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:02:18.706108207Z"} -{"taskCreatedAt":"2025-11-14T18:19:45.999699368Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1ewrh","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test in emacs and narrow terminals","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:02:18.766470937Z"} -{"taskCreatedAt":"2025-11-14T18:19:46.028016768Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1eeer","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Handle edge cases and polish UX","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:02:18.827147429Z"} -{"taskCreatedAt":"2025-11-14T18:19:46.056655181Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1a1elgl","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update documentation","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:02:47.319855049Z"} -{"taskCreatedAt":"2025-11-16T04:06:48.014952363Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ga8v8o","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"PodcastItLater: Public Feed, Metrics & Audio Improvements","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:57:42.45932002Z"} -{"taskCreatedAt":"2025-11-16T04:06:57.071621037Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gakvc7","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add database migrations for new columns (is_public, user_episodes table, episode_metrics table, original_url_hash)","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:09:27.336080901Z"} -{"taskCreatedAt":"2025-11-16T04:06:57.609993104Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ganbfx","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement URL hashing and normalization function for episode deduplication","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:09:27.896576613Z"} -{"taskCreatedAt":"2025-11-16T04:06:58.132246645Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gapn6z","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add Core.py database functions for public episodes (mark_public, unmark_public, get_public_episodes)","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:09:28.463907822Z"} -{"taskCreatedAt":"2025-11-16T04:06:58.665794496Z","taskDependencies":[],"taskDescription":null,"taskId":"t-garbua","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add Core.py database functions for user_episodes junction table","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:09:29.027348473Z"} -{"taskCreatedAt":"2025-11-16T04:06:59.199139475Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gatqev","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add Core.py database functions for episode metrics tracking","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:09:29.602931183Z"} -{"taskCreatedAt":"2025-11-16T04:07:07.307576303Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gbrs2a","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Modify submission flow to check for existing episodes by URL hash","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:13:56.073214768Z"} -{"taskCreatedAt":"2025-11-16T04:07:07.834181871Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gbu51o","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add /public route to display public feed","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:16:43.926763164Z"} -{"taskCreatedAt":"2025-11-16T04:07:08.369657826Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gbwkkw","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add /public.rss route for public RSS feed generation","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:16:44.383466957Z"} -{"taskCreatedAt":"2025-11-16T04:07:08.906237761Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gbyzv2","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update home page to show public feed when user is logged out","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:16:44.848713835Z"} -{"taskCreatedAt":"2025-11-16T04:07:09.433392796Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gban3x","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add admin toggle button to episode cards for public/private status","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:13:58.676381973Z"} -{"taskCreatedAt":"2025-11-16T04:07:17.092115521Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gc6vrk","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add POST /admin/episode/{id}/toggle-public endpoint","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:13:58.727479053Z"} -{"taskCreatedAt":"2025-11-16T04:07:17.6266109Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gc9aud","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add '+ Add to your feed' button on episode pages for logged-in users","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:22:35.253656788Z"} -{"taskCreatedAt":"2025-11-16T04:07:18.165342861Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gcbqdl","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add POST /episode/{id}/add-to-feed endpoint","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:22:35.305050805Z"} -{"taskCreatedAt":"2025-11-16T04:07:18.700573408Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gcdfsb","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add POST /episode/{id}/track endpoint for metrics tracking","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:30:51.238117273Z"} -{"taskCreatedAt":"2025-11-16T04:07:19.229153372Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gcftng","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add JavaScript to episode player for tracking play events","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:30:51.289470508Z"} -{"taskCreatedAt":"2025-11-16T04:07:27.174644219Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gcnemk","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Enhance Worker.py to extract publication date and author metadata from articles","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.196162661Z"} -{"taskCreatedAt":"2025-11-16T04:07:27.700527081Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gcpraj","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add intro TTS generation with metadata (title, author, date)","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.247694148Z"} -{"taskCreatedAt":"2025-11-16T04:07:28.221004581Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gcrczw","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add outro TTS generation with title and author","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.298838151Z"} -{"taskCreatedAt":"2025-11-16T04:07:28.74867703Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gctpqn","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Combine intro, pauses, article content, and outro in Worker.py","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.360155369Z"} -{"taskCreatedAt":"2025-11-16T04:07:29.289653388Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gcw6zn","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Write tests for public feed functionality","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.410867588Z"} -{"taskCreatedAt":"2025-11-16T04:07:35.447349966Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gdlwtu","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Write tests for episode deduplication","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.461656748Z"} -{"taskCreatedAt":"2025-11-16T04:07:35.995113703Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gdoeyo","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Write tests for metrics tracking","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.513956262Z"} -{"taskCreatedAt":"2025-11-16T04:07:36.52315156Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gdqsl7","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Write tests for audio intro/outro generation","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.574397661Z"} -{"taskCreatedAt":"2025-11-16T04:07:37.059671738Z","taskDependencies":[],"taskDescription":null,"taskId":"t-gdshua","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Create admin metrics dashboard view","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:57:35.681938898Z"} -{"taskCreatedAt":"2025-11-20T15:04:38.423818806Z","taskDependencies":[],"taskDescription":null,"taskId":"t-xfkjyy","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add JSON output flag","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T15:07:40.861538248Z"} -{"taskCreatedAt":"2025-11-20T15:07:33.14012157Z","taskDependencies":[],"taskDescription":null,"taskId":"t-xr9pfs","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test JSON output","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T15:07:41.093795231Z"} -{"taskCreatedAt":"2025-11-20T15:14:01.809791032Z","taskDependencies":[],"taskDescription":null,"taskId":"t-xrsdzb","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add comprehensive CLI tests for task command","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T15:14:09.958477456Z"} -{"taskCreatedAt":"2025-11-20T15:25:13.591317838Z","taskDependencies":[],"taskDescription":null,"taskId":"t-yavn30","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add priority flag support to task create","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T16:00:44.985924365Z"} -{"taskCreatedAt":"2025-11-20T15:25:27.424518009Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ybrphe","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"CLI parsing fails with multiple flags","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T15:58:11.990663284Z"} -{"taskCreatedAt":"2025-11-20T15:25:27.720568105Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ybseie","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Namespace filter broken","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T15:58:12.269456015Z"} -{"taskCreatedAt":"2025-11-20T15:25:27.948491266Z","taskDependencies":[],"taskDescription":null,"taskId":"t-ybtc0p","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Discovered-from flag broken","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T15:58:12.685064773Z"} -{"taskCreatedAt":"2025-11-20T15:58:11.740041636Z","taskDependencies":[],"taskDescription":null,"taskId":"t-10knttf","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Docopt flag order matters incorrectly","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T18:35:44.798128524Z"} -{"taskCreatedAt":"2025-11-20T18:44:29.330834039Z","taskDependencies":[{"depId":"t-uumhrq","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-1be2r3q","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Document TASK_TEST_MODE in AGENTS.md","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T18:53:22.852670919Z"} -{"taskCreatedAt":"2025-11-20T19:46:53.636713383Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1fjra3k","taskNamespace":"Omni/Bild.hs","taskParent":null,"taskPriority":"P1","taskStatus":"Done","taskTitle":"Fix bild --plan to output only JSON without logging","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T19:51:46.854882315Z"} -{"taskCreatedAt":"2025-11-20T21:41:12.7461675Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1nddhlo","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143kql2","taskPriority":"P2","taskStatus":"Done","taskTitle":"PodcastItLater: Add Pricing Page UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T00:25:09.131891321Z"} -{"taskCreatedAt":"2025-11-20T21:41:12.764720659Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1nddmad","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143kql2","taskPriority":"P2","taskStatus":"Done","taskTitle":"PodcastItLater: Add Stripe Checkout Route","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:16:02.758048988Z"} -{"taskCreatedAt":"2025-11-20T21:41:12.783999704Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1nddrba","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143kql2","taskPriority":"P2","taskStatus":"Done","taskTitle":"PodcastItLater: Add Stripe Portal Route","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:16:02.82972272Z"} -{"taskCreatedAt":"2025-11-20T21:41:12.802988426Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1nddwxq","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143kql2","taskPriority":"P2","taskStatus":"Done","taskTitle":"PodcastItLater: Add Stripe Webhook Handler","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:16:02.911223697Z"} -{"taskCreatedAt":"2025-11-20T21:41:12.821995769Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1nddbuq","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143kql2","taskPriority":"P2","taskStatus":"Done","taskTitle":"PodcastItLater: Enforce Paid Limits in UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:16:02.993133469Z"} -{"taskCreatedAt":"2025-11-20T21:41:32.113815607Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1newyao","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-144hcmj","taskPriority":"P2","taskStatus":"Done","taskTitle":"Add tests for Admin dashboard","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:22:55.020324428Z"} -{"taskCreatedAt":"2025-11-20T21:41:32.132888832Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1newd8r","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-144hcmj","taskPriority":"P2","taskStatus":"Done","taskTitle":"Add error handling tests for Worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:22:55.103182521Z"} -{"taskCreatedAt":"2025-11-20T22:42:03.728732682Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1rcir6x","taskNamespace":"Omni/Task.hs","taskParent":"t-ppxwsu","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement 'task progress ' command","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:59:47.987586572Z"} -{"taskCreatedAt":"2025-11-20T22:42:03.748273499Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1rciwc8","taskNamespace":"Omni/Task.hs","taskParent":"t-ppxwsu","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement 'task stats --epic=' filtering","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:02:43.362372647Z"} -{"taskCreatedAt":"2025-11-20T22:42:03.767665854Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1rcibeu","taskNamespace":"Omni/Task.hs","taskParent":"t-ppxwsu","taskPriority":"P2","taskStatus":"Done","taskTitle":"Add colored output to 'task list' and 'task tree'","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T11:21:58.208142783Z"} -{"taskCreatedAt":"2025-11-20T22:42:18.766787128Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1rdjxcd","taskNamespace":"Omni/Task.hs","taskParent":"t-ppxwsu","taskPriority":"P2","taskStatus":"Done","taskTitle":"Namespace normalization incorrect for Haskell files ending in .hs","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T11:27:04.388679271Z"} -{"taskCreatedAt":"2025-11-20T22:42:37.706495845Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1rf10ho","taskNamespace":"Biz/PodcastItLater/hs.hs","taskParent":"t-143kql2","taskPriority":"P3","taskStatus":"Done","taskTitle":"Research and add intro/outro sound effects","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:22:55.185034515Z"} -{"taskCreatedAt":"2025-11-20T22:42:37.725796962Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1rf15ih","taskNamespace":"Biz/PodcastItLater/hs.hs","taskParent":"t-143kql2","taskPriority":"P3","taskStatus":"Done","taskTitle":"Implement audio crossfading for intro/outro","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:22:55.265928659Z"} -{"taskCreatedAt":"2025-11-20T23:17:30.579211649Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1tweu4w","taskNamespace":"Omni/Agent/hs.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Multi-Agent System 2.0","taskType":"Epic","taskUpdatedAt":"2025-11-21T09:11:58.668761493Z"} -{"taskCreatedAt":"2025-11-20T23:17:39.613719647Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1txgomo","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1tweu4w","taskPriority":"P2","taskStatus":"Done","taskTitle":"Design Omni/Agent.hs CLI and module structure","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:11:58.730191261Z"} -{"taskCreatedAt":"2025-11-20T23:17:39.632912633Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1txgtmn","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1tweu4w","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement worker process management (start/stop/pid)","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:11:58.792225554Z"} -{"taskCreatedAt":"2025-11-20T23:17:39.651751765Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1txgyge","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1tweu4w","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement git worktree and sync logic in Haskell","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:11:58.864032147Z"} -{"taskCreatedAt":"2025-11-20T23:17:39.670723428Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1txgdcd","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1tweu4w","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement log streaming and filtering (replace monitor-worker.sh)","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:11:58.924913379Z"} -{"taskCreatedAt":"2025-11-20T23:17:39.689755832Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1txgi9c","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1tweu4w","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement harvesting logic in Haskell","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:11:58.996256086Z"} -{"taskCreatedAt":"2025-11-20T23:17:39.708649865Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1txgn3w","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1tweu4w","taskPriority":"P2","taskStatus":"Done","taskTitle":"Add integration tests for Agent workflow","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:11:59.057748775Z"} -{"taskCreatedAt":"2025-11-20T23:51:02.843631362Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1vipjyg","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"PodcastItLater: UX Polish","taskType":"Epic","taskUpdatedAt":"2025-11-20T23:51:02.843631362Z"} -{"taskCreatedAt":"2025-11-21T00:19:08.811498926Z","taskDependencies":[{"depId":"t-ppyzt2","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-1fkilh","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"bild fails in agent environment due to CODEROOT mismatch","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T11:29:06.373587499Z"} -{"taskCreatedAt":"2025-11-21T00:19:08.829956304Z","taskDependencies":[{"depId":"t-ppyzt2","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-1fkn9o","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Race condition in generateChildId when concurrent tasks are created","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T11:31:03.502746068Z"} -{"taskCreatedAt":"2025-11-21T02:31:40.268267384Z","taskDependencies":[],"taskDescription":null,"taskId":"t-9vrnuj","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove horizontal bars from task show output","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T02:32:57.50736116Z"} -{"taskCreatedAt":"2025-11-21T02:39:47.740342035Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwa5yilwm","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Multi-Agent System 2.0 (Haskell Agent)","taskType":"Epic","taskUpdatedAt":"2025-11-21T11:20:44.580140023Z"} -{"taskCreatedAt":"2025-11-21T02:39:51.467615692Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwa5yilwm.1","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwa5yilwm","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement Omni.Agent.Git module with robust checkout","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:22:18.728481212Z"} -{"taskCreatedAt":"2025-11-21T02:39:55.225849981Z","taskDependencies":[{"depId":"t-rwa5yilwm.1","depType":"Blocks"}],"taskDescription":null,"taskId":"t-rwa5yilwm.2","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwa5yilwm","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement Omni.Agent.Worker loop logic","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T10:23:29.636375257Z"} -{"taskCreatedAt":"2025-11-21T02:39:58.185671478Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwa5yilwm.3","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwa5yilwm","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement Omni.Agent.Log module with Aeson parsing","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:23:04.905183947Z"} -{"taskCreatedAt":"2025-11-21T02:40:01.165180998Z","taskDependencies":[{"depId":"t-rwa5yilwm.2","depType":"Blocks"}],"taskDescription":null,"taskId":"t-rwa5yilwm.4","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwa5yilwm","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement Omni.Agent.CLI entry point","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T11:20:16.539667879Z"} -{"taskCreatedAt":"2025-11-21T02:49:51.7176629Z","taskDependencies":[{"depId":"t-rwa5yilwm.3","depType":"Blocks"}],"taskDescription":null,"taskId":"t-rwa5yilwm.5","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwa5yilwm","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement enhanced 2-line status logging","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T10:32:44.812735182Z"} -{"taskCreatedAt":"2025-11-21T02:59:12.848135132Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwa6p91hx","taskNamespace":"Omni/Ide.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix gitlint regex-style-search warning","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T02:59:42.296704815Z"} -{"taskCreatedAt":"2025-11-21T03:12:57.890285833Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwa7iyorq","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Parent Epic","taskType":"Epic","taskUpdatedAt":"2025-11-21T09:11:53.722815626Z"} -{"taskCreatedAt":"2025-11-21T03:13:01.031231982Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwa7iyorq.1","taskNamespace":null,"taskParent":"t-rwa7iyorq","taskPriority":"P2","taskStatus":"Done","taskTitle":"Child Task","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:11:53.783593127Z"} -{"taskCreatedAt":"2025-11-21T04:09:41.699239296Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwabrkqdq","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix task ready to exclude Review tasks","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T04:10:49.102675623Z"} -{"taskCreatedAt":"2025-11-21T04:30:05.792313193Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwacmb1av","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Make task IDs case-insensitive","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:45:08.327277718Z"} -{"taskCreatedAt":"2025-11-13T19:38:07.804316976Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1f9qp23","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"General Code Quality Refactor","taskType":"Epic","taskUpdatedAt":"2025-11-13T19:38:07.804316976Z"} -{"taskCreatedAt":"2025-11-20T21:41:20.029426381Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1ne7qtj","taskNamespace":"Network/Wai/Middleware/Braid.hs","taskParent":"t-1f9qp23","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement Braid keep-alive mechanism","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:34:29.80017783Z"} -{"taskCreatedAt":"2025-11-20T21:41:20.048368004Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1ne7voo","taskNamespace":"Biz/Que/Host.hs","taskParent":"t-1f9qp23","taskPriority":"P2","taskStatus":"Done","taskTitle":"Revive authkey authentication in Que/Host","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:23:49.928716814Z"} -{"taskCreatedAt":"2025-11-20T21:41:20.067644599Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1ne80pj","taskNamespace":"Biz/Dragons.hs","taskParent":"t-1f9qp23","taskPriority":"P2","taskStatus":"Done","taskTitle":"Store generated JWK in persistent file","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:54:17.655700806Z"} -{"taskCreatedAt":"2025-11-21T22:31:08.234511588Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwblzndp4","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Improve Worker Autonomy","taskType":"Epic","taskUpdatedAt":"2025-11-22T03:14:43.299045498Z"} -{"taskCreatedAt":"2025-11-21T22:31:12.793338417Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwblzndp4.1","taskNamespace":null,"taskParent":"t-rwblzndp4","taskPriority":"P2","taskStatus":"Done","taskTitle":"Add description field to Task model","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T22:32:25.035499529Z"} -{"taskCreatedAt":"2025-11-21T22:31:17.536649882Z","taskDependencies":[{"depId":"t-rwblzndp4.1","depType":"Blocks"}],"taskDescription":null,"taskId":"t-rwblzndp4.2","taskNamespace":null,"taskParent":"t-rwblzndp4","taskPriority":"P2","taskStatus":"Done","taskTitle":"Update CLI to support task description","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T22:34:14.436031595Z"} -{"taskCreatedAt":"2025-11-21T22:31:20.872934097Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwblzndp4.3","taskNamespace":null,"taskParent":"t-rwblzndp4","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement smart base branch selection in Worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T22:36:36.614180518Z"} -{"taskCreatedAt":"2025-11-21T23:01:48.224051611Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwbnajcjh","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update start-worker.sh to use Haskell agent","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T01:34:02.545292575Z"} -{"taskCreatedAt":"2025-11-22T01:34:07.407341455Z","taskDependencies":[],"taskDescription":"Omni/Bild.hs:776 has a TODO: wrapper should just be removed, instead rely on upstream nixpkgs builders to make wrappers. This simplifies the codebase by removing manual bash script generation.","taskId":"t-rwbmpcv4v","taskNamespace":"Omni/Bild.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove manual wrapper generation in Omni/Bild","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T03:21:49.357422745Z"} -{"taskCreatedAt":"2025-11-22T01:34:12.233596517Z","taskDependencies":[],"taskDescription":"Implement a metrics view in the Admin dashboard (Biz/PodcastItLater/Admin.py). Show total users, active subscriptions, and recent submission counts. Ref: Biz/PodcastItLater/DESIGN.md","taskId":"t-rwbmpxabk","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement metrics view in Admin dashboard","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:30:02.510477593Z"} -{"taskCreatedAt":"2025-11-22T01:34:19.451799517Z","taskDependencies":[],"taskDescription":"Update Omni/Agent/start-worker.sh to invoke the new Haskell-based agent binary ('agent start ') instead of running the legacy bash loop. Ensure it still sets up the environment correctly. The agent binary handles the loop internally.","taskId":"t-rwbmq1snx","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update start-worker.sh to use Haskell agent","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T01:57:09.161716208Z"} -{"taskCreatedAt":"2025-11-22T02:13:44.805917094Z","taskDependencies":[],"taskDescription":"Modify Omni/Agent/Git.hs to proactively clean up stale rebase/merge states before attempting operations. The worker should attempt 'git rebase --abort' (ignoring errors) before syncing to prevent 'already rebase-merge' errors.","taskId":"t-rwbp06f2o","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Make worker agent robust to stale git states","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T02:14:40.413090556Z"} -{"taskCreatedAt":"2025-11-22T02:26:44.02456019Z","taskDependencies":[],"taskDescription":"Modify Omni/Agent/Git.hs to check for .git/rebase-merge or .git/rebase-apply before running git rebase --abort. This avoids blindly running abort commands.","taskId":"t-rwbpqplps","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Detect in-progress rebase before aborting in Agent","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T02:27:45.377866012Z"} -{"taskCreatedAt":"2025-11-22T03:01:36.84628158Z","taskDependencies":[],"taskDescription":"Modify Omni/Agent/Worker.hs to check if the task branch already exists before trying to create it. If it exists, simply checkout the branch. This prevents 'fatal: a branch named ... already exists' errors when restarting the worker.","taskId":"t-rwbs8t1wv","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Handle existing task branch in Worker Agent","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T03:02:31.746506652Z"} -{"taskCreatedAt":"2025-11-22T03:09:54.022974779Z","taskDependencies":[],"taskDescription":"Implement the 2-line status UI described in Omni/Agent/DESIGN.md (Section 4.3). It should reserve 2 lines at the bottom for Meta (Task ID, Time) and Activity (current thought/action), allowing history to scroll above. Use ANSI codes for cursor management.","taskId":"t-rwbsg78jq","taskNamespace":"Omni/Agent/Log.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement 2-line Agent Status UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T03:21:54.480763142Z"} -{"taskCreatedAt":"2025-11-22T11:31:50.378377038Z","taskDependencies":[],"taskDescription":"Test that lowercase task ids are accepted and do not clash with old tasks.","taskId":"t-rwcpygi7d","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test Lowercase","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T18:52:36.983207381Z"} -{"taskCreatedAt":"2025-11-22T11:34:17.854509264Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwcpif5ov","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"--help","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:39:43.304029721Z"} -{"taskCreatedAt":"2025-11-22T04:02:16.914288868Z","taskDependencies":[{"depId":"t-rwbmpcv4v","depType":"Blocks"},{"depId":"t-rwbmpxabk","depType":"Blocks"},{"depId":"t-rwbs8t1wv","depType":"Blocks"}],"taskDescription":"Update Omni/Agent/Worker.hs to spawn a background thread that tails '_/llm/amp.log' while the Amp agent is running. For each new line in the log: 1. Parse it (it's JSON). 2. Extract a user-friendly summary (e.g. 'Thinking...', 'Tool: Bash'). 3. Update the status bar activity line (AgentLog.updateActivity) with this summary. This provides real-time visibility into what the agent is doing.","taskId":"t-rwbw6onuo","taskNamespace":"Omni/Agent/Worker.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Stream Amp logs to Agent status bar","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:05:14.217613978Z"} -{"taskCreatedAt":"2025-11-22T09:41:06.786529414Z","taskDependencies":[],"taskDescription":"Replace 'git rebase live' with 'git sync' (which maps to git-branchless sync) in Omni.Agent.Git.syncWithLive. This aligns with the branchless workflow and handles stack rebasing automatically.","taskId":"t-rwciiesnz","taskNamespace":"Omni/Agent/Git.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Use 'git sync' instead of 'git rebase' in Agent","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T09:42:37.875643446Z"} -{"taskCreatedAt":"2025-11-22T09:50:59.154884329Z","taskDependencies":[],"taskDescription":"1. Add Thread ID to the status bar (requires log parsing later, but add field now). 2. Make the status layout responsive or vertical (4 lines) to fit on small screens (iPhone). 3. Reserve more lines in init.","taskId":"t-rwciwjysi","taskNamespace":"Omni/Agent/Log.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Improve Agent Status UI for mobile & debugging","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T09:52:36.176467065Z"} -{"taskCreatedAt":"2025-11-22T10:09:23.249166289Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwck9sdoa","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Split Thread and Credits in Worker status bar","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:10:17.800528662Z"} -{"taskCreatedAt":"2025-11-22T10:12:35.129294132Z","taskDependencies":[{"depId":"t-rwck9sdoa","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-rwckmrkbm","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix Worker status bar activity not updating","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:14:43.612634394Z"} -{"taskCreatedAt":"2025-11-22T10:24:04.441689132Z","taskDependencies":[{"depId":"t-rwckmrkbm","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-rwcl762fd","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix credit calculation in Worker status bar","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:25:51.468062833Z"} -{"taskCreatedAt":"2025-11-22T10:32:31.370216711Z","taskDependencies":[],"taskDescription":"Map raw Amp log messages to human-friendly status updates (e.g. 'READ: ...', 'TOOL: ...'), similar to monitor-worker.sh, but WITHOUT using emojis as they are unnecessary.","taskId":"t-rwclfp3vn","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Improve Worker status bar activity formatting (No Emojis)","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T13:40:28.250551154Z"} -{"taskCreatedAt":"2025-11-22T10:35:13.559736706Z","taskDependencies":[{"depId":"t-rwcl762fd","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-rwclqnapm","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Verify credit units in amp logs","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:41:51.876980566Z"} -{"taskCreatedAt":"2025-11-22T10:41:55.215833393Z","taskDependencies":[],"taskDescription":"The credits in usage-ledger logs are in cents, but we display them as dollars. We need to divide by 100.","taskId":"t-rwcmhytvv","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Divide usage-ledger credits by 100 to get dollars","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:42:42.156523503Z"} -{"taskCreatedAt":"2025-11-22T10:50:50.329217484Z","taskDependencies":[],"taskDescription":"Collection of tasks to improve the robustness of the codebase (builds), the usability of the 'task' tool, and the accuracy of the agent's status reporting.","taskId":"t-rwcmrmawx","taskNamespace":"Omni.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Codebase Health and Tooling Improvements","taskType":"Epic","taskUpdatedAt":"2025-11-22T10:50:50.329217484Z"} -{"taskCreatedAt":"2025-11-22T10:50:57.552875891Z","taskDependencies":[],"taskDescription":"Implement a 'task edit ' command (or 'task update' extension) that allows modifying a task's title, description, priority, and other fields in-place. Currently 'task update' only changes status.","taskId":"t-rwcmrmawx.1","taskNamespace":"Omni/Task.hs","taskParent":"t-rwcmrmawx","taskPriority":"P2","taskStatus":"Done","taskTitle":"Add 'task edit' command","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:16:49.365516683Z"} -{"taskCreatedAt":"2025-11-22T10:51:01.309897479Z","taskDependencies":[],"taskDescription":"Update the Worker Agent status bar logic to round the displayed credit usage to 2 decimal places (nearest cent). Currently it may show long floating point numbers.","taskId":"t-rwcmrmawx.2","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwcmrmawx","taskPriority":"P2","taskStatus":"Done","taskTitle":"Round credits to nearest cent in Agent status","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T13:40:28.332806178Z"} -{"taskCreatedAt":"2025-11-22T10:51:04.73629995Z","taskDependencies":[],"taskDescription":"Update Omni/Task/Core.hs to handle task IDs case-insensitively for lookups and normalize them to lowercase when storing/creating. This improves user experience when typing IDs manually.","taskId":"t-rwcmrmawx.3","taskNamespace":"Omni/Task.hs","taskParent":"t-rwcmrmawx","taskPriority":"P2","taskStatus":"Done","taskTitle":"Case-insensitive task IDs","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:54:45.741575622Z"} -{"taskCreatedAt":"2025-11-22T10:51:08.813653444Z","taskDependencies":[],"taskDescription":"Create an agent or script that iterates through every namespace in the project and runs 'bild' (e.g. 'bild --time 0 **/*'). For every build failure encountered, it should automatically create a new task with the error details and link it to this epic (or the discovery context).","taskId":"t-rwcmrmawx.4","taskNamespace":"Omni/Bild.hs","taskParent":"t-rwcmrmawx","taskPriority":"P2","taskStatus":"Done","taskTitle":"Audit codebase builds and file repair tasks","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:51:55.014259557Z"} -{"taskCreatedAt":"2025-11-22T11:27:59.621730567Z","taskDependencies":[],"taskDescription":"Update Omni/Agent/Worker.hs to read the content of AGENTS.md and include a relevant summary or the full content in the initial system prompt provided to the Amp agent. This ensures the worker knows about repository conventions, testing standards, and tool usage.","taskId":"t-rwcpie3lo","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Include AGENTS.md context in Worker initial prompt","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:36:14.542146518Z"} -{"taskCreatedAt":"2025-11-22T11:45:43.502171517Z","taskDependencies":[],"taskDescription":"Remove unused test files, migrate useful tests to the main suite, and remove legacy bash prototype scripts replaced by the Haskell implementation.","taskId":"t-rwcqsdzfm","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Cleanup Omni/Agent files and tests","taskType":"Epic","taskUpdatedAt":"2025-11-22T20:37:04.443006039Z"} -{"taskCreatedAt":"2025-11-22T11:45:49.548163416Z","taskDependencies":[],"taskDescription":"Omni/Agent/LogTest.hs is currently unused by the main 'bild --test Omni/Agent.hs' command. Review its contents, move any valuable tests to Omni/Agent.hs (or Omni/Agent/Log.hs's test section), and delete the file.","taskId":"t-rwcqsdzfm.1","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwcqsdzfm","taskPriority":"P2","taskStatus":"Done","taskTitle":"Consolidate LogTest.hs into main test suite","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T19:48:09.779433932Z"} -{"taskCreatedAt":"2025-11-22T11:45:57.926946967Z","taskDependencies":[],"taskDescription":"Remove bash scripts that have been superseded by the Haskell agent implementation. Candidates for removal: harvest-tasks.sh, merge-tasks.sh, sync-tasks.sh, setup-worker.sh. Ensure functionality is covered by Haskell code before deletion.","taskId":"t-rwcqsdzfm.2","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwcqsdzfm","taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove legacy bash prototype scripts","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T18:54:20.854014849Z"} -{"taskCreatedAt":"2025-11-22T11:46:03.875940421Z","taskDependencies":[],"taskDescription":"We have both 'monitor.sh' and 'monitor-worker.sh'. Consolidate them into a single 'monitor.sh' script and remove the duplicate.","taskId":"t-rwcqsdzfm.3","taskNamespace":"Omni/Agent.hs","taskParent":"t-rwcqsdzfm","taskPriority":"P2","taskStatus":"Done","taskTitle":"Consolidate monitor scripts","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T19:48:10.034617175Z"} -{"taskCreatedAt":"2025-11-22T12:42:35.9228659Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2bk9tzanj","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Capture Amp summary for commit message","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T12:48:02.872211474Z"} -{"taskCreatedAt":"2025-11-22T12:42:39.927855226Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2bk9wd4x9","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update Amp prompt to forbid git commits","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T12:48:07.355031023Z"} -{"taskCreatedAt":"2025-11-22T12:57:29.984013645Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2bkoma4nf","taskNamespace":"Omni.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Update AGENTS.md with commit message guidelines","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T12:59:00.994108608Z"} -{"taskCreatedAt":"2025-11-22T12:57:52.859363726Z","taskDependencies":[{"depId":"t-1o2bkoma4nf","depType":"Related"}],"taskDescription":null,"taskId":"t-1o2bkozwfdt","taskNamespace":"Omni.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Configure git commit template (.gitmessage)","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T12:59:25.14786599Z"} -{"taskCreatedAt":"2025-11-22T13:01:18.426816879Z","taskDependencies":[],"taskDescription":"Update repository setup scripts (e.g. Omni/Ide/hooks or task init) to automatically run 'git config commit.template .gitmessage' so all users get the template.","taskId":"t-1o2bkseag8u","taskNamespace":"Omni/Ide.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Automate git commit template configuration","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T18:56:46.2904441Z"} -{"taskCreatedAt":"2025-11-22T13:03:21.434586142Z","taskDependencies":[],"taskDescription":"Move detailed documentation (Task Manager, Bild, Git Workflow) to separate README files in their respective namespaces. Keep AGENTS.md focused on critical rules, cheat sheets, and pointers to the detailed docs. Goal is to reduce token usage.","taskId":"t-1o2bkufixnc","taskNamespace":"Omni.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Refactor and condense AGENTS.md","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:30:14.384583642Z"} -{"taskCreatedAt":"2025-11-21T04:37:55.163249193Z","taskDependencies":[{"depId":"t-144gqry","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-rwadhwrzt","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix bild failure for Biz/PodcastItLater/Web.py","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:32:24.679826325Z"} -{"taskCreatedAt":"2025-11-21T05:28:31.973657907Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwagbsb6w","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Add error handling tests for Worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:40:59.255645021Z"} -{"taskCreatedAt":"2025-11-22T10:39:11.364170862Z","taskDependencies":[{"depId":"t-rwbmpxabk","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-rwcm6todb","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix failing tests in Biz/PodcastItLater/Web.py (UsageLimits and EpisodeDetail)","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T14:32:24.762100815Z"} -{"taskCreatedAt":"2025-11-22T20:37:09.166630362Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2bxcq7999","taskNamespace":"Omni/Workflow.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Phase 1: Foundations (Task & CI)","taskType":"Epic","taskUpdatedAt":"2025-11-22T22:11:39.261315992Z"} -{"taskCreatedAt":"2025-11-22T20:37:13.980489314Z","taskDependencies":[],"taskDescription":"Configure .gitattributes and .git/config (via Omni/Ide/hooks or setup) to use 'agent merge-driver' for .tasks/tasks.jsonl. This prevents data loss when merging branches with divergent task lists.","taskId":"t-1o2bxcq7999.1","taskNamespace":"Omni/Ide.hs","taskParent":"t-1o2bxcq7999","taskPriority":"P0","taskStatus":"Done","taskTitle":"Configure git merge driver for tasks.jsonl","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T21:57:23.592078308Z"} -{"taskCreatedAt":"2025-11-22T20:37:18.719690905Z","taskDependencies":[],"taskDescription":"Update Task Core to include Approved status, update CLI to support it, update TaskStats, and fix any compilation errors. Reference plan: /home/ben/omni/_/llm/PLAN_Autonomous_Workflow.md","taskId":"t-1o2bxcq7999.2","taskNamespace":"Omni/Task.hs","taskParent":"t-1o2bxcq7999","taskPriority":"P1","taskStatus":"Done","taskTitle":"Add Approved status to Omni/Task","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T21:59:08.985299564Z"} -{"taskCreatedAt":"2025-11-22T20:37:23.378739333Z","taskDependencies":[],"taskDescription":"Rewrite Omni/Ci.sh into a robust Haskell program (Omni/Ci.hs). Reference plan: /home/ben/omni/_/llm/PLAN_Autonomous_Workflow.md","taskId":"t-1o2bxcq7999.3","taskNamespace":"Omni/Ci.hs","taskParent":"t-1o2bxcq7999","taskPriority":"P1","taskStatus":"Done","taskTitle":"Implement Omni/Ci.hs","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T22:01:09.779228442Z"} -{"taskCreatedAt":"2025-11-22T20:37:27.396872011Z","taskDependencies":[],"taskDescription":"The Time, Thread, and Credits fields in the agent status bar are not being populated. Update Omni/Agent/Log.hs to parse these fields from the JSON log output.","taskId":"t-1o2bxd11zv9","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P1","taskStatus":"Done","taskTitle":"Fix missing Time, Thread, and Credits in Agent Log","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T22:02:50.438643714Z"} -{"taskCreatedAt":"2025-11-22T20:37:31.615764727Z","taskDependencies":[],"taskDescription":"The 'task ready' command currently lists Epics. Update 'getReadyTasks' in Omni/Task/Core.hs to exclude tasks where taskType == Epic.","taskId":"t-1o2bxd3kezj","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P1","taskStatus":"Done","taskTitle":"Fix task ready to exclude Epics","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T22:08:41.720176373Z"} -{"taskCreatedAt":"2025-11-22T21:45:10.578083608Z","taskDependencies":[],"taskDescription":"Update Omni/Agent/start-worker.sh to run 'git sync' in the worker directory before building 'task' and 'agent'. This ensures the worker has the latest tools and code from live.","taskId":"t-1o2bxcq7999.4","taskNamespace":"Omni/Agent.hs","taskParent":"t-1o2bxcq7999","taskPriority":"P1","taskStatus":"Done","taskTitle":"Sync worker repo in start-worker.sh","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T22:01:47.245671772Z"} -{"taskCreatedAt":"2025-11-22T21:19:54.675769476Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rwd249bi3","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test Approved Status","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T21:20:10.652509625Z"} -{"taskCreatedAt":"2025-11-23T00:24:33.85216903Z","taskDependencies":[],"taskDescription":"Add HumanTask to TaskType in Omni/Task/Core.hs. Update 'task ready' and 'Omni/Agent/Worker.hs' to exclude HumanTask. Update docs (Omni/Task/README.md, AGENTS.md) to explain HumanTask usage.","taskId":"t-1o2c9vazf64","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P1","taskStatus":"Done","taskTitle":"Add HumanTask type to Task system","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T00:37:37.189983777Z"} -{"taskCreatedAt":"2025-11-23T00:25:37.243000855Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2c9wcq3go","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P1","taskStatus":"Open","taskTitle":"PodcastItLater: Mailgun Integration","taskType":"Epic","taskUpdatedAt":"2025-11-23T00:25:37.243000855Z"} -{"taskCreatedAt":"2025-11-23T00:41:46.590529112Z","taskDependencies":[],"taskDescription":"Revert the agent status bar layout to use 5 vertical lines instead of 2 horizontal lines, as it is easier to read on small screens. Update Omni/Agent/Log.hs 'render' function and 'init' function (to reserve lines).","taskId":"t-1o2cacdulgn","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Restore vertical layout for Agent Status","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:29:32.049530208Z"} -{"taskCreatedAt":"2025-11-23T00:42:49.682439567Z","taskDependencies":[],"taskDescription":"Sign up for Mailgun, configure domain podcastitlater.com, setup DNS, verify domain, and generate API Key.","taskId":"t-1o2c9wcq3go.1","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-1o2c9wcq3go","taskPriority":"P2","taskStatus":"Done","taskTitle":"Setup Mailgun Infrastructure","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:30:32.204478766Z"} -{"taskCreatedAt":"2025-11-23T00:42:56.80437736Z","taskDependencies":[{"depId":"t-1o2c9wcq3go.1","depType":"Blocks"}],"taskDescription":"Implement Mailgun email sending in Biz/PodcastItLater/Mail.py. Use requests. Blocked by Setup Mailgun Infrastructure.","taskId":"t-1o2c9wcq3go.2","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-1o2c9wcq3go","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement Mailgun Client","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T16:49:48.009344483Z"} -{"taskCreatedAt":"2025-11-23T01:18:20.705021976Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2cbco62ly","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Build failed: Biz.nix - 1","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:32:59.957672612Z"} -{"taskCreatedAt":"2025-11-23T01:20:43.938765636Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2cbf1fzh2","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Build failed: Biz/PodcastItLater/Episode.py - 1","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:35:07.74267138Z"} -{"taskCreatedAt":"2025-11-23T01:21:11.642226289Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2cbfhxu5e","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Review","taskTitle":"Build failed: Biz/PodcastItLater/Test.py - 1","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T18:37:33.773584669Z"} -{"taskCreatedAt":"2025-11-23T01:21:53.713796565Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2cbg6zl25","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Build failed: Biz/PodcastItLater/UI.py - 1","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:35:26.517302875Z"} -{"taskCreatedAt":"2025-11-23T01:22:34.513743178Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2cbgva26h","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Build failed: Biz/PodcastItLater/Worker.py - 1","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:35:26.599046196Z"} -{"taskCreatedAt":"2025-11-23T01:32:43.559862931Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2cbqxw13j","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Build failed: pyproject.toml - ","taskType":"WorkTask","taskUpdatedAt":"2025-11-23T03:35:26.680640676Z"} -{"taskCreatedAt":"2025-11-23T01:40:20.696284164Z","taskDependencies":[{"depId":"t-1o2cbco62ly","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-1o2cbyi23kl","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Investigate why bild uses different source than workspace","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T19:13:58.53979977Z"} -{"taskCreatedAt":"2025-11-23T01:40:20.879380653Z","taskDependencies":[{"depId":"t-1o2cbco62ly","depType":"DiscoveredFrom"}],"taskDescription":null,"taskId":"t-1o2cbyi61hb","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix ruff formatting consistency in build environment","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T20:05:59.207163876Z"} -{"taskCreatedAt":"2025-11-24T19:28:02.443317007Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2efbs6lst","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Remove looping logic from agent worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T19:28:40.993778347Z"} -{"taskCreatedAt":"2025-11-24T19:28:07.348886747Z","taskDependencies":[],"taskDescription":"The agent credit usage is displayed as dollars when they should be displayed as cents. For example: it currently displays $50 when it should show $0.50","taskId":"t-1o2efbv3t8o","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Fix agent credit usage display (show as dollars)","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T19:29:46.525888911Z"} -{"taskCreatedAt":"2025-11-24T20:04:04.203336147Z","taskDependencies":[],"taskDescription":"Evolve the current task and agent tools into a unified 'jr' binary. This is MVP 1. Reference Plan: /home/ben/omni/_/llm/PLAN_Jr_Evolution.md","taskId":"t-1o2egbj8o0n","taskNamespace":"Omni/Jr.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"MVP 1: Unified Jr Interface","taskType":"Epic","taskUpdatedAt":"2025-11-24T20:09:03.829276845Z"} -{"taskCreatedAt":"2025-11-24T20:04:10.647799773Z","taskDependencies":[],"taskDescription":"Create the main entry point for the new 'jr' CLI (Omni/Jr.hs). This module should handle command-line argument parsing (using docopt) and dispatch to sub-commands. The main function should delegate to a Cli.main style handler. Reference Plan: /home/ben/omni/_/llm/PLAN_Jr_Evolution.md. Acceptance Criteria: Omni/Jr.hs module exists, jr --help prints usage info, supports subcommands: task, work, harvest, and main function delegates correctly.","taskId":"t-1o2egbj8o0n.1","taskNamespace":"Omni/Jr.hs","taskParent":"t-1o2egbj8o0n","taskPriority":"P2","taskStatus":"Review","taskTitle":"Create Omni/Jr.hs (Main entry point)","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T20:22:51.046120174Z"} -{"taskCreatedAt":"2025-11-24T20:04:10.849946307Z","taskDependencies":[],"taskDescription":"Integrate the existing Omni/Task functionality into jr. 'jr task ...' should behave exactly like the current 'task ...' command. Reference Plan: /home/ben/omni/_/llm/PLAN_Jr_Evolution.md. Acceptance Criteria: jr task list works, jr task create works, jr task show works, all Omni/Task commands accessible via jr task, reuses Omni.Task logic.","taskId":"t-1o2egbj8o0n.2","taskNamespace":"Omni/Jr.hs","taskParent":"t-1o2egbj8o0n","taskPriority":"P2","taskStatus":"Open","taskTitle":"Port task commands to jr task","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T20:08:57.975243724Z"} -{"taskCreatedAt":"2025-11-24T20:04:11.130368679Z","taskDependencies":[],"taskDescription":"Integrate the Omni/Agent worker functionality into jr. The command 'jr work' should replace 'agent start'. Reference Plan: /home/ben/omni/_/llm/PLAN_Jr_Evolution.md. Acceptance Criteria: jr work starts the worker loop (sync, claim task, work), reuses Omni.Agent.Worker logic.","taskId":"t-1o2egbj8o0n.3","taskNamespace":"Omni/Jr.hs","taskParent":"t-1o2egbj8o0n","taskPriority":"P2","taskStatus":"Open","taskTitle":"Port agent start to jr work","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T20:08:58.300887813Z"} -{"taskCreatedAt":"2025-11-24T20:04:11.422967354Z","taskDependencies":[],"taskDescription":"Integrate the harvesting logic into jr. 'jr harvest' should replace 'agent harvest'. Reference Plan: /home/ben/omni/_/llm/PLAN_Jr_Evolution.md. Acceptance Criteria: jr harvest scans worker branches and merges task updates, reuses Omni.Agent.Core/Git logic.","taskId":"t-1o2egbj8o0n.4","taskNamespace":"Omni/Jr.hs","taskParent":"t-1o2egbj8o0n","taskPriority":"P2","taskStatus":"Open","taskTitle":"Port agent harvest to jr harvest","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T20:08:58.581036298Z"} -{"taskCreatedAt":"2025-11-24T20:04:11.719466495Z","taskDependencies":[],"taskDescription":"Update the build system (Omni/Bild.hs and metadata in Omni/Jr.hs) to compile the new 'jr' executable. Reference Plan: /home/ben/omni/_/llm/PLAN_Jr_Evolution.md. Acceptance Criteria: bild Omni/Jr.hs produces a binary named 'jr' (via -- : out jr comment), the binary runs and executes commands.","taskId":"t-1o2egbj8o0n.5","taskNamespace":"Omni/Jr.hs","taskParent":"t-1o2egbj8o0n","taskPriority":"P2","taskStatus":"Open","taskTitle":"Update bild to compile jr","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T20:08:58.851738246Z"} -{"taskCreatedAt":"2025-11-24T20:29:46.422934385Z","taskDependencies":[],"taskDescription":"Currently, the worker checks out the worker-named branch (e.g., omni-worker-1) after finishing a task. This is unnecessary. The worker should simply finish the task (commit) and leave the repo in that state, ready for the next action (review/merge) or for the operator to inspect. Remove the 'Git.checkout repo base' call in Omni.Agent.Worker.","taskId":"t-1o2eh11fsj1","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P0","taskStatus":"Review","taskTitle":"Worker should not reset to worker branch after task completion","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T20:55:41.829168949Z"} -{"taskCreatedAt":"2025-11-24T20:29:46.662959298Z","taskDependencies":[],"taskDescription":"When the 'amp' subprocess completes, it provides a summary of the work done. Currently, Omni.Agent.Worker uses a generic commit message ('feat: implement '). Modify Omni.Agent.Worker to capture the output/summary from 'amp' and use it as the git commit message. You may need to adjust how 'amp' is invoked or read a specific output file if 'amp' writes the summary to disk.","taskId":"t-1o2eh11kww0","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P0","taskStatus":"Done","taskTitle":"Use Amp's exit summary for task commit message","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T21:03:36.520930608Z"} -{"taskCreatedAt":"2025-11-24T21:23:40.913601862Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2eiij6mgr","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Done","taskTitle":"Test SQLite migration","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T21:24:15.922082744Z"} -{"taskCreatedAt":"2025-11-24T21:24:06.492597003Z","taskDependencies":[],"taskDescription":null,"taskId":"t-1o2eiiyev9r","taskNamespace":null,"taskParent":null,"taskPriority":"P0","taskStatus":"Open","taskTitle":"Migrate task storage to SQLite","taskType":"WorkTask","taskUpdatedAt":"2025-11-24T21:24:06.492597003Z"} diff --git a/Omni/Bild/Deps/Haskell.nix b/Omni/Bild/Deps/Haskell.nix index 6930860..d714799 100644 --- a/Omni/Bild/Deps/Haskell.nix +++ b/Omni/Bild/Deps/Haskell.nix @@ -50,6 +50,7 @@ "servant-lucid" "servant-server" "split" + "sqlite-simple" "stm" "tasty" "tasty-hunit" diff --git a/Omni/Task.hs b/Omni/Task.hs index 653e5fe..85314ad 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -4,6 +4,7 @@ {-# LANGUAGE NoImplicitPrelude #-} -- : out task +-- : dep sqlite-simple -- : modified by benign worker module Omni.Task where @@ -19,7 +20,6 @@ import qualified Omni.Test as Test import qualified System.Console.Docopt as Docopt import System.Directory (doesFileExist, removeFile) import System.Environment (setEnv) -import System.Process (callCommand) import qualified Test.Tasty as Tasty import Prelude (read) @@ -52,9 +52,8 @@ Usage: task tree [] [--json] task progress [--json] task stats [--epic=] [--json] - task export [--flush] + task export [-o ] task import -i - task sync task test task (-h | --help) @@ -70,9 +69,8 @@ Commands: tree Show task tree (epics with children, or all epics if no ID given) progress Show progress for an epic stats Show task statistics - export Export and consolidate tasks to JSONL + export Export tasks to JSONL import Import tasks from JSONL file - sync Export and commit tasks to git (does NOT push) test Run tests Options: @@ -88,10 +86,12 @@ Options: --discovered-from= Shortcut for --deps= --dep-type=discovered-from --namespace= Optional namespace (e.g., Omni/Task, Biz/Cloud) --description= Task description + --db= Path to SQLite database (overrides TASK_DB_PATH) --flush Force immediate export --json Output in JSON format (for agent use) --quiet Non-interactive mode (for agents) -i Input file for import + -o Output file for export Arguments: Task title @@ -113,14 +113,20 @@ outputSuccess :: Text -> IO () outputSuccess msg = outputJson <| Aeson.object ["success" Aeson..= True, "message" Aeson..= msg] move :: Cli.Arguments -> IO () -move args +move args = do + -- Handle --db flag globally + for_ + (Cli.getArg args (Cli.longOption "db")) + (setEnv "TASK_DB_PATH") + + move' args + +move' :: Cli.Arguments -> IO () +move' args | args `Cli.has` Cli.command "init" = do let quiet = args `Cli.has` Cli.longOption "quiet" initTaskDb - callCommand "git config commit.template .gitmessage" - callCommand "git config merge.agent.name 'Agent Merge Driver' || true" - callCommand "git config merge.agent.driver 'agent merge-driver %A %B' || true" - unless quiet <| putText "Task database initialized and configured. Use 'task create' to add tasks." + unless quiet <| putText "Task database initialized. Use 'task create' to add tasks." | args `Cli.has` Cli.command "create" = do title <- getArgText args "title" taskType <- case Cli.getArg args (Cli.longOption "type") of @@ -353,8 +359,13 @@ move args outputJson stats else showTaskStats maybeEpic | args `Cli.has` Cli.command "export" = do - exportTasks - putText "Exported and consolidated tasks to .tasks/tasks.jsonl" + file <- case Cli.getArg args (Cli.shortOption 'o') of + Nothing -> pure Nothing + Just f -> pure (Just f) + exportTasks file + case file of + Just f -> putText <| "Exported tasks to " <> T.pack f + Nothing -> pure () | args `Cli.has` Cli.command "import" = do -- Note: -i <file> means the value is stored in option 'i', not argument "file" file <- case Cli.getArg args (Cli.shortOption 'i') of @@ -362,12 +373,6 @@ move args Just f -> pure (T.pack f) importTasks (T.unpack file) putText <| "Imported tasks from " <> file - | args `Cli.has` Cli.command "sync" = do - -- Export tasks and commit locally only - exportTasks - callCommand "git add .tasks/tasks.jsonl" - callCommand "git commit -m 'task: sync database' || true" - putText "Synced tasks: exported and committed to git (use 'git push' to share with remote)" | otherwise = putText (T.pack <| Cli.usage help) where getArgText :: Cli.Arguments -> String -> IO Text diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 1eb820f..4ce9066 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE NoImplicitPrelude #-} module Omni.Task.Core where @@ -7,21 +8,19 @@ module Omni.Task.Core where import Alpha import Data.Aeson (FromJSON, ToJSON, decode, encode) import qualified Data.Aeson as Aeson -import qualified Data.Aeson.KeyMap as KM -import Data.Aeson.Types (parseMaybe) import qualified Data.ByteString.Lazy.Char8 as BLC import qualified Data.List as List import qualified Data.Text as T import qualified Data.Text.IO as TIO import Data.Time (UTCTime, diffTimeToPicoseconds, getCurrentTime, utctDay, utctDayTime) import Data.Time.Calendar (toModifiedJulianDay) +import qualified Database.SQLite.Simple as SQL +import qualified Database.SQLite.Simple.FromField as SQL +import qualified Database.SQLite.Simple.Ok as SQLOk +import qualified Database.SQLite.Simple.ToField as SQL import GHC.Generics () import System.Directory (createDirectoryIfMissing, doesFileExist) import System.Environment (lookupEnv) -import System.IO (SeekMode (AbsoluteSeek)) -import qualified System.IO as IO -import System.IO.Unsafe (unsafePerformIO) -import System.Posix.IO (LockRequest (..), closeFd, handleToFd, waitToSetLock) -- Core data types data Task = Task @@ -40,14 +39,14 @@ data Task = Task deriving (Show, Eq, Generic) data TaskType = Epic | WorkTask | HumanTask - deriving (Show, Eq, Generic) + deriving (Show, Eq, Read, Generic) data Status = Open | InProgress | Review | Approved | Done - deriving (Show, Eq, Generic) + deriving (Show, Eq, Read, Generic) -- Priority levels (matching beads convention) data Priority = P0 | P1 | P2 | P3 | P4 - deriving (Show, Eq, Ord, Generic) + deriving (Show, Eq, Ord, Read, Generic) data Dependency = Dependency { depId :: Text, -- ID of the task this depends on @@ -60,7 +59,7 @@ data DependencyType | DiscoveredFrom -- Work discovered during other work | ParentChild -- Epic/subtask relationship | Related -- Soft relationship, doesn't block - deriving (Show, Eq, Generic) + deriving (Show, Eq, Read, Generic) data TaskProgress = TaskProgress { progressTaskId :: Text, @@ -94,6 +93,83 @@ instance ToJSON Task instance FromJSON Task +instance ToJSON TaskProgress + +instance FromJSON TaskProgress + +-- SQLite Instances + +instance SQL.FromField TaskType where + fromField f = do + t <- SQL.fromField f :: SQLOk.Ok String + case readMaybe t of + Just x -> pure x + Nothing -> SQL.returnError SQL.ConversionFailed f "Invalid TaskType" + +instance SQL.ToField TaskType where + toField x = SQL.toField (show x :: String) + +instance SQL.FromField Status where + fromField f = do + t <- SQL.fromField f :: SQLOk.Ok String + case readMaybe t of + Just x -> pure x + Nothing -> SQL.returnError SQL.ConversionFailed f "Invalid Status" + +instance SQL.ToField Status where + toField x = SQL.toField (show x :: String) + +instance SQL.FromField Priority where + fromField f = do + t <- SQL.fromField f :: SQLOk.Ok String + case readMaybe t of + Just x -> pure x + Nothing -> SQL.returnError SQL.ConversionFailed f "Invalid Priority" + +instance SQL.ToField Priority where + toField x = SQL.toField (show x :: String) + +-- Store dependencies as JSON text +instance SQL.FromField [Dependency] where + fromField f = do + t <- SQL.fromField f :: SQLOk.Ok String + case Aeson.decode (BLC.pack t) of + Just x -> pure x + Nothing -> pure [] -- Default to empty if parse fail or null + +instance SQL.ToField [Dependency] where + toField deps = SQL.toField (BLC.unpack (encode deps)) + +instance SQL.FromRow Task where + fromRow = + Task + </ SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + <*> SQL.field + +instance SQL.ToRow Task where + toRow t = + [ SQL.toField (taskId t), + SQL.toField (taskTitle t), + SQL.toField (taskType t), + SQL.toField (taskParent t), + SQL.toField (taskNamespace t), + SQL.toField (taskStatus t), + SQL.toField (taskPriority t), + SQL.toField (taskDependencies t), + SQL.toField (taskDescription t), + SQL.toField (taskCreatedAt t), + SQL.toField (taskUpdatedAt t) + ] + -- | Case-insensitive ID comparison matchesId :: Text -> Text -> Bool matchesId id1 id2 = normalizeId id1 == normalizeId id2 @@ -118,108 +194,66 @@ normalizeTask t = normalizeDependency :: Dependency -> Dependency normalizeDependency d = d {depId = normalizeId (depId d)} -instance ToJSON TaskProgress - -instance FromJSON TaskProgress - --- Get the tasks database file path (use test file if TASK_TEST_MODE is set) -getTasksFilePath :: IO FilePath -getTasksFilePath = do +-- Get the tasks database file path +getTasksDbPath :: IO FilePath +getTasksDbPath = do customPath <- lookupEnv "TASK_DB_PATH" testMode <- lookupEnv "TASK_TEST_MODE" let path = case (customPath, testMode) of (Just p, _) -> p - (_, Just "1") -> ".tasks/tasks-test.jsonl" - _ -> ".tasks/tasks.jsonl" + (_, Just "1") -> ".tasks/tasks-test.db" + _ -> ".tasks/tasks.db" pure path +-- DB Helper +withDb :: (SQL.Connection -> IO a) -> IO a +withDb action = do + dbPath <- getTasksDbPath + SQL.withConnection dbPath <| \conn -> do + SQL.execute_ conn "PRAGMA busy_timeout = 5000" + action conn + -- Initialize the task database initTaskDb :: IO () initTaskDb = do createDirectoryIfMissing True ".tasks" - tasksFile <- getTasksFilePath - exists <- doesFileExist tasksFile - unless exists <| do - TIO.writeFile tasksFile "" - putText <| "Initialized task database at " <> T.pack tasksFile - --- Lock for in-process thread safety -taskLock :: MVar () -taskLock = unsafePerformIO (newMVar ()) -{-# NOINLINE taskLock #-} - --- Execute action with write lock (exclusive) -withTaskWriteLock :: IO a -> IO a -withTaskWriteLock action = - withMVar taskLock <| \_ -> do - -- In test mode, we rely on MVar for thread safety to avoid GHC "resource busy" errors - -- when mixing openFd (flock) and standard IO in threaded tests. - testMode <- lookupEnv "TASK_TEST_MODE" - case testMode of - Just "1" -> action - _ -> do - tasksFile <- getTasksFilePath - let lockFile = tasksFile <> ".lock" - bracket - ( do - h <- IO.openFile lockFile IO.ReadWriteMode - handleToFd h - ) - closeFd - ( \fd -> do - waitToSetLock fd (WriteLock, AbsoluteSeek, 0, 0) - action - ) - --- Execute action with read lock (shared) -withTaskReadLock :: IO a -> IO a -withTaskReadLock action = - withMVar taskLock <| \_ -> do - testMode <- lookupEnv "TASK_TEST_MODE" - case testMode of - Just "1" -> action - _ -> do - tasksFile <- getTasksFilePath - let lockFile = tasksFile <> ".lock" - bracket - ( do - h <- IO.openFile lockFile IO.ReadWriteMode - handleToFd h - ) - closeFd - ( \fd -> do - waitToSetLock fd (ReadLock, AbsoluteSeek, 0, 0) - action - ) + withDb <| \conn -> do + SQL.execute_ + conn + "CREATE TABLE IF NOT EXISTS tasks (\ + \ id TEXT PRIMARY KEY, \ + \ title TEXT NOT NULL, \ + \ type TEXT NOT NULL, \ + \ parent TEXT, \ + \ namespace TEXT, \ + \ status TEXT NOT NULL, \ + \ priority TEXT NOT NULL, \ + \ dependencies TEXT NOT NULL, \ + \ description TEXT, \ + \ created_at TIMESTAMP NOT NULL, \ + \ updated_at TIMESTAMP NOT NULL \ + \)" -- Generate a short ID using base36 encoding of timestamp (lowercase) generateId :: IO Text generateId = do now <- getCurrentTime - -- Convert current time to microseconds since epoch (using MJD) let day = utctDay now dayTime = utctDayTime now mjd = toModifiedJulianDay day micros = diffTimeToPicoseconds dayTime `div` 1000000 - -- Combine MJD and micros to ensure uniqueness across days. - -- Multiplier 10^11 (100,000 seconds) is safe for any day length. totalMicros = (mjd * 100000000000) + micros encoded = toBase36 totalMicros pure <| "t-" <> T.pack encoded --- Generate a child ID based on parent ID (e.g. "t-abc.1", "t-abc.1.2") --- Finds the next available sequential suffix among existing children. +-- Generate a child ID based on parent ID generateChildId :: Text -> IO Text -generateChildId parentId = - withTaskReadLock <| do - tasks <- loadTasksInternal - pure <| computeNextChildId tasks (normalizeId parentId) +generateChildId parentId = do + tasks <- loadTasks + pure <| computeNextChildId tasks (normalizeId parentId) computeNextChildId :: [Task] -> Text -> Text computeNextChildId tasks parentId = - -- Find the max suffix among ALL tasks that look like children (to avoid ID collisions) - -- We check all tasks, not just those with taskParent set, because we want to ensure - -- ID uniqueness even if the parent link is missing. let suffixes = mapMaybe (getSuffix parentId <. taskId) tasks nextSuffix = case suffixes of [] -> 1 @@ -248,178 +282,104 @@ toBase36 n = reverse <| go n idx = fromIntegral r char = case drop idx alphabet of (c : _) -> c - [] -> '0' -- Fallback (should never happen) + [] -> '0' in char : go q --- Load all tasks from JSONL file (with migration support) +-- Load all tasks from DB loadTasks :: IO [Task] -loadTasks = withTaskReadLock loadTasksInternal - -loadTasksInternal :: IO [Task] -loadTasksInternal = do - tasksFile <- getTasksFilePath - exists <- doesFileExist tasksFile - if exists - then do - content <- TIO.readFile tasksFile - let taskLines = T.lines content - pure <| mapMaybe decodeTask taskLines - else pure [] - where - decodeTask :: Text -> Maybe Task - decodeTask line = - if T.null line - then Nothing - else case decode (BLC.pack <| T.unpack line) of - Just task -> Just task - Nothing -> migrateTask line - - -- Migrate old task formats to new format - migrateTask :: Text -> Maybe Task - migrateTask line = case Aeson.decode (BLC.pack <| T.unpack line) :: Maybe Aeson.Object of - Nothing -> Nothing - Just obj -> - let taskId' = KM.lookup "taskId" obj +> parseMaybe Aeson.parseJSON - taskTitle' = KM.lookup "taskTitle" obj +> parseMaybe Aeson.parseJSON - taskStatus' = KM.lookup "taskStatus" obj +> parseMaybe Aeson.parseJSON - taskCreatedAt' = KM.lookup "taskCreatedAt" obj +> parseMaybe Aeson.parseJSON - taskUpdatedAt' = KM.lookup "taskUpdatedAt" obj +> parseMaybe Aeson.parseJSON - - -- Extract taskDescription (new field) - taskDescription' = KM.lookup "taskDescription" obj +> parseMaybe Aeson.parseJSON - - -- Extract dependencies (handle V1 [Dependency] and V0 [Text]) - v1Deps = KM.lookup "taskDependencies" obj +> parseMaybe Aeson.parseJSON :: Maybe [Dependency] - v0Deps = KM.lookup "taskDependencies" obj +> parseMaybe Aeson.parseJSON :: Maybe [Text] - finalDeps = case v1Deps of - Just ds -> ds - Nothing -> case v0Deps of - Just ts -> map (\tid -> Dependency {depId = tid, depType = Blocks}) ts - Nothing -> [] - - -- taskProject is ignored in new format (use epics instead) - taskType' = fromMaybe WorkTask (KM.lookup "taskType" obj +> parseMaybe Aeson.parseJSON) - taskParent' = KM.lookup "taskParent" obj +> parseMaybe Aeson.parseJSON - taskNamespace' = KM.lookup "taskNamespace" obj +> parseMaybe Aeson.parseJSON - -- Default priority to P2 (medium) for old tasks - taskPriority' = fromMaybe P2 (KM.lookup "taskPriority" obj +> parseMaybe Aeson.parseJSON) - in case (taskId', taskTitle', taskStatus', taskCreatedAt', taskUpdatedAt') of - (Just tid, Just title, Just status, Just created, Just updated) -> - Just - Task - { taskId = tid, - taskTitle = title, - taskType = taskType', - taskParent = taskParent', - taskNamespace = taskNamespace', - taskStatus = status, - taskPriority = taskPriority', - taskDependencies = finalDeps, - taskDescription = taskDescription', - taskCreatedAt = created, - taskUpdatedAt = updated - } - _ -> Nothing - --- Save a single task (append to JSONL) -saveTask :: Task -> IO () -saveTask = withTaskWriteLock <. saveTaskInternal +loadTasks = + withDb <| \conn -> do + SQL.query_ conn "SELECT id, title, type, parent, namespace, status, priority, dependencies, description, created_at, updated_at FROM tasks" -saveTaskInternal :: Task -> IO () -saveTaskInternal task = do - tasksFile <- getTasksFilePath - let json = encode task - BLC.appendFile tasksFile (json <> "\n") +-- Save a single task (UPSERT) +saveTask :: Task -> IO () +saveTask task = + withDb <| \conn -> do + SQL.execute + conn + "INSERT OR REPLACE INTO tasks \ + \ (id, title, type, parent, namespace, status, priority, dependencies, description, created_at, updated_at) \ + \ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + task -- Create a new task createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> Priority -> [Dependency] -> Maybe Text -> IO Task -createTask title taskType parent namespace priority deps description = - withTaskWriteLock <| do - let parent' = fmap normalizeId parent - deps' = map normalizeDependency deps - - tid <- case parent' of - Nothing -> generateUniqueId - Just pid -> do - tasks <- loadTasksInternal - pure <| computeNextChildId tasks pid - now <- getCurrentTime - let task = - Task - { taskId = normalizeId tid, - taskTitle = title, - taskType = taskType, - taskParent = parent', - taskNamespace = namespace, - taskStatus = Open, - taskPriority = priority, - taskDependencies = deps', - taskDescription = description, - taskCreatedAt = now, - taskUpdatedAt = now - } - saveTaskInternal task - pure task - --- Generate a unique ID (checking against existing tasks) +createTask title taskType parent namespace priority deps description = do + let parent' = fmap normalizeId parent + deps' = map normalizeDependency deps + + tid <- case parent' of + Nothing -> generateUniqueId + Just pid -> do + tasks <- loadTasks + pure <| computeNextChildId tasks pid + now <- getCurrentTime + let task = + Task + { taskId = normalizeId tid, + taskTitle = title, + taskType = taskType, + taskParent = parent', + taskNamespace = namespace, + taskStatus = Open, + taskPriority = priority, + taskDependencies = deps', + taskDescription = description, + taskCreatedAt = now, + taskUpdatedAt = now + } + saveTask task + pure task + +-- Generate a unique ID generateUniqueId :: IO Text generateUniqueId = do - tasks <- loadTasksInternal - go tasks + -- We can query DB directly to check existence + go where - go tasks = do + go = do tid <- generateId - case findTask tid tasks of - Nothing -> pure tid - Just _ -> go tasks -- Retry if collision (case-insensitive) + exists <- + withDb <| \conn -> do + [SQL.Only c] <- SQL.query conn "SELECT COUNT(*) FROM tasks WHERE id = ?" (SQL.Only tid) :: IO [SQL.Only Int] + pure (c > 0) + if exists then go else pure tid -- Update task status updateTaskStatus :: Text -> Status -> [Dependency] -> IO () updateTaskStatus tid newStatus newDeps = - withTaskWriteLock <| do - tasks <- loadTasksInternal + withDb <| \conn -> do now <- getCurrentTime - let updatedTasks = map updateIfMatch tasks - updateIfMatch t = - if matchesId (taskId t) tid - then t {taskStatus = newStatus, taskUpdatedAt = now, taskDependencies = if null newDeps then taskDependencies t else newDeps} - else t - -- Rewrite the entire file (simple approach for MVP) - tasksFile <- getTasksFilePath - TIO.writeFile tasksFile "" - traverse_ saveTaskInternal updatedTasks - --- Edit a task by applying a modification function + -- If newDeps is empty, we need to preserve existing deps. + -- If newDeps is NOT empty, we replace them. + -- This logic is slightly tricky in SQL. We fetch first. + rows <- SQL.query conn "SELECT dependencies FROM tasks WHERE id = ?" (SQL.Only tid) :: IO [SQL.Only [Dependency]] + case rows of + [] -> pure () -- Task not found + (SQL.Only existingDeps : _) -> do + let finalDeps = if null newDeps then existingDeps else newDeps + SQL.execute + conn + "UPDATE tasks SET status = ?, updated_at = ?, dependencies = ? WHERE id = ?" + (newStatus, now, finalDeps, tid) + +-- Edit a task editTask :: Text -> (Task -> Task) -> IO Task -editTask tid modifyFn = - withTaskWriteLock <| do - tasks <- loadTasksInternal - now <- getCurrentTime - - -- Find the task first to ensure it exists - case findTask tid tasks of - Nothing -> panic "Task not found" - Just original -> do - let modified = modifyFn original - -- Only update timestamp if something actually changed - -- But for simplicity, we always update it if the user explicitly ran 'edit' - finalTask = modified {taskUpdatedAt = now} - - updateIfMatch t = - if matchesId (taskId t) tid - then finalTask - else t - updatedTasks = map updateIfMatch tasks - - -- Rewrite the entire file - tasksFile <- getTasksFilePath - TIO.writeFile tasksFile "" - traverse_ saveTaskInternal updatedTasks - pure finalTask - --- List tasks, optionally filtered by type, parent, status, or namespace +editTask tid modifyFn = do + tasks <- loadTasks + case findTask tid tasks of + Nothing -> panic "Task not found" + Just original -> do + now <- getCurrentTime + let modified = modifyFn original + finalTask = modified {taskUpdatedAt = now} + saveTask finalTask + pure finalTask + +-- List tasks listTasks :: Maybe TaskType -> Maybe Text -> Maybe Status -> Maybe Text -> IO [Task] listTasks maybeType maybeParent maybeStatus maybeNamespace = do + -- Implementing specific filters in SQL would be more efficient, but for MVP and API compat: tasks <- loadTasks let filtered = tasks @@ -438,20 +398,16 @@ listTasks maybeType maybeParent maybeStatus maybeNamespace = do filterByNamespace Nothing ts = ts filterByNamespace (Just ns) ts = filter (\t -> taskNamespace t == Just ns) ts --- Get ready tasks (not blocked by dependencies and not a parent) +-- Get ready tasks getReadyTasks :: IO [Task] getReadyTasks = do allTasks <- loadTasks - -- Only Open or InProgress tasks are considered ready for work. - -- Review tasks are waiting for review, and Done tasks are complete. let openTasks = filter (\t -> taskStatus t == Open || taskStatus t == InProgress) allTasks doneIds = map taskId <| filter (\t -> taskStatus t == Done) allTasks - -- Find all tasks that act as parents parentIds = mapMaybe taskParent allTasks isParent tid = tid `elem` parentIds - -- Only Blocks and ParentChild dependencies block ready work blockingDepIds task = [depId dep | dep <- taskDependencies task, depType dep `elem` [Blocks, ParentChild]] isReady task = taskType task @@ -462,7 +418,7 @@ getReadyTasks = do /= HumanTask pure <| filter isReady openTasks --- Get dependency tree for a task (returns tasks) +-- Get dependency tree getDependencyTree :: Text -> IO [Task] getDependencyTree tid = do tasks <- loadTasks @@ -470,7 +426,6 @@ getDependencyTree tid = do Nothing -> pure [] Just task -> pure <| collectDeps tasks task where - collectDeps :: [Task] -> Task -> [Task] collectDeps allTasks task = let depIds = map depId (taskDependencies task) deps = filter (\t -> any (matchesId (taskId t)) depIds) allTasks @@ -480,8 +435,8 @@ getDependencyTree tid = do getTaskProgress :: Text -> IO TaskProgress getTaskProgress tidRaw = do let tid = normalizeId tidRaw + -- Could be SQL optimized tasks <- loadTasks - -- Verify task exists (optional, but good for error handling) case findTask tid tasks of Nothing -> panic "Task not found" Just _ -> do @@ -497,13 +452,11 @@ getTaskProgress tidRaw = do progressPercentage = percentage } --- Show task progress showTaskProgress :: Text -> IO () showTaskProgress tid = do progress <- getTaskProgress tid putText <| "Progress for " <> tid <> ": " <> T.pack (show (progressCompleted progress)) <> "/" <> T.pack (show (progressTotal progress)) <> " (" <> T.pack (show (progressPercentage progress)) <> "%)" --- Show dependency tree for a task showDependencyTree :: Text -> IO () showDependencyTree tid = do tasks <- loadTasks @@ -518,53 +471,42 @@ showDependencyTree tid = do deps = filter (\t -> any (matchesId (taskId t)) depIds) allTasks traverse_ (\dep -> printTree allTasks dep (indent + 1)) deps --- Get task tree (returns tasks hierarchically) getTaskTree :: Maybe Text -> IO [Task] getTaskTree maybeId = do tasks <- loadTasks case maybeId of Nothing -> do - -- Return all epics with their children let epics = filter (\t -> taskType t == Epic) tasks in pure <| concatMap (collectChildren tasks) epics Just tid -> do - -- Return specific task/epic with its children case findTask tid tasks of Nothing -> pure [] Just task -> pure <| collectChildren tasks task where - collectChildren :: [Task] -> Task -> [Task] collectChildren allTasks task = let children = filter (maybe False (`matchesId` taskId task) <. taskParent) allTasks in task : concatMap (collectChildren allTasks) children --- Show task tree (epic with children, or all epics if no ID given) showTaskTree :: Maybe Text -> IO () showTaskTree maybeId = do tasks <- loadTasks case maybeId of Nothing -> do - -- Show all epics with their children let epics = filter (\t -> taskType t == Epic) tasks if null epics then putText "No epics found" else traverse_ (printEpicTree tasks) epics Just tid -> do - -- Show specific task/epic with its children case findTask tid tasks of Nothing -> putText "Task not found" Just task -> printEpicTree tasks task where - printEpicTree :: [Task] -> Task -> IO () - printEpicTree allTasks task = printTreeNode allTasks task 0 + printEpicTree allTasks task = printTreeNode allTasks task (0 :: Int) - printTreeNode :: [Task] -> Task -> Int -> IO () printTreeNode allTasks task indent = printTreeNode' allTasks task indent [] - printTreeNode' :: [Task] -> Task -> Int -> [Bool] -> IO () printTreeNode' allTasks task indent ancestry = do let children = filter (maybe False (`matchesId` taskId task) <. taskParent) allTasks - -- Build tree prefix using box-drawing characters prefix = if indent == 0 then "" @@ -572,7 +514,6 @@ showTaskTree maybeId = do let ancestorPrefixes = map (\hasMore -> if hasMore then "│ " else " ") (List.init ancestry) myPrefix = if List.last ancestry then "├── " else "└── " in T.pack <| concat ancestorPrefixes ++ myPrefix - -- For epics, show progress count [completed/total]; for tasks, show status checkbox statusStr = case taskType task of Epic -> let total = length children @@ -602,7 +543,6 @@ showTaskTree maybeId = do Nothing -> "" Just _ -> gray nsStr - -- Calculate available width for title (80 cols - prefix - id - labels) usedWidth = T.length prefix + T.length (taskId task) + T.length statusStr + T.length nsStr + 2 availableWidth = max 20 (80 - usedWidth) truncatedTitle = @@ -614,7 +554,6 @@ showTaskTree maybeId = do putText <| prefix <> cyan (taskId task) <> " " <> coloredStatusStr <> " " <> coloredNsStr <> coloredTitle - -- Print children with updated ancestry let indexedChildren = zip [1 ..] children totalChildren = length children traverse_ @@ -624,7 +563,6 @@ showTaskTree maybeId = do ) indexedChildren --- Helper to print a task printTask :: Task -> IO () printTask t = do tasks <- loadTasks @@ -655,13 +593,10 @@ printTask t = do Done -> green s coloredTitle = if taskType t == Epic then bold (taskTitle t) else taskTitle t - coloredProgress = if taskType t == Epic then magenta progressInfo else progressInfo - coloredNamespace = case taskNamespace t of Nothing -> "" Just _ -> gray namespaceInfo - coloredParent = case taskParent t of Nothing -> "" Just _ -> gray parentInfo @@ -678,7 +613,6 @@ printTask t = do <> coloredParent <> coloredNamespace --- Show detailed task information (human-readable) showTaskDetailed :: Task -> IO () showTaskDetailed t = do tasks <- loadTasks @@ -690,7 +624,6 @@ showTaskDetailed t = do putText <| "Status: " <> T.pack (show (taskStatus t)) putText <| "Priority: " <> T.pack (show (taskPriority t)) <> priorityDesc - -- Show epic progress if this is an epic when (taskType t == Epic) <| do let children = filter (maybe False (`matchesId` taskId t) <. taskParent) tasks total = length children @@ -707,19 +640,16 @@ showTaskDetailed t = do putText <| "Created: " <> T.pack (show (taskCreatedAt t)) putText <| "Updated: " <> T.pack (show (taskUpdatedAt t)) - -- Show dependencies unless (null (taskDependencies t)) <| do putText "" putText "Dependencies:" traverse_ printDependency (taskDependencies t) - -- Show description case taskDescription t of Nothing -> pure () Just desc -> do putText "" putText "Description:" - -- Indent description for better readability let indented = T.unlines <| map (" " <>) (T.lines desc) putText indented @@ -735,7 +665,6 @@ showTaskDetailed t = do printDependency dep = putText <| " - " <> depId dep <> " [" <> T.pack (show (depType dep)) <> "]" --- ANSI Colors red, green, yellow, blue, magenta, cyan, gray, bold :: Text -> Text red t = "\ESC[31m" <> t <> "\ESC[0m" green t = "\ESC[32m" <> t <> "\ESC[0m" @@ -746,17 +675,23 @@ cyan t = "\ESC[36m" <> t <> "\ESC[0m" gray t = "\ESC[90m" <> t <> "\ESC[0m" bold t = "\ESC[1m" <> t <> "\ESC[0m" --- Export tasks: Consolidate JSONL file (remove duplicates, keep latest version) -exportTasks :: IO () -exportTasks = - withTaskWriteLock <| do - tasks <- loadTasksInternal - -- Rewrite the entire file with deduplicated tasks - tasksFile <- getTasksFilePath - TIO.writeFile tasksFile "" - traverse_ saveTaskInternal tasks - --- Task statistics +-- Export tasks: Dump SQLite to JSONL +exportTasks :: Maybe FilePath -> IO () +exportTasks maybePath = do + tasks <- loadTasks + case maybePath of + Just path -> do + TIO.writeFile path "" + traverse_ (saveTaskToJsonl path) tasks + Nothing -> + -- Stream to stdout + traverse_ (BLC.putStrLn <. encode) tasks + +saveTaskToJsonl :: FilePath -> Task -> IO () +saveTaskToJsonl path task = do + let json = encode task + BLC.appendFile path (json <> "\n") + data TaskStats = TaskStats { totalTasks :: Int, openTasks :: Int, @@ -776,7 +711,6 @@ instance ToJSON TaskStats instance FromJSON TaskStats --- Get task statistics getTaskStats :: Maybe Text -> IO TaskStats getTaskStats maybeEpicId = do allTasks <- loadTasks @@ -790,7 +724,6 @@ getTaskStats maybeEpicId = do globalReady <- getReadyTasks let readyIds = map taskId globalReady - -- Filter ready tasks to only include those in our target set readyCount = length <| filter (\t -> taskId t `elem` readyIds) targetTasks tasks = targetTasks @@ -803,7 +736,6 @@ getTaskStats maybeEpicId = do epics = length <| filter (\t -> taskType t == Epic) tasks readyCount' = readyCount blockedCount = total - readyCount' - done - -- Count tasks by priority byPriority = [ (P0, length <| filter (\t -> taskPriority t == P0) tasks), (P1, length <| filter (\t -> taskPriority t == P1) tasks), @@ -811,7 +743,6 @@ getTaskStats maybeEpicId = do (P3, length <| filter (\t -> taskPriority t == P3) tasks), (P4, length <| filter (\t -> taskPriority t == P4) tasks) ] - -- Count tasks by namespace namespaces = mapMaybe taskNamespace tasks uniqueNs = List.nub namespaces byNamespace = map (\ns -> (ns, length <| filter (\t -> taskNamespace t == Just ns) tasks)) uniqueNs @@ -830,13 +761,11 @@ getTaskStats maybeEpicId = do tasksByNamespace = byNamespace } --- Helper to get all descendants of a task (recursive) getAllDescendants :: [Task] -> Text -> [Task] getAllDescendants allTasks parentId = let children = filter (maybe False (`matchesId` parentId) <. taskParent) allTasks in children ++ concatMap (getAllDescendants allTasks <. taskId) children --- Show task statistics (human-readable) showTaskStats :: Maybe Text -> IO () showTaskStats maybeEpicId = do stats <- getTaskStats maybeEpicId @@ -876,48 +805,21 @@ showTaskStats maybeEpicId = do printNamespace (ns, count) = putText <| " " <> T.pack (show count) <> " " <> ns --- Import tasks: Read from another JSONL file and merge with existing tasks +-- Import tasks: Read from JSONL and insert/update DB importTasks :: FilePath -> IO () -importTasks filePath = - withTaskWriteLock <| do - exists <- doesFileExist filePath - unless exists <| panic (T.pack filePath <> " does not exist") - - -- Load tasks from import file - content <- TIO.readFile filePath - let importLines = T.lines content - importedTasks = map normalizeTask (mapMaybe decodeTask importLines) - - -- Load existing tasks - existingTasks <- loadTasksInternal - - -- Create a map of existing task IDs for quick lookup - let existingIds = map taskId existingTasks - -- Filter to only new tasks (not already in our database) - newTasks = filter (\t -> not (any (`matchesId` taskId t) existingIds)) importedTasks - -- For tasks that exist, update them with imported data - updatedTasks = map (updateWithImported importedTasks) existingTasks - -- Combine: updated existing tasks + new tasks - allTasks = updatedTasks ++ newTasks - - -- Rewrite tasks.jsonl with merged data - tasksFile <- getTasksFilePath - TIO.writeFile tasksFile "" - traverse_ saveTaskInternal allTasks +importTasks filePath = do + exists <- doesFileExist filePath + unless exists <| panic (T.pack filePath <> " does not exist") + + content <- TIO.readFile filePath + let importLines = T.lines content + importedTasks = map normalizeTask (mapMaybe decodeTask importLines) + + -- Save all imported tasks (UPSERT logic handles updates) + traverse_ saveTask importedTasks where decodeTask :: Text -> Maybe Task decodeTask line = if T.null line then Nothing else decode (BLC.pack <| T.unpack line) - - -- Update an existing task if there's a newer version in imported tasks - updateWithImported :: [Task] -> Task -> Task - updateWithImported imported existing = - case findTask (taskId existing) imported of - Nothing -> existing -- No imported version, keep existing - Just importedTask -> - -- Use imported version if it's newer (based on updatedAt) - if taskUpdatedAt importedTask > taskUpdatedAt existing - then importedTask - else existing -- cgit v1.2.3