summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOmni Worker <bot@omni.agent>2025-11-21 04:44:32 -0500
committerOmni Worker <bot@omni.agent>2025-11-21 04:44:32 -0500
commit5f709412759f8c3f7c81f1343fa9a94ece5117d9 (patch)
treea0696d09a500b324515a5184472cc86a79f34f31
parent65c0b02b23a8b3125b0c10112d48c1a637f01cf9 (diff)
parent7801b1dc328b07a8589d651d4af843cc6acb4552 (diff)
Merge live into task/t-rWacMb1av
Amp-Thread-ID: https://ampcode.com/threads/T-7109f8d0-feb4-4a24-bc4b-37743227e2cb Co-authored-by: Amp <amp@ampcode.com>
-rw-r--r--.tasks/tasks.jsonl34
-rwxr-xr-xBiz/Que/Host.hs73
-rw-r--r--Network/Wai/Middleware/Braid.hs29
-rw-r--r--Omni/Agent/Git.hs138
-rw-r--r--Omni/Agent/Log.hs71
-rw-r--r--Omni/Agent/LogTest.hs72
-rw-r--r--Omni/Agent/WORKER_AGENT_GUIDE.md61
-rwxr-xr-xOmni/Agent/harvest-tasks.sh9
-rwxr-xr-xOmni/Agent/setup-worker.sh7
-rw-r--r--Omni/Bild.hs3
-rw-r--r--Omni/Bild/Deps/Haskell.nix1
-rw-r--r--Omni/Task.hs53
-rw-r--r--Omni/Task/Core.hs93
13 files changed, 541 insertions, 103 deletions
diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl
index bef18ee..2d8fdba 100644
--- a/.tasks/tasks.jsonl
+++ b/.tasks/tasks.jsonl
@@ -12,7 +12,7 @@
{"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":"Epic","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":"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":[],"taskId":"t-PpZ6JC","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Review","taskTitle":"Add child_counters storage","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T04:06:13.167447697Z"}
+{"taskCreatedAt":"2025-11-09T13:05:06.746734115Z","taskDependencies":[],"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":[],"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"}
@@ -126,35 +126,35 @@
{"taskCreatedAt":"2025-11-20T21:41:12.821995769Z","taskDependencies":[],"taskId":"t-1ndDBuq","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143KQl2","taskPriority":"P2","taskStatus":"Review","taskTitle":"PodcastItLater: Enforce Paid Limits in UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:23:39.337972299Z"}
{"taskCreatedAt":"2025-11-20T21:41:32.113815607Z","taskDependencies":[],"taskId":"t-1neWyaO","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-144hCMJ","taskPriority":"P2","taskStatus":"Review","taskTitle":"Add tests for Admin dashboard","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:27:20.741813376Z"}
{"taskCreatedAt":"2025-11-20T21:41:32.132888832Z","taskDependencies":[],"taskId":"t-1neWD8r","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-144hCMJ","taskPriority":"P2","taskStatus":"Review","taskTitle":"Add error handling tests for Worker","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:41:19.218858972Z"}
-{"taskCreatedAt":"2025-11-20T22:42:03.728732682Z","taskDependencies":[],"taskId":"t-1rcIr6X","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement 'task progress <epic-id>' command","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:45:23.74069979Z"}
-{"taskCreatedAt":"2025-11-20T22:42:03.748273499Z","taskDependencies":[],"taskId":"t-1rcIwc8","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement 'task stats --epic=<id>' filtering","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:49:27.049349665Z"}
+{"taskCreatedAt":"2025-11-20T22:42:03.728732682Z","taskDependencies":[],"taskId":"t-1rcIr6X","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement 'task progress <epic-id>' command","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:59:47.987586572Z"}
+{"taskCreatedAt":"2025-11-20T22:42:03.748273499Z","taskDependencies":[],"taskId":"t-1rcIwc8","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Done","taskTitle":"Implement 'task stats --epic=<id>' filtering","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T09:02:43.362372647Z"}
{"taskCreatedAt":"2025-11-20T22:42:03.767665854Z","taskDependencies":[],"taskId":"t-1rcIBeU","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Review","taskTitle":"Add colored output to 'task list' and 'task tree'","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:55:32.252270173Z"}
{"taskCreatedAt":"2025-11-20T22:42:18.766787128Z","taskDependencies":[],"taskId":"t-1rdJxcd","taskNamespace":"Omni/Task.hs","taskParent":"t-PpXWsU","taskPriority":"P2","taskStatus":"Review","taskTitle":"Namespace normalization incorrect for Haskell files ending in .hs","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:57:50.196197695Z"}
{"taskCreatedAt":"2025-11-20T22:42:37.706495845Z","taskDependencies":[],"taskId":"t-1rf10ho","taskNamespace":"Biz/PodcastItLater/hs.hs","taskParent":"t-143KQl2","taskPriority":"P3","taskStatus":"Review","taskTitle":"Research and add intro/outro sound effects","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T05:58:46.725770278Z"}
{"taskCreatedAt":"2025-11-20T22:42:37.725796962Z","taskDependencies":[],"taskId":"t-1rf15iH","taskNamespace":"Biz/PodcastItLater/hs.hs","taskParent":"t-143KQl2","taskPriority":"P3","taskStatus":"Review","taskTitle":"Implement audio crossfading for intro/outro","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T06:04:16.484604854Z"}
-{"taskCreatedAt":"2025-11-20T23:17:30.579211649Z","taskDependencies":[],"taskId":"t-1twEu4W","taskNamespace":"Omni/Agent/hs.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Multi-Agent System 2.0","taskType":"Epic","taskUpdatedAt":"2025-11-20T23:17:30.579211649Z"}
-{"taskCreatedAt":"2025-11-20T23:17:39.613719647Z","taskDependencies":[],"taskId":"t-1txgomO","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1twEu4W","taskPriority":"P2","taskStatus":"Review","taskTitle":"Design Omni/Agent.hs CLI and module structure","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T06:10:32.465106297Z"}
-{"taskCreatedAt":"2025-11-20T23:17:39.632912633Z","taskDependencies":[],"taskId":"t-1txgtmn","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1twEu4W","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement worker process management (start/stop/pid)","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T06:26:27.067388975Z"}
-{"taskCreatedAt":"2025-11-20T23:17:39.651751765Z","taskDependencies":[],"taskId":"t-1txgyge","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1twEu4W","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement git worktree and sync logic in Haskell","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T06:27:29.057039948Z"}
-{"taskCreatedAt":"2025-11-20T23:17:39.670723428Z","taskDependencies":[],"taskId":"t-1txgDcd","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1twEu4W","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement log streaming and filtering (replace monitor-worker.sh)","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T06:37:42.211009174Z"}
-{"taskCreatedAt":"2025-11-20T23:17:39.689755832Z","taskDependencies":[],"taskId":"t-1txgI9c","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1twEu4W","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement harvesting logic in Haskell","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T06:46:29.413778983Z"}
-{"taskCreatedAt":"2025-11-20T23:17:39.708649865Z","taskDependencies":[],"taskId":"t-1txgN3W","taskNamespace":"Omni/Agent/hs.hs","taskParent":"t-1twEu4W","taskPriority":"P2","taskStatus":"Review","taskTitle":"Add integration tests for Agent workflow","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T06:56:44.846675027Z"}
+{"taskCreatedAt":"2025-11-20T23:17:30.579211649Z","taskDependencies":[],"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":[],"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":[],"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":[],"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":[],"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":[],"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":[],"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":[],"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"}],"taskId":"t-1fKilH","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Review","taskTitle":"bild fails in agent environment due to CODEROOT mismatch","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T07:30:23.523758491Z"}
{"taskCreatedAt":"2025-11-21T00:19:08.829956304Z","taskDependencies":[{"depId":"t-PpYZt2","depType":"DiscoveredFrom"}],"taskId":"t-1fKn9o","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Review","taskTitle":"Race condition in generateChildId when concurrent tasks are created","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T07:48:28.858771992Z"}
{"taskCreatedAt":"2025-11-21T02:31:40.268267384Z","taskDependencies":[],"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":[],"taskId":"t-rWa5yilwM","taskNamespace":"Omni/Agent.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Multi-Agent System 2.0 (Haskell Agent)","taskType":"Epic","taskUpdatedAt":"2025-11-21T02:39:47.740342035Z"}
-{"taskCreatedAt":"2025-11-21T02:39:51.467615692Z","taskDependencies":[],"taskId":"t-rWa5yilwM.1","taskNamespace":"Omni/Agent.hs","taskParent":"t-rWa5yilwM","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement Omni.Agent.Git module with robust checkout","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T07:59:21.235592604Z"}
+{"taskCreatedAt":"2025-11-21T02:39:51.467615692Z","taskDependencies":[],"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"}],"taskId":"t-rWa5yilwM.2","taskNamespace":"Omni/Agent.hs","taskParent":"t-rWa5yilwM","taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement Omni.Agent.Worker loop logic","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T02:39:55.225849981Z"}
-{"taskCreatedAt":"2025-11-21T02:39:58.185671478Z","taskDependencies":[],"taskId":"t-rWa5yilwM.3","taskNamespace":"Omni/Agent.hs","taskParent":"t-rWa5yilwM","taskPriority":"P2","taskStatus":"Review","taskTitle":"Implement Omni.Agent.Log module with Aeson parsing","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:06:00.247723851Z"}
+{"taskCreatedAt":"2025-11-21T02:39:58.185671478Z","taskDependencies":[],"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"}],"taskId":"t-rWa5yilwM.4","taskNamespace":"Omni/Agent.hs","taskParent":"t-rWa5yilwM","taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement Omni.Agent.CLI entry point","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T02:40:01.165180998Z"}
{"taskCreatedAt":"2025-11-21T02:49:51.7176629Z","taskDependencies":[{"depId":"t-rWa5yilwM.3","depType":"Blocks"}],"taskId":"t-rWa5yilwM.5","taskNamespace":"Omni/Agent.hs","taskParent":"t-rWa5yilwM","taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement enhanced 2-line status logging","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T02:49:51.7176629Z"}
{"taskCreatedAt":"2025-11-21T02:59:12.848135132Z","taskDependencies":[],"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":[],"taskId":"t-rWa7IYOrq","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Parent Epic","taskType":"Epic","taskUpdatedAt":"2025-11-21T03:12:57.890285833Z"}
-{"taskCreatedAt":"2025-11-21T03:13:01.031231982Z","taskDependencies":[],"taskId":"t-rWa7IYOrq.1","taskNamespace":null,"taskParent":"t-rWa7IYOrq","taskPriority":"P2","taskStatus":"Review","taskTitle":"Child Task","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:09:24.025629665Z"}
+{"taskCreatedAt":"2025-11-21T03:12:57.890285833Z","taskDependencies":[],"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":[],"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":[],"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":[],"taskId":"t-rWacMb1av","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"InProgress","taskTitle":"Make task IDs case-insensitive","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:09:33.999760866Z"}
+{"taskCreatedAt":"2025-11-21T04:30:05.792313193Z","taskDependencies":[],"taskId":"t-rWacMb1av","taskNamespace":"Omni/Task.hs","taskParent":null,"taskPriority":"P2","taskStatus":"Review","taskTitle":"Make task IDs case-insensitive","taskType":"WorkTask","taskUpdatedAt":"2025-11-21T08:18:06.371310379Z"}
{"taskCreatedAt":"2025-11-13T19:38:07.804316976Z","taskDependencies":[],"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":[],"taskId":"t-1ne7Qtj","taskNamespace":"Network/Wai/Middleware/Braid.hs","taskParent":"t-1f9QP23","taskPriority":"P2","taskStatus":"Open","taskTitle":"Implement Braid keep-alive mechanism","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:20.029426381Z"}
-{"taskCreatedAt":"2025-11-20T21:41:20.048368004Z","taskDependencies":[],"taskId":"t-1ne7VoO","taskNamespace":"Biz/Que/Host.hs","taskParent":"t-1f9QP23","taskPriority":"P2","taskStatus":"Open","taskTitle":"Revive authkey authentication in Que/Host","taskType":"WorkTask","taskUpdatedAt":"2025-11-20T21:41:20.048368004Z"}
+{"taskCreatedAt":"2025-11-20T21:41:20.029426381Z","taskDependencies":[],"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":[],"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":[],"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"}
diff --git a/Biz/Que/Host.hs b/Biz/Que/Host.hs
index 834ce0e..34b1a01 100755
--- a/Biz/Que/Host.hs
+++ b/Biz/Que/Host.hs
@@ -34,6 +34,7 @@ import Data.HashMap.Lazy (HashMap)
import qualified Data.HashMap.Lazy as HashMap
import Network.HTTP.Media ((//), (/:))
import qualified Network.Wai.Handler.Warp as Warp
+import Network.Socket (SockAddr (..))
import qualified Omni.Cli as Cli
import qualified Omni.Log as Log
import Omni.Test ((@=?))
@@ -75,7 +76,30 @@ Usage:
|]
test :: Test.Tree
-test = Test.group "Biz.Que.Host" [Test.unit "id" <| 1 @=? (1 :: Integer)]
+test =
+ Test.group
+ "Biz.Que.Host"
+ [ Test.unit "id" <| 1 @=? (1 :: Integer),
+ Test.unit "putQue requires auth for '_'" <| do
+ st <- atomically <| STM.newTVar mempty
+ let cfg = Envy.defConfig
+ let handlers = paths cfg
+
+ -- Case 1: No auth, should fail
+ let nonLocalHost = SockAddrInet 0 0
+ let handler1 = putQue handlers nonLocalHost Nothing "_" "testq" "body"
+ res1 <- Servant.runHandler (runReaderT handler1 st)
+ case res1 of
+ Left err -> if errHTTPCode err == 401 then pure () else Test.assertFailure ("Expected 401, got " <> show err)
+ Right _ -> Test.assertFailure "Expected failure, got success"
+
+ -- Case 2: Correct auth, should succeed
+ let handler2 = putQue handlers nonLocalHost (Just "admin-key") "_" "testq" "body"
+ res2 <- Servant.runHandler (runReaderT handler2 st)
+ case res2 of
+ Left err -> Test.assertFailure (show err)
+ Right _ -> pure ()
+ ]
type App = ReaderT AppState Servant.Handler
@@ -125,23 +149,31 @@ data Paths path = Paths
:- Get '[JSON] NoContent,
dash ::
path
- :- "_"
+ :- RemoteHost
+ :> Header "Authorization" Text
+ :> "_"
:> "dash"
:> Get '[JSON] Ques,
getQue ::
path
- :- Capture "ns" Text
+ :- RemoteHost
+ :> Header "Authorization" Text
+ :> Capture "ns" Text
:> Capture "quename" Text
:> Get '[PlainText, HTML, OctetStream] Message,
getStream ::
path
- :- Capture "ns" Text
+ :- RemoteHost
+ :> Header "Authorization" Text
+ :> Capture "ns" Text
:> Capture "quename" Text
:> "stream"
:> StreamGet NoFraming OctetStream (SourceIO Message),
putQue ::
path
- :- Capture "ns" Text
+ :- RemoteHost
+ :> Header "Authorization" Text
+ :> Capture "ns" Text
:> Capture "quepath" Text
:> ReqBody '[PlainText, HTML, OctetStream] Text
:> Post '[PlainText, HTML, OctetStream] NoContent
@@ -149,15 +181,15 @@ data Paths path = Paths
deriving (Generic)
paths :: Config -> Paths (AsServerT App)
-paths _ =
- -- TODO revive authkey stuff
- -- - read Authorization header, compare with queSkey
- -- - Only allow my IP or localhost to publish to '_' namespace
+paths Config {..} =
Paths
{ home =
throwError <| err301 {errHeaders = [("Location", "/_/index")]},
- dash = gets,
- getQue = \ns qn -> do
+ dash = \rh mAuth -> do
+ checkAuth queSkey rh mAuth "_"
+ gets,
+ getQue = \rh mAuth ns qn -> do
+ checkAuth queSkey rh mAuth ns
guardNs ns ["pub", "_"]
modify <| upsertNamespace ns
q <- que ns qn
@@ -165,7 +197,8 @@ paths _ =
|> liftIO
+> Go.tap
|> liftIO,
- getStream = \ns qn -> do
+ getStream = \rh mAuth ns qn -> do
+ checkAuth queSkey rh mAuth ns
guardNs ns ["pub", "_"]
modify <| upsertNamespace ns
q <- que ns qn
@@ -174,7 +207,8 @@ paths _ =
+> Go.tap
|> Source.fromAction (const False) -- peek chan instead of False?
|> pure,
- putQue = \ns qp body -> do
+ putQue = \rh mAuth ns qp body -> do
+ checkAuth queSkey rh mAuth ns
guardNs ns ["pub", "_"]
modify <| upsertNamespace ns
q <- que ns qp
@@ -188,6 +222,19 @@ paths _ =
>> pure NoContent
}
+checkAuth :: Text -> SockAddr -> Maybe Text -> Text -> App ()
+checkAuth skey rh mAuth ns = do
+ let authorized = mAuth == Just skey
+ let isLocal = isLocalhost rh
+ when (ns == "_" && not (authorized || isLocal)) <| do
+ throwError err401 {errBody = "Authorized access only for '_' namespace"}
+
+isLocalhost :: SockAddr -> Bool
+isLocalhost (SockAddrInet _ h) = h == 0x0100007f -- 127.0.0.1
+isLocalhost (SockAddrInet6 _ _ (0, 0, 0, 1) _) = True -- ::1
+isLocalhost (SockAddrUnix _) = True
+isLocalhost _ = False
+
-- | Given `guardNs ns whitelist`, if `ns` is not in the `whitelist`
-- list, return a 405 error.
guardNs :: (Applicative a, MonadError ServerError a) => Text -> [Text] -> a ()
diff --git a/Network/Wai/Middleware/Braid.hs b/Network/Wai/Middleware/Braid.hs
index 5dbc7f4..c14e099 100644
--- a/Network/Wai/Middleware/Braid.hs
+++ b/Network/Wai/Middleware/Braid.hs
@@ -57,11 +57,13 @@ import Data.ByteString.Builder (Builder, byteString)
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as L
import qualified Data.CaseInsensitive as CI
+import Data.Char (isDigit)
import Network.HTTP.Types.Header (Header, HeaderName, RequestHeaders)
import Network.HTTP.Types.Method (methodGet, methodPatch, methodPut)
import Network.HTTP.Types.Status (Status, mkStatus)
import qualified Network.Wai as Wai
import Network.Wai.Middleware.AddHeaders (addHeaders)
+import System.Timeout (timeout)
type Topic = [Text]
@@ -101,10 +103,13 @@ hSub = "Subscribe"
getSubscription :: Wai.Request -> Maybe B.ByteString
getSubscription req = lookupHeader hSub <| Wai.requestHeaders req
-getSubscriptionKeepAliveTime :: Wai.Request -> B.ByteString
+getSubscriptionKeepAliveTime :: Wai.Request -> Maybe Int
getSubscriptionKeepAliveTime req =
- let Just s = lookupHeader hSub <| Wai.requestHeaders req
- in snd <| BC.breakSubstring "=" s
+ lookupHeader hSub (Wai.requestHeaders req) +> \h ->
+ let (_, rest) = BC.breakSubstring "keep-alive=" h
+ in if B.null rest
+ then Nothing
+ else readMaybe <| BC.unpack <| BC.takeWhile isDigit <| B.drop 11 rest
hasSubscription :: Wai.Request -> Bool
hasSubscription req = isJust <| getSubscription req
@@ -222,15 +227,21 @@ addPatchHeader = Wai.ifRequest isPutRequest <| addHeaders [("Patches", "OK")]
-- |
-- TODO: look into Chan vs BroadcastChan (https://github.com/merijn/broadcast-chan)
-streamUpdates :: Chan Update -> Topic -> Maybe ByteString -> Wai.StreamingBody
-streamUpdates chan topic client write flush = do
+streamUpdates :: Chan Update -> Topic -> Maybe ByteString -> Maybe Int -> Wai.StreamingBody
+streamUpdates chan topic client keepAlive write flush = do
flush
src <- liftIO <| dupChan chan
fix <| \loop -> do
- update <- readChan src
- case updateToBuilder topic client update of
- Just b -> write b >> flush >> loop
- Nothing -> loop
+ update <-
+ case keepAlive of
+ Just t -> timeout (t * 1000000) (readChan src)
+ Nothing -> Just </ readChan src
+ case update of
+ Just u ->
+ case updateToBuilder topic client u of
+ Just b -> write b >> flush >> loop
+ Nothing -> loop
+ Nothing -> write (byteString ": \n") >> flush >> loop
braidify :: Chan Update -> Wai.Middleware
braidify src =
diff --git a/Omni/Agent/Git.hs b/Omni/Agent/Git.hs
new file mode 100644
index 0000000..a7afb20
--- /dev/null
+++ b/Omni/Agent/Git.hs
@@ -0,0 +1,138 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Git operations for the agent.
+--
+-- : out omni-agent-git
+-- : dep temporary
+module Omni.Agent.Git
+ ( checkout,
+ main,
+ test,
+ )
+where
+
+import Alpha
+import qualified Data.Text as Text
+import qualified Omni.Log as Log
+import Omni.Test ((@=?))
+import qualified Omni.Test as Test
+import qualified System.Directory as Directory
+import qualified System.Exit as Exit
+import qualified System.IO.Temp as Temp
+import qualified System.Process as Process
+
+main :: IO ()
+main = Test.run test
+
+test :: Test.Tree
+test =
+ Test.group
+ "Omni.Agent.Git"
+ [ Test.unit "checkout works" <| do
+ Temp.withSystemTempDirectory "omni-agent-git-test" <| \tmpDir -> do
+ let repo = tmpDir <> "/repo"
+ Directory.createDirectory repo
+ -- init repo
+ git repo ["init"]
+ git repo ["branch", "-m", "master"]
+ git repo ["config", "user.email", "you@example.com"]
+ git repo ["config", "user.name", "Your Name"]
+
+ -- commit A
+ writeFile (repo <> "/a.txt") "A"
+ git repo ["add", "a.txt"]
+ git repo ["commit", "-m", "A"]
+ shaA <- getSha repo "HEAD"
+
+ -- create branch dev
+ git repo ["checkout", "-b", "dev"]
+
+ -- commit B
+ writeFile (repo <> "/b.txt") "B"
+ git repo ["add", "b.txt"]
+ git repo ["commit", "-m", "B"]
+ shaB <- getSha repo "HEAD"
+
+ -- switch back to master
+ git repo ["checkout", "master"]
+
+ -- Test 1: checkout dev
+ checkout repo "dev"
+ current <- getSha repo "HEAD"
+ shaB @=? current
+
+ -- Test 2: checkout master
+ checkout repo "master"
+ current' <- getSha repo "HEAD"
+ shaA @=? current'
+
+ -- Test 3: dirty state
+ writeFile (repo <> "/a.txt") "DIRTY"
+ checkout repo "dev"
+ current'' <- getSha repo "HEAD"
+ shaB @=? current''
+ -- Verify dirty file is gone/overwritten (b.txt should exist, a.txt should be A from master? No, a.txt is in A and B)
+ -- Wait, in dev, a.txt is "A".
+ content <- readFile (repo <> "/a.txt")
+ "A" @=? content
+
+ -- Test 4: untracked file
+ writeFile (repo <> "/untracked.txt") "DELETE ME"
+ checkout repo "master"
+ exists <- Directory.doesFileExist (repo <> "/untracked.txt")
+ False @=? exists
+ ]
+
+getSha :: FilePath -> String -> IO String
+getSha dir ref = do
+ let cmd = (Process.proc "git" ["rev-parse", ref]) {Process.cwd = Just dir}
+ (code, out, _) <- Process.readCreateProcessWithExitCode cmd ""
+ case code of
+ Exit.ExitSuccess -> pure <| strip out
+ _ -> panic "getSha failed"
+
+-- | Checkout a specific ref (SHA, branch, tag) in the given repository path.
+-- This function ensures the repository is in the correct state by:
+-- 1. Fetching all updates
+-- 2. Checking out the ref (forcing overwrites of local changes)
+-- 3. Resetting hard to the ref (to ensure clean state)
+-- 4. Cleaning untracked files
+-- 5. Updating submodules
+checkout :: FilePath -> Text -> IO ()
+checkout repoPath ref = do
+ let r = Text.unpack ref
+
+ Log.info ["git", "checkout", ref, "in", Text.pack repoPath]
+
+ -- Fetch all refs to ensure we have the target
+ git repoPath ["fetch", "--all", "--tags"]
+
+ -- Checkout the ref, discarding local changes
+ git repoPath ["checkout", "--force", r]
+
+ -- Reset hard to ensure we are exactly at the target state
+ git repoPath ["reset", "--hard", r]
+
+ -- Remove untracked files and directories
+ git repoPath ["clean", "-fdx"]
+
+ -- Update submodules
+ git repoPath ["submodule", "update", "--init", "--recursive"]
+
+ Log.good ["git", "checkout", "complete"]
+ Log.br
+
+-- | Run a git command in the given directory.
+git :: FilePath -> [String] -> IO ()
+git dir args = do
+ let cmd = (Process.proc "git" args) {Process.cwd = Just dir}
+ (exitCode, out, err) <- Process.readCreateProcessWithExitCode cmd ""
+ case exitCode of
+ Exit.ExitSuccess -> pure ()
+ Exit.ExitFailure code -> do
+ Log.fail ["git command failed", Text.pack (show args), "code: " <> show code]
+ Log.info [Text.pack out]
+ Log.info [Text.pack err]
+ Log.br
+ panic <| "git command failed: git " <> show args
diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs
new file mode 100644
index 0000000..c93479b
--- /dev/null
+++ b/Omni/Agent/Log.hs
@@ -0,0 +1,71 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Omni.Agent.Log
+ ( LogEntry (..),
+ parseLine,
+ format,
+ )
+where
+
+import Alpha
+import Data.Aeson (FromJSON (..), (.:), (.:?))
+import qualified Data.Aeson as Aeson
+import qualified Data.ByteString.Lazy as BSL
+
+data LogEntry = LogEntry
+ { leMessage :: Text,
+ leLevel :: Maybe Text,
+ leToolName :: Maybe Text,
+ leBatches :: Maybe [[Text]],
+ leMethod :: Maybe Text,
+ lePath :: Maybe Text
+ }
+ deriving (Show, Eq, Generic)
+
+instance FromJSON LogEntry where
+ parseJSON =
+ Aeson.withObject "LogEntry" <| \v ->
+ ( LogEntry
+ </ (v .: "message")
+ )
+ <*> v
+ .:? "level"
+ <*> v
+ .:? "toolName"
+ <*> v
+ .:? "batches"
+ <*> v
+ .:? "method"
+ <*> v
+ .:? "path"
+
+parseLine :: Text -> Maybe LogEntry
+parseLine line = Aeson.decode <| BSL.fromStrict <| encodeUtf8 line
+
+format :: LogEntry -> Maybe Text
+format e =
+ case leMessage e of
+ "executing 1 tools in 1 batch(es)" ->
+ let tool = case leBatches e of
+ Just ((t : _) : _) -> t
+ _ -> "unknown"
+ in Just <| "🤖 THOUGHT: Planning tool execution (" <> tool <> ")"
+ "Tool Bash permitted - action: allow" ->
+ Just "🔧 TOOL: Bash command executed"
+ msg
+ | "Processing tool completion for ledger" == msg && isJust (leToolName e) ->
+ Just <| "✅ TOOL: " <> fromMaybe "" (leToolName e) <> " completed"
+ "ide-fs" ->
+ case leMethod e of
+ Just "readFile" -> Just <| "📂 READ: " <> fromMaybe "" (lePath e)
+ _ -> Nothing
+ "System prompt build complete (no changes)" ->
+ Just "🧠 THINKING..."
+ "System prompt build complete (first build)" ->
+ Just "🚀 STARTING new task context"
+ msg ->
+ case leLevel e of
+ Just "error" -> Just <| "❌ ERROR: " <> msg
+ _ -> Nothing
diff --git a/Omni/Agent/LogTest.hs b/Omni/Agent/LogTest.hs
new file mode 100644
index 0000000..0d085b1
--- /dev/null
+++ b/Omni/Agent/LogTest.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- : out agent-log-test
+module Omni.Agent.LogTest where
+
+import Alpha
+import Omni.Agent.Log
+import qualified Omni.Test as Test
+
+main :: IO ()
+main = Test.run tests
+
+tests :: Test.Tree
+tests =
+ Test.group
+ "Omni.Agent.Log"
+ [ Test.unit "Parse LogEntry" testParse,
+ Test.unit "Format LogEntry" testFormat
+ ]
+
+testParse :: IO ()
+testParse = do
+ let json = "{\"message\": \"executing 1 tools in 1 batch(es)\", \"batches\": [[\"grep\"]]}"
+ let expected =
+ LogEntry
+ { leMessage = "executing 1 tools in 1 batch(es)",
+ leLevel = Nothing,
+ leToolName = Nothing,
+ leBatches = Just [["grep"]],
+ leMethod = Nothing,
+ lePath = Nothing
+ }
+ parseLine json @?= Just expected
+
+testFormat :: IO ()
+testFormat = do
+ let entry =
+ LogEntry
+ { leMessage = "executing 1 tools in 1 batch(es)",
+ leLevel = Nothing,
+ leToolName = Nothing,
+ leBatches = Just [["grep"]],
+ leMethod = Nothing,
+ lePath = Nothing
+ }
+ format entry @?= Just "🤖 THOUGHT: Planning tool execution (grep)"
+
+ let entry2 =
+ LogEntry
+ { leMessage = "some random log",
+ leLevel = Nothing,
+ leToolName = Nothing,
+ leBatches = Nothing,
+ leMethod = Nothing,
+ lePath = Nothing
+ }
+ format entry2 @?= Nothing
+
+ let entry3 =
+ LogEntry
+ { leMessage = "some error",
+ leLevel = Just "error",
+ leToolName = Nothing,
+ leBatches = Nothing,
+ leMethod = Nothing,
+ lePath = Nothing
+ }
+ format entry3 @?= Just "❌ ERROR: some error"
+
+(@?=) :: (Eq a, Show a) => a -> a -> IO ()
+(@?=) = (Test.@?=)
diff --git a/Omni/Agent/WORKER_AGENT_GUIDE.md b/Omni/Agent/WORKER_AGENT_GUIDE.md
index af81bb0..5bae08f 100644
--- a/Omni/Agent/WORKER_AGENT_GUIDE.md
+++ b/Omni/Agent/WORKER_AGENT_GUIDE.md
@@ -55,13 +55,10 @@ task update t-123 in-progress
2. **Check for Unmerged Work**: Look for dependencies that have existing branches (e.g., `task/t-parent-id`) which are NOT yet merged into `live`.
3. **Select Base**:
* If you find an unmerged dependency branch, check it out: `git checkout task/t-parent-id`.
- * Otherwise, start from fresh live code: `git checkout omni-worker-1` (which tracks `live`).
+ * Otherwise, start from fresh live code: `git fetch origin live && git checkout -b task/t-123 origin/live`.
-4. **Create/Checkout Feature Branch**:
- ```bash
- # Try to switch to existing branch, otherwise create new one
- git checkout task/t-123 || git checkout -b task/t-123
- ```
+4. **Implement**:
+ (Proceed to implementation)
### Step 4: Implement
@@ -70,29 +67,35 @@ task update t-123 in-progress
3. **Run Tests**: `bild --test Omni/YourNamespace.hs`
### Step 5: Submit for Review
-
-1. **Commit Implementation**:
- ```bash
- git add .
- git commit -m "feat: implement t-123"
- ```
-
-2. **Signal Review Readiness**:
- The Planner checks the `omni-worker-X` branch for status updates. You must switch back and update the status there.
-
- ```bash
- # Switch to base branch
- git checkout omni-worker-1
-
- # Sync to get latest state (and any manual merges)
- ./Omni/Agent/sync-tasks.sh
-
- # Mark task for review
- task update t-123 review
-
- # Commit this status change to the worker branch
- ./Omni/Agent/sync-tasks.sh --commit
- ```
+
+ 1. **Update Status and Commit**:
+ Bundle the task status update with your implementation to keep history clean.
+
+ ```bash
+ # 1. Mark task for review (updates .tasks/tasks.jsonl)
+ task update t-123 review
+
+ # 2. Commit changes + task update
+ git add .
+ git commit -m "feat: implement t-123"
+ ```
+
+ 2. **Signal Review Readiness**:
+ Update the worker branch to signal the planner.
+
+ ```bash
+ # Switch to base branch
+ git checkout omni-worker-1
+
+ # Sync to get latest state
+ ./Omni/Agent/sync-tasks.sh
+
+ # Ensure the task is marked review here too (for harvest visibility)
+ task update t-123 review
+
+ # Commit this status change to the worker branch
+ ./Omni/Agent/sync-tasks.sh --commit
+ ```
*Note: The Planner will now see 't-123' in 'Review' when it runs `harvest-tasks.sh`.*
diff --git a/Omni/Agent/harvest-tasks.sh b/Omni/Agent/harvest-tasks.sh
index 282beab..44c2322 100755
--- a/Omni/Agent/harvest-tasks.sh
+++ b/Omni/Agent/harvest-tasks.sh
@@ -45,7 +45,14 @@ if [ "$UPDATED" -eq 1 ]; then
# Commit if there are changes
if [[ -n $(git status --porcelain .tasks/tasks.jsonl) ]]; then
git add .tasks/tasks.jsonl
- git commit -m "task: harvest updates from workers"
+
+ LAST_MSG=$(git log -1 --pretty=%s)
+ if [[ "$LAST_MSG" == "task: harvest updates from workers" ]]; then
+ echo "Squashing with previous harvest commit..."
+ git commit --amend --no-edit
+ else
+ git commit -m "task: harvest updates from workers"
+ fi
echo "Success: Task database updated and committed."
else
echo "No effective changes found."
diff --git a/Omni/Agent/setup-worker.sh b/Omni/Agent/setup-worker.sh
index 28c29b1..42b7fc9 100755
--- a/Omni/Agent/setup-worker.sh
+++ b/Omni/Agent/setup-worker.sh
@@ -22,3 +22,10 @@ if [ -f "$REPO_ROOT/.envrc.local" ]; then
echo "Copying .envrc.local..."
cp "$REPO_ROOT/.envrc.local" "$WORKTREE_PATH/"
fi
+
+# Configure git identity for the worker
+echo "Configuring git identity for worker..."
+git -C "$WORKTREE_PATH" config user.name "Omni Worker"
+git -C "$WORKTREE_PATH" config user.email "bot@omni.agent"
+
+echo "Worker setup complete at $WORKTREE_PATH"
diff --git a/Omni/Bild.hs b/Omni/Bild.hs
index 53d96a5..ba54c93 100644
--- a/Omni/Bild.hs
+++ b/Omni/Bild.hs
@@ -653,7 +653,8 @@ analyzeAll isPlanMode nss = do
contentLines
|> Meta.detectAll "--"
|> \Meta.Parsed {..} ->
- detectHaskellImports mempty contentLines +> \(langdeps, srcs) -> do
+ detectHaskellImports mempty contentLines +> \(autoDeps, srcs) -> do
+ let langdeps = autoDeps <> pdep
graph <- buildHsModuleGraph namespace quapath srcs
pure
<| Just
diff --git a/Omni/Bild/Deps/Haskell.nix b/Omni/Bild/Deps/Haskell.nix
index 5d6abbb..6930860 100644
--- a/Omni/Bild/Deps/Haskell.nix
+++ b/Omni/Bild/Deps/Haskell.nix
@@ -54,6 +54,7 @@
"tasty"
"tasty-hunit"
"tasty-quickcheck"
+ "temporary"
"text"
"time"
"transformers"
diff --git a/Omni/Task.hs b/Omni/Task.hs
index 4a36dcf..01b5ad9 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -45,7 +45,8 @@ Usage:
task update <id> <status> [--json]
task deps <id> [--json]
task tree [<id>] [--json]
- task stats [--json]
+ task progress <id> [--json]
+ task stats [--epic=<id>] [--json]
task export [--flush]
task import -i <file>
task sync
@@ -61,6 +62,7 @@ Commands:
update Update task status
deps Show dependency tree
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
import Import tasks from JSONL file
@@ -73,6 +75,7 @@ Options:
--parent=<id> Parent epic ID
--priority=<p> Priority: 0-4 (0=critical, 4=backlog, default: 2)
--status=<status> Filter by status: open, in-progress, review, done
+ --epic=<id> Filter stats by epic (recursive)
--deps=<ids> Comma-separated list of dependency IDs
--dep-type=<type> Dependency type: blocks, discovered-from, parent-child, related (default: blocks)
--discovered-from=<id> Shortcut for --deps=<id> --dep-type=discovered-from
@@ -231,12 +234,22 @@ move args
tree <- getTaskTree maybeId
outputJson tree
else showTaskTree maybeId
+ | args `Cli.has` Cli.command "progress" = do
+ tid <- getArgText args "id"
+ if isJsonMode args
+ then do
+ progress <- getTaskProgress tid
+ outputJson progress
+ else showTaskProgress tid
| args `Cli.has` Cli.command "stats" = do
+ maybeEpic <- case Cli.getArg args (Cli.longOption "epic") of
+ Nothing -> pure Nothing
+ Just e -> pure <| Just (T.pack e)
if isJsonMode args
then do
- stats <- getTaskStats
+ stats <- getTaskStats maybeEpic
outputJson stats
- else showTaskStats
+ else showTaskStats maybeEpic
| args `Cli.has` Cli.command "export" = do
exportTasks
putText "Exported and consolidated tasks to .tasks/tasks.jsonl"
@@ -336,20 +349,21 @@ unitTests =
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
- }
+ 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",
@@ -525,6 +539,13 @@ cliTests =
Right args -> do
args `Cli.has` Cli.command "stats" Test.@?= True
args `Cli.has` Cli.longOption "json" Test.@?= True,
+ Test.unit "stats with --epic flag" <| do
+ let result = Docopt.parseArgs help ["stats", "--epic=t-abc123"]
+ case result of
+ Left err -> Test.assertFailure <| "Failed to parse 'stats --epic': " <> show err
+ Right args -> do
+ args `Cli.has` Cli.command "stats" Test.@?= True
+ Cli.getArg args (Cli.longOption "epic") Test.@?= Just "t-abc123",
Test.unit "create with flags in different order" <| do
let result = Docopt.parseArgs help ["create", "Test", "--json", "--priority=1", "--namespace=Omni/Task"]
case result of
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index 2b00bca..3f665da 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -57,6 +57,14 @@ data DependencyType
| Related -- Soft relationship, doesn't block
deriving (Show, Eq, Generic)
+data TaskProgress = TaskProgress
+ { progressTaskId :: Text,
+ progressTotal :: Int,
+ progressCompleted :: Int,
+ progressPercentage :: Int
+ }
+ deriving (Show, Eq, Generic)
+
instance ToJSON TaskType
instance FromJSON TaskType
@@ -87,7 +95,11 @@ matchesId id1 id2 = T.toLower id1 == T.toLower id2
-- | Find a task by ID (case-insensitive)
findTask :: Text -> [Task] -> Maybe Task
-findTask tid tasks = List.find (\t -> matchesId (taskId t) tid) tasks
+findTask tid = List.find (\t -> matchesId (taskId t) tid)
+
+instance ToJSON TaskProgress
+
+instance FromJSON TaskProgress
-- Get the tasks database file path (use test file if TASK_TEST_MODE is set)
getTasksFilePath :: IO FilePath
@@ -317,6 +329,32 @@ getDependencyTree tid = do
deps = filter (\t -> any (matchesId (taskId t)) depIds) allTasks
in task : concatMap (collectDeps allTasks) deps
+-- Get task progress
+getTaskProgress :: Text -> IO TaskProgress
+getTaskProgress tid = do
+ tasks <- loadTasks
+ -- Verify task exists (optional, but good for error handling)
+ case filter (\t -> taskId t == tid) tasks of
+ [] -> panic "Task not found"
+ _ -> do
+ let children = filter (\child -> taskParent child == Just tid) tasks
+ total = length children
+ completed = length <| filter (\child -> taskStatus child == Done) children
+ percentage = if total == 0 then 0 else (completed * 100) `div` total
+ pure
+ TaskProgress
+ { progressTaskId = tid,
+ progressTotal = total,
+ progressCompleted = completed,
+ 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
@@ -349,7 +387,7 @@ getTaskTree maybeId = do
where
collectChildren :: [Task] -> Task -> [Task]
collectChildren allTasks task =
- let children = filter (\t -> maybe False (`matchesId` taskId task) (taskParent t)) allTasks
+ 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)
@@ -377,7 +415,7 @@ showTaskTree maybeId = do
printTreeNode' :: [Task] -> Task -> Int -> [Bool] -> IO ()
printTreeNode' allTasks task indent ancestry = do
- let children = filter (\t -> maybe False (`matchesId` taskId task) (taskParent t)) allTasks
+ let children = filter (maybe False (`matchesId` taskId task) <. taskParent) allTasks
-- Build tree prefix using box-drawing characters
prefix =
if indent == 0
@@ -426,7 +464,7 @@ printTask t = do
let progressInfo =
if taskType t == Epic
then
- let children = filter (\child -> maybe False (`matchesId` taskId t) (taskParent child)) tasks
+ let children = filter (maybe False (`matchesId` taskId t) <. taskParent) tasks
total = length children
completed = length <| filter (\child -> taskStatus child == Done) children
in " [" <> T.pack (show completed) <> "/" <> T.pack (show total) <> "]"
@@ -464,7 +502,7 @@ showTaskDetailed t = do
-- Show epic progress if this is an epic
when (taskType t == Epic) <| do
- let children = filter (\child -> maybe False (`matchesId` taskId t) (taskParent child)) tasks
+ let children = filter (maybe False (`matchesId` taskId t) <. taskParent) tasks
total = length children
completed = length <| filter (\child -> taskStatus child == Done) children
percentage = if total == 0 then 0 else (completed * 100) `div` total
@@ -526,18 +564,31 @@ instance ToJSON TaskStats
instance FromJSON TaskStats
-- Get task statistics
-getTaskStats :: IO TaskStats
-getTaskStats = do
- tasks <- loadTasks
- ready <- getReadyTasks
- let total = length tasks
+getTaskStats :: Maybe Text -> IO TaskStats
+getTaskStats maybeEpicId = do
+ allTasks <- loadTasks
+
+ targetTasks <- case maybeEpicId of
+ Nothing -> pure allTasks
+ Just epicId ->
+ case findTask epicId allTasks of
+ Nothing -> panic "Epic not found"
+ Just task -> pure <| getAllDescendants allTasks (taskId task)
+
+ 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
+ total = length tasks
open = length <| filter (\t -> taskStatus t == Open) tasks
inProg = length <| filter (\t -> taskStatus t == InProgress) tasks
review = length <| filter (\t -> taskStatus t == Review) tasks
done = length <| filter (\t -> taskStatus t == Done) tasks
epics = length <| filter (\t -> taskType t == Epic) tasks
- readyCount = length ready
- blockedCount = total - readyCount - done
+ readyCount' = readyCount
+ blockedCount = total - readyCount' - done
-- Count tasks by priority
byPriority =
[ (P0, length <| filter (\t -> taskPriority t == P0) tasks),
@@ -558,18 +609,26 @@ getTaskStats = do
reviewTasks = review,
doneTasks = done,
totalEpics = epics,
- readyTasks = readyCount,
+ readyTasks = readyCount',
blockedTasks = blockedCount,
tasksByPriority = byPriority,
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 :: IO ()
-showTaskStats = do
- stats <- getTaskStats
+showTaskStats :: Maybe Text -> IO ()
+showTaskStats maybeEpicId = do
+ stats <- getTaskStats maybeEpicId
putText ""
- putText "Task Statistics"
+ case maybeEpicId of
+ Nothing -> putText "Task Statistics"
+ Just epicId -> putText <| "Task Statistics for Epic " <> epicId
putText ""
putText <| "Total tasks: " <> T.pack (show (totalTasks stats))
putText <| " Open: " <> T.pack (show (openTasks stats))