From dda4968427d67adf319e97ea383299b7e80aefc8 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 17:43:06 -0500 Subject: task: claim t-1rb7Xhg --- .tasks/tasks.jsonl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index 66fd4a1..c63b7d1 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -130,3 +130,9 @@ {"taskCreatedAt":"2025-11-20T21:41:20.067644599Z","taskDependencies":[],"taskId":"t-1ne80pJ","taskNamespace":"Biz/Dragons.hs","taskParent":"t-1f9QP23","taskPriority":"P2","taskStatus":"Open","taskTitle":"Store generated JWK in persistent file","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:20.067644599Z"} {"taskCreatedAt":"2025-11-20T21:41:32.113815607Z","taskDependencies":[],"taskId":"t-1neWyaO","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-144hCMJ","taskPriority":"P2","taskStatus":"Open","taskTitle":"Add tests for Admin dashboard","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:32.113815607Z"} {"taskCreatedAt":"2025-11-20T21:41:32.132888832Z","taskDependencies":[],"taskId":"t-1neWD8r","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-144hCMJ","taskPriority":"P2","taskStatus":"Open","taskTitle":"Add error handling tests for Worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:32.132888832Z"} +{"taskCreatedAt":"2025-11-20T22:42:03.728732682Z","taskDependencies":[],"taskId":"t-1rcIr6X","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement 'task progress ' command","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:03.728732682Z"} +{"taskCreatedAt":"2025-11-20T22:42:03.748273499Z","taskDependencies":[],"taskId":"t-1rcIwc8","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement 'task stats --epic=' filtering","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:03.748273499Z"} +{"taskCreatedAt":"2025-11-20T22:42:03.767665854Z","taskDependencies":[],"taskId":"t-1rcIBeU","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Open","taskTitle":"Add colored output to 'task list' and 'task tree'","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:03.767665854Z"} +{"taskCreatedAt":"2025-11-20T22:42:18.766787128Z","taskDependencies":[],"taskId":"t-1rdJxcd","taskNamespace":"Omni/Task/hs.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Namespace normalization incorrect for Haskell files ending in .hs","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:18.766787128Z"} +{"taskCreatedAt":"2025-11-20T22:42:37.706495845Z","taskDependencies":[],"taskId":"t-1rf10ho","taskNamespace":"Biz/PodcastItLater/hs.hs","taskParent":"t-143KQl2","taskPriority":"P3","taskStatus":"Open","taskTitle":"Research and add intro/outro sound effects","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:37.706495845Z"} +{"taskCreatedAt":"2025-11-20T22:42:37.725796962Z","taskDependencies":[],"taskId":"t-1rf15iH","taskNamespace":"Biz/PodcastItLater/hs.hs","taskParent":"t-143KQl2","taskPriority":"P3","taskStatus":"Open","taskTitle":"Implement audio crossfading for intro/outro","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:37.725796962Z"} -- cgit v1.2.3 From 93e49d92fd0c3ad07376c8aad091374dcfe484cf Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 17:49:06 -0500 Subject: task: claim t-1ndDhLo --- .tasks/tasks.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index c63b7d1..4ea9f61 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -120,7 +120,7 @@ {"taskCreatedAt":"2025-11-20T15:58:11.740041636Z","taskDependencies":[],"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"}],"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":[],"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":[],"taskId":"t-1ndDhLo","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143KQl2","taskPriority":"P2","taskStatus":"Open","taskTitle":"PodcastItLater: Add Pricing Page UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:12.7461675Z"} +{"taskCreatedAt":"2025-11-20T21:41:12.7461675Z","taskDependencies":[],"taskId":"t-1ndDhLo","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143KQl2","taskPriority":"P2","taskStatus":"InProgress","taskTitle":"PodcastItLater: Add Pricing Page UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:49:02.292783525Z"} {"taskCreatedAt":"2025-11-20T21:41:12.764720659Z","taskDependencies":[],"taskId":"t-1ndDmAD","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143KQl2","taskPriority":"P2","taskStatus":"Open","taskTitle":"PodcastItLater: Add Stripe Checkout Route","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:12.764720659Z"} {"taskCreatedAt":"2025-11-20T21:41:12.783999704Z","taskDependencies":[],"taskId":"t-1ndDrBA","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143KQl2","taskPriority":"P2","taskStatus":"Open","taskTitle":"PodcastItLater: Add Stripe Portal Route","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:12.783999704Z"} {"taskCreatedAt":"2025-11-20T21:41:12.802988426Z","taskDependencies":[],"taskId":"t-1ndDwxQ","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143KQl2","taskPriority":"P2","taskStatus":"Open","taskTitle":"PodcastItLater: Add Stripe Webhook Handler","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:12.802988426Z"} -- cgit v1.2.3 From 010b44d7d5c8ac1a20d77967d821409810557c73 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 17:58:48 -0500 Subject: task: sync database --- .tasks/tasks.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index 409dc2e..daba9f0 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -11,7 +11,7 @@ {"taskCreatedAt":"2025-11-08T21:00:29.901677247Z","taskDependencies":[],"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":[],"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":[],"taskId":"t-PpXWsU","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Task Manager Improvements","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.468930038Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.718797697Z","taskDependencies":[],"taskId":"t-PpYZt2","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement child ID generation (t-abc123.1)","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.718797697Z"} +{"taskCreatedAt":"2025-11-09T13:05:06.718797697Z","taskDependencies":[],"taskId":"t-PpYZt2","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"InProgress","taskTitle":"Implement child ID generation (t-abc123.1)","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:58:48.569585026Z"} {"taskCreatedAt":"2025-11-09T13:05:06.746734115Z","taskDependencies":[],"taskId":"t-PpZ6JC","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Add child_counters storage","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.746734115Z"} {"taskCreatedAt":"2025-11-09T13:05:06.774903465Z","taskDependencies":[],"taskId":"t-PpZe3X","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Update createTask to auto-generate child IDs","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.774903465Z"} {"taskCreatedAt":"2025-11-09T13:05:06.802295008Z","taskDependencies":[],"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"} -- cgit v1.2.3 From 6e90d59acf45cc481e4e78101a36231af43cbd96 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:06:58 -0500 Subject: task: sync database --- .tasks/tasks.jsonl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index daba9f0..3cea557 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -11,9 +11,9 @@ {"taskCreatedAt":"2025-11-08T21:00:29.901677247Z","taskDependencies":[],"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":[],"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":[],"taskId":"t-PpXWsU","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Task Manager Improvements","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.468930038Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.718797697Z","taskDependencies":[],"taskId":"t-PpYZt2","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"InProgress","taskTitle":"Implement child ID generation (t-abc123.1)","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:58:48.569585026Z"} +{"taskCreatedAt":"2025-11-09T13:05:06.718797697Z","taskDependencies":[],"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-20T23:06:53.062229689Z"} {"taskCreatedAt":"2025-11-09T13:05:06.746734115Z","taskDependencies":[],"taskId":"t-PpZ6JC","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Add child_counters storage","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.746734115Z"} -{"taskCreatedAt":"2025-11-09T13:05:06.774903465Z","taskDependencies":[],"taskId":"t-PpZe3X","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Update createTask to auto-generate child IDs","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T13:05:06.774903465Z"} +{"taskCreatedAt":"2025-11-09T13:05:06.774903465Z","taskDependencies":[],"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":[],"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":[],"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":[],"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"} -- cgit v1.2.3 From 028bbda4282515b95a7555209d397aaf22d32244 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:07:12 -0500 Subject: feat: implement t-PpYZt2 --- Biz/PodcastItLater/UI.py | 162 ++++++++++++++++++++++++++++++++++++++++++++++ Biz/PodcastItLater/Web.py | 30 +++++++++ Omni/Task.hs | 8 ++- Omni/Task/Core.hs | 26 +++++++- 4 files changed, 224 insertions(+), 2 deletions(-) diff --git a/Biz/PodcastItLater/UI.py b/Biz/PodcastItLater/UI.py index da0193c..27f5fff 100644 --- a/Biz/PodcastItLater/UI.py +++ b/Biz/PodcastItLater/UI.py @@ -210,6 +210,24 @@ class PageLayout(Component[AnyChildren, PageLayoutAttrs]): ), classes=["nav-item"], ), + html.li( + html.a( + html.i( + classes=[ + "bi", + "bi-stars", + "me-1", + ], + ), + "Pricing", + href="/pricing", + classes=[ + "nav-link", + "active" if is_active("pricing") else "", + ], + ), + classes=["nav-item"], + ), html.li( html.a( html.i( @@ -387,3 +405,147 @@ class PageLayout(Component[AnyChildren, PageLayoutAttrs]): create_bootstrap_js(), ), ) + + +class PricingPageAttrs(Attrs): + """Attributes for PricingPage component.""" + + user: dict[str, typing.Any] | None + + +class PricingPage(Component[AnyChildren, PricingPageAttrs]): + """Pricing page component.""" + + @override + def render(self) -> PageLayout: + user = self.attrs.get("user") + current_tier = user.get("plan_tier", "free") if user else "free" + + return PageLayout( + user=user, + current_page="pricing", + page_title="Pricing - PodcastItLater", + error=None, + meta_tags=[], + children=[ + html.div( + html.h2("Simple Pricing", classes=["text-center", "mb-5"]), + html.div( + # Free Tier + html.div( + html.div( + html.div( + html.h3("Free", classes=["card-title"]), + html.h4( + "$0", + classes=[ + "card-subtitle", + "mb-3", + "text-muted", + ], + ), + html.p( + "10 articles total", + classes=["card-text"], + ), + html.ul( + html.li("Convert 10 articles"), + html.li("Basic features"), + classes=["list-unstyled", "mb-4"], + ), + html.button( + "Current Plan", + classes=[ + "btn", + "btn-outline-primary", + "w-100", + ], + disabled=True, + ) + if current_tier == "free" + else html.div(), + classes=["card-body"], + ), + classes=["card", "mb-4", "shadow-sm", "h-100"], + ), + classes=["col-md-6"], + ), + # Paid Tier + html.div( + html.div( + html.div( + html.h3( + "Unlimited", + classes=["card-title"], + ), + html.h4( + "$12/mo", + classes=[ + "card-subtitle", + "mb-3", + "text-muted", + ], + ), + html.p( + "Unlimited articles", + classes=["card-text"], + ), + html.ul( + html.li("Unlimited conversions"), + html.li("Priority processing"), + html.li("Support independent software"), + classes=["list-unstyled", "mb-4"], + ), + html.form( + html.button( + "Upgrade Now", + type="submit", + classes=[ + "btn", + "btn-primary", + "w-100", + ], + ), + action="/upgrade", + method="POST", + ) + if user and current_tier == "free" + else ( + html.button( + "Current Plan", + classes=[ + "btn", + "btn-success", + "w-100", + ], + disabled=True, + ) + if user and current_tier == "paid" + else html.a( + "Login to Upgrade", + href="/", + classes=[ + "btn", + "btn-primary", + "w-100", + ], + ) + ), + classes=["card-body"], + ), + classes=[ + "card", + "mb-4", + "shadow-sm", + "border-primary", + "h-100", + ], + ), + classes=["col-md-6"], + ), + classes=["row"], + ), + classes=["container", "py-3"], + ), + ], + ) diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index 4a8e57e..7e8e969 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -973,6 +973,36 @@ def public_feed(request: Request) -> PublicFeedPage: ) +@app.get("/pricing") +def pricing(request: Request) -> UI.PricingPage: + """Display pricing page.""" + user_id = request.session.get("user_id") + user = Core.Database.get_user_by_id(user_id) if user_id else None + + return UI.PricingPage( + user=user, + ) + + +@app.post("/upgrade") +def upgrade(request: Request) -> RedirectResponse: + """Start upgrade checkout flow.""" + user_id = request.session.get("user_id") + if not user_id: + return RedirectResponse(url="/?error=login_required") + + try: + checkout_url = Billing.create_checkout_session( + user_id, + "paid", + BASE_URL, + ) + return RedirectResponse(url=checkout_url, status_code=303) + except ValueError: + logger.exception("Failed to create checkout session") + return RedirectResponse(url="/pricing?error=checkout_failed") + + def _handle_test_login(email: str, request: Request) -> Response: """Handle login in test mode.""" # Special handling for demo account diff --git a/Omni/Task.hs b/Omni/Task.hs index d1e672a..10136c9 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -312,7 +312,13 @@ unitTests = ready <- getReadyTasks -- Both should be ready since Related doesn't block (taskId task1 `elem` map taskId ready) Test.@?= True - (taskId task2 `elem` map taskId ready) Test.@?= True + (taskId task2 `elem` map taskId ready) Test.@?= True, + Test.unit "child task gets sequential ID" <| do + parent <- createTask "Parent" Epic Nothing Nothing P2 [] + child1 <- createTask "Child 1" WorkTask (Just (taskId parent)) Nothing P2 [] + child2 <- createTask "Child 2" WorkTask (Just (taskId parent)) Nothing P2 [] + taskId child1 Test.@?= taskId parent <> ".1" + taskId child2 Test.@?= taskId parent <> ".2" ] -- | Test CLI argument parsing to ensure docopt string matches actual usage diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index f7b7915..798f8fe 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -111,6 +111,28 @@ generateId = do encoded = toBase62 (fromIntegral microseconds) pure <| "t-" <> T.pack encoded +-- Generate a child ID based on parent ID (e.g. "t-abc.1") +generateChildId :: Text -> IO Text +generateChildId parentId = do + tasks <- loadTasks + let children = filter (\t -> taskParent t == Just parentId) tasks + -- Find the max suffix + suffixes = mapMaybe (\t -> getSuffix parentId (taskId t)) children + nextSuffix = case suffixes of + [] -> 1 + s -> maximum s + 1 + pure <| parentId <> "." <> T.pack (show nextSuffix) + +getSuffix :: Text -> Text -> Maybe Int +getSuffix parent childId = + if parent `T.isPrefixOf` childId && T.length childId > T.length parent + then + let rest = T.drop (T.length parent) childId + in if T.head rest == '.' + then readMaybe (T.unpack (T.tail rest)) + else Nothing + else Nothing + -- Convert number to base62 (0-9, a-z, A-Z) toBase62 :: Integer -> String toBase62 0 = "0" @@ -192,7 +214,9 @@ saveTask task = do -- Create a new task createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> Priority -> [Dependency] -> IO Task createTask title taskType parent namespace priority deps = do - tid <- generateId + tid <- case parent of + Nothing -> generateId + Just pid -> generateChildId pid now <- getCurrentTime let task = Task -- cgit v1.2.3 From cb37c2632cf945c1993d8b338abb1ce35899d5de Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:15:16 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task.hs | 7 ++++++- Omni/Task/Core.hs | 12 +++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Omni/Task.hs b/Omni/Task.hs index 10136c9..9b89a1d 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -318,7 +318,12 @@ unitTests = child1 <- createTask "Child 1" WorkTask (Just (taskId parent)) Nothing P2 [] child2 <- createTask "Child 2" WorkTask (Just (taskId parent)) Nothing P2 [] taskId child1 Test.@?= taskId parent <> ".1" - taskId child2 Test.@?= taskId parent <> ".2" + taskId child2 Test.@?= taskId parent <> ".2", + Test.unit "grandchild task gets sequential ID" <| do + parent <- createTask "Grandparent" Epic Nothing Nothing P2 [] + child <- createTask "Parent" Epic (Just (taskId parent)) Nothing P2 [] + grandchild <- createTask "Grandchild" WorkTask (Just (taskId child)) Nothing P2 [] + taskId grandchild Test.@?= taskId parent <> ".1.1" ] -- | Test CLI argument parsing to ensure docopt string matches actual usage diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 798f8fe..31c0981 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -117,7 +117,7 @@ generateChildId parentId = do tasks <- loadTasks let children = filter (\t -> taskParent t == Just parentId) tasks -- Find the max suffix - suffixes = mapMaybe (\t -> getSuffix parentId (taskId t)) children + suffixes = mapMaybe (getSuffix parentId <. taskId) children nextSuffix = case suffixes of [] -> 1 s -> maximum s + 1 @@ -127,10 +127,10 @@ getSuffix :: Text -> Text -> Maybe Int getSuffix parent childId = if parent `T.isPrefixOf` childId && T.length childId > T.length parent then - let rest = T.drop (T.length parent) childId + let rest = T.drop (T.length parent) childId in if T.head rest == '.' - then readMaybe (T.unpack (T.tail rest)) - else Nothing + then readMaybe (T.unpack (T.tail rest)) + else Nothing else Nothing -- Convert number to base62 (0-9, a-z, A-Z) @@ -214,9 +214,7 @@ saveTask task = do -- Create a new task createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> Priority -> [Dependency] -> IO Task createTask title taskType parent namespace priority deps = do - tid <- case parent of - Nothing -> generateId - Just pid -> generateChildId pid + tid <- maybe generateId generateChildId parent now <- getCurrentTime let task = Task -- cgit v1.2.3 From 0d8ba7ec8b7b06a490eb7f2d625e169b2ed0ad72 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:20:09 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task/Core.hs | 3 ++- debug_ns.hs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 debug_ns.hs diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 31c0981..54ed04d 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -111,7 +111,8 @@ generateId = do encoded = toBase62 (fromIntegral microseconds) pure <| "t-" <> T.pack encoded --- Generate a child ID based on parent ID (e.g. "t-abc.1") +-- 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. generateChildId :: Text -> IO Text generateChildId parentId = do tasks <- loadTasks diff --git a/debug_ns.hs b/debug_ns.hs new file mode 100644 index 0000000..fbd4ea3 --- /dev/null +++ b/debug_ns.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoImplicitPrelude #-} + +module Main where + +import Omni.Namespace +import Protolude +import qualified Data.Text as T + +main :: IO () +main = do + let root = "/home/ben/omni-worker-1" + let path = "/home/ben/omni-worker-1/Omni/Task.hs" + putStrLn $ "Root: " ++ root + putStrLn $ "Path: " ++ path + let res = fromPath root path + print res -- cgit v1.2.3 From feda8ac221d88650850a3eaac2fbe2f2b215beac Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 18:39:25 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task/Core.hs | 17 +++++++++++------ debug_ns.hs | 17 ----------------- 2 files changed, 11 insertions(+), 23 deletions(-) delete mode 100644 debug_ns.hs diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 54ed04d..525ceb4 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -13,7 +13,8 @@ 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, utctDayTime) +import Data.Time (UTCTime, diffTimeToPicoseconds, getCurrentTime, utctDayTime, utctDay) +import Data.Time.Calendar (toModifiedJulianDay) import GHC.Generics () import System.Directory (createDirectoryIfMissing, doesFileExist) import System.Environment (lookupEnv) @@ -104,11 +105,15 @@ initTaskDb = do generateId :: IO Text generateId = do now <- getCurrentTime - -- Convert current time to microseconds since midnight - let dayTime = utctDayTime now - microseconds = diffTimeToPicoseconds dayTime `div` 1000000 - -- Convert to base62 for shorter IDs - encoded = toBase62 (fromIntegral microseconds) + -- 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 = toBase62 totalMicros pure <| "t-" <> T.pack encoded -- Generate a child ID based on parent ID (e.g. "t-abc.1", "t-abc.1.2") diff --git a/debug_ns.hs b/debug_ns.hs deleted file mode 100644 index fbd4ea3..0000000 --- a/debug_ns.hs +++ /dev/null @@ -1,17 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE NoImplicitPrelude #-} - -module Main where - -import Omni.Namespace -import Protolude -import qualified Data.Text as T - -main :: IO () -main = do - let root = "/home/ben/omni-worker-1" - let path = "/home/ben/omni-worker-1/Omni/Task.hs" - putStrLn $ "Root: " ++ root - putStrLn $ "Path: " ++ path - let res = fromPath root path - print res -- cgit v1.2.3 From 809c0009da015588ffbf8c4221df04c13aacc215 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 19:06:41 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task/Core.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 525ceb4..98ef6f9 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -121,9 +121,10 @@ generateId = do generateChildId :: Text -> IO Text generateChildId parentId = do tasks <- loadTasks - let children = filter (\t -> taskParent t == Just parentId) tasks - -- Find the max suffix - suffixes = mapMaybe (getSuffix parentId <. taskId) children + -- 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 s -> maximum s + 1 -- cgit v1.2.3 From 9a256f5c8232713ee99cb78093434a32c304e192 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 19:12:52 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task.hs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Omni/Task.hs b/Omni/Task.hs index 9b89a1d..6624b31 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -323,7 +323,14 @@ unitTests = parent <- createTask "Grandparent" Epic Nothing Nothing P2 [] child <- createTask "Parent" Epic (Just (taskId parent)) Nothing P2 [] grandchild <- createTask "Grandchild" WorkTask (Just (taskId child)) Nothing P2 [] - taskId grandchild Test.@?= taskId parent <> ".1.1" + taskId grandchild Test.@?= taskId parent <> ".1.1", + Test.unit "siblings of grandchild task get sequential ID" <| do + parent <- createTask "Grandparent" Epic Nothing Nothing P2 [] + child <- createTask "Parent" Epic (Just (taskId parent)) Nothing P2 [] + grandchild1 <- createTask "Grandchild 1" WorkTask (Just (taskId child)) Nothing P2 [] + grandchild2 <- createTask "Grandchild 2" WorkTask (Just (taskId child)) Nothing P2 [] + taskId grandchild1 Test.@?= taskId parent <> ".1.1" + taskId grandchild2 Test.@?= taskId parent <> ".1.2" ] -- | Test CLI argument parsing to ensure docopt string matches actual usage -- cgit v1.2.3 From 946ebcfbb2b19e7469c9519c231736563a6b6fb6 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 19:15:41 -0500 Subject: feat: implement t-PpYZt2 --- Omni/Task.hs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Omni/Task.hs b/Omni/Task.hs index 6624b31..24e528b 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -330,7 +330,29 @@ unitTests = grandchild1 <- createTask "Grandchild 1" WorkTask (Just (taskId child)) Nothing P2 [] grandchild2 <- createTask "Grandchild 2" WorkTask (Just (taskId child)) Nothing P2 [] taskId grandchild1 Test.@?= taskId parent <> ".1.1" - taskId grandchild2 Test.@?= taskId parent <> ".1.2" + taskId grandchild2 Test.@?= taskId parent <> ".1.2", + Test.unit "child ID generation skips gaps" <| do + parent <- createTask "Parent with gaps" Epic Nothing Nothing P2 [] + child1 <- createTask "Child 1" WorkTask (Just (taskId parent)) Nothing P2 [] + -- Manually create a task with .3 suffix to simulate a gap (or deleted task) + let child3Id = taskId parent <> ".3" + child3 = Task + { taskId = child3Id, + taskTitle = "Child 3", + taskType = WorkTask, + taskParent = Just (taskId parent), + taskNamespace = Nothing, + taskStatus = Open, + taskPriority = P2, + taskDependencies = [], + taskCreatedAt = taskCreatedAt child1, + taskUpdatedAt = taskUpdatedAt child1 + } + saveTask child3 + + -- Create a new child, it should get .4, not .2 + child4 <- createTask "Child 4" WorkTask (Just (taskId parent)) Nothing P2 [] + taskId child4 Test.@?= taskId parent <> ".4" ] -- | Test CLI argument parsing to ensure docopt string matches actual usage -- cgit v1.2.3 From ada597842025a4dd083dcaf0f278b1123447c760 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 20 Nov 2025 19:19:23 -0500 Subject: task: sync database --- .tasks/tasks.jsonl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index 3cea557..23eed93 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -136,3 +136,5 @@ {"taskCreatedAt":"2025-11-20T22:42:18.766787128Z","taskDependencies":[],"taskId":"t-1rdJxcd","taskNamespace":"Omni/Task/hs.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Namespace normalization incorrect for Haskell files ending in .hs","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:18.766787128Z"} {"taskCreatedAt":"2025-11-20T22:42:37.706495845Z","taskDependencies":[],"taskId":"t-1rf10ho","taskNamespace":"Biz/PodcastItLater/hs.hs","taskParent":"t-143KQl2","taskPriority":"P3","taskStatus":"Open","taskTitle":"Research and add intro/outro sound effects","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:37.706495845Z"} {"taskCreatedAt":"2025-11-20T22:42:37.725796962Z","taskDependencies":[],"taskId":"t-1rf15iH","taskNamespace":"Biz/PodcastItLater/hs.hs","taskParent":"t-143KQl2","taskPriority":"P3","taskStatus":"Open","taskTitle":"Implement audio crossfading for intro/outro","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T22:42:37.725796962Z"} +{"taskCreatedAt":"2025-11-21T00:19:08.811498926Z","taskDependencies":[{"depId":"t-PpYZt2","depType":"DiscoveredFrom"}],"taskId":"t-1fKilH","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"bild fails in agent environment due to CODEROOT mismatch","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T00:19:08.811498926Z"} +{"taskCreatedAt":"2025-11-21T00:19:08.829956304Z","taskDependencies":[{"depId":"t-PpYZt2","depType":"DiscoveredFrom"}],"taskId":"t-1fKn9o","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Race condition in generateChildId when concurrent tasks are created","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T00:19:08.829956304Z"} -- cgit v1.2.3