diff options
| author | Omni Worker <bot@omni.agent> | 2025-11-22 05:55:10 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-11-22 12:46:07 -0500 |
| commit | 7c649a257aa79f01bcaf989191a065acf8105132 (patch) | |
| tree | 840bf87ce42111e36c5b8e5441ddffa8617f3dcd | |
| parent | 1fcf654b30c5ca9e1d680805df40435e6fe20e4e (diff) | |
Implement task edit command
Amp-Thread-ID:
https://ampcode.com/threads/T-a65df310-235f-4d63-9f78-4affc537b80b
Co-authored-by: Amp <amp@ampcode.com>
| -rw-r--r-- | .tasks/race-test.jsonl | 11 | ||||
| -rw-r--r-- | .tasks/tasks.jsonl | 2 | ||||
| -rw-r--r-- | Omni/Task.hs | 104 | ||||
| -rw-r--r-- | Omni/Task/Core.hs | 28 |
4 files changed, 140 insertions, 5 deletions
diff --git a/.tasks/race-test.jsonl b/.tasks/race-test.jsonl new file mode 100644 index 0000000..a7bc9ab --- /dev/null +++ b/.tasks/race-test.jsonl @@ -0,0 +1,11 @@ +{"taskCreatedAt":"2025-11-22T10:55:59.216273867Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Parent Epic","taskType":"Epic","taskUpdatedAt":"2025-11-22T10:55:59.216273867Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.216886313Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.1","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 1","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.216886313Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.217310198Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.2","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 2","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.217310198Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.217861324Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.3","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 3","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.217861324Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.21845605Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.4","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 4","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.21845605Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.219104117Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.5","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 5","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.219104117Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.219911335Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.6","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 6","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.219911335Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.220817855Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.7","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 7","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.220817855Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.222028498Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.8","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 8","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.222028498Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.223023128Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.9","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 9","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.223023128Z"} +{"taskCreatedAt":"2025-11-22T10:55:59.224075759Z","taskDependencies":[],"taskDescription":null,"taskId":"t-rWcncGeAg.10","taskNamespace":null,"taskParent":"t-rWcncGeAg","taskPriority":"P2","taskStatus":"Open","taskTitle":"Child 10","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T10:55:59.224075759Z"} diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index 1ac4d14..5e32bcb 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -170,7 +170,7 @@ {"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":null,"taskId":"t-rWcpygi7d","taskNamespace":null,"taskParent":null,"taskPriority":"P2","taskStatus":"Open","taskTitle":"Test Lowercase","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T11:31:50.378377038Z"} +{"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":"Open","taskTitle":"Test Lowercase","taskType":"WorkTask","taskUpdatedAt":"2025-11-22T17:39:24.351019865Z"} {"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"} diff --git a/Omni/Task.hs b/Omni/Task.hs index e1457fb..6edd161 100644 --- a/Omni/Task.hs +++ b/Omni/Task.hs @@ -41,6 +41,7 @@ task Usage: task init [--quiet] task create <title> [options] + task edit <id> [options] task list [options] task ready [--json] task show <id> [--json] @@ -58,6 +59,7 @@ Usage: Commands: init Initialize task database create Create a new task or epic + edit Edit an existing task list List all tasks ready Show ready tasks (not blocked) show Show detailed task information @@ -73,13 +75,14 @@ Commands: Options: -h --help Show this help - --type=<type> Task type: epic or task (default: task) + --title=<title> Task title + --type=<type> Task type: epic or task --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 + --priority=<p> Priority: 0-4 (0=critical, 4=backlog) + --status=<status> Task 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) + --dep-type=<type> Dependency type: blocks, discovered-from, parent-child, related --discovered-from=<id> Shortcut for --deps=<id> --dep-type=discovered-from --namespace=<ns> Optional namespace (e.g., Omni/Task, Biz/Cloud) --description=<desc> Task description @@ -169,6 +172,71 @@ move args if isJsonMode args then outputJson createdTask else putStrLn <| "Created task: " <> T.unpack (taskId createdTask) + | args `Cli.has` Cli.command "edit" = do + tid <- getArgText args "id" + + -- Parse optional edits + maybeTitle <- pure <| Cli.getArg args (Cli.longOption "title") + maybeType <- case Cli.getArg args (Cli.longOption "type") of + Nothing -> pure Nothing + Just "epic" -> pure <| Just Epic + Just "task" -> pure <| Just WorkTask + Just other -> panic <| "Invalid task type: " <> T.pack other <> ". Use: epic or task" + maybeParent <- pure <| fmap T.pack (Cli.getArg args (Cli.longOption "parent")) + maybePriority <- case Cli.getArg args (Cli.longOption "priority") of + Nothing -> pure Nothing + Just "0" -> pure <| Just P0 + Just "1" -> pure <| Just P1 + Just "2" -> pure <| Just P2 + Just "3" -> pure <| Just P3 + Just "4" -> pure <| Just P4 + Just other -> panic <| "Invalid priority: " <> T.pack other <> ". Use: 0-4" + maybeStatus <- case Cli.getArg args (Cli.longOption "status") of + Nothing -> pure Nothing + Just "open" -> pure <| Just Open + Just "in-progress" -> pure <| Just InProgress + Just "review" -> pure <| Just Review + Just "done" -> pure <| Just Done + Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: open, in-progress, review, or done" + maybeNamespace <- case Cli.getArg args (Cli.longOption "namespace") of + Nothing -> pure Nothing + Just ns -> do + let validNs = Namespace.fromHaskellModule ns + nsPath = T.pack <| Namespace.toPath validNs + pure <| Just nsPath + maybeDesc <- pure <| fmap T.pack (Cli.getArg args (Cli.longOption "description")) + + maybeDeps <- case Cli.getArg args (Cli.longOption "discovered-from") of + Just discoveredId -> pure <| Just [Dependency {depId = T.pack discoveredId, depType = DiscoveredFrom}] + Nothing -> case Cli.getArg args (Cli.longOption "deps") of + Nothing -> pure Nothing + Just depStr -> do + let ids = T.splitOn "," (T.pack depStr) + dtype <- case Cli.getArg args (Cli.longOption "dep-type") of + Nothing -> pure Blocks + Just "blocks" -> pure Blocks + Just "discovered-from" -> pure DiscoveredFrom + Just "parent-child" -> pure ParentChild + Just "related" -> pure Related + Just other -> panic <| "Invalid dependency type: " <> T.pack other + pure <| Just (map (\did -> Dependency {depId = did, depType = dtype}) ids) + + let modifyFn task = + task + { taskTitle = maybe (taskTitle task) T.pack maybeTitle, + taskType = fromMaybe (taskType task) maybeType, + taskParent = case maybeParent of Nothing -> taskParent task; Just p -> Just p, + taskNamespace = case maybeNamespace of Nothing -> taskNamespace task; Just ns -> Just ns, + taskStatus = fromMaybe (taskStatus task) maybeStatus, + taskPriority = fromMaybe (taskPriority task) maybePriority, + taskDescription = case maybeDesc of Nothing -> taskDescription task; Just d -> Just d, + taskDependencies = fromMaybe (taskDependencies task) maybeDeps + } + + updatedTask <- editTask tid modifyFn + if isJsonMode args + then outputJson updatedTask + else putStrLn <| "Updated task: " <> T.unpack (taskId updatedTask) | args `Cli.has` Cli.command "list" = do maybeType <- case Cli.getArg args (Cli.longOption "type") of Nothing -> pure Nothing @@ -402,6 +470,19 @@ unitTests = -- Create a new child, it should get .4, not .2 child4 <- createTask "Child 4" WorkTask (Just (taskId parent)) Nothing P2 [] Nothing taskId child4 Test.@?= taskId parent <> ".4", + Test.unit "can edit task" <| do + task <- createTask "Original Title" WorkTask Nothing Nothing P2 [] Nothing + let modifyFn t = t {taskTitle = "New Title", taskPriority = P0} + updated <- editTask (taskId task) modifyFn + taskTitle updated Test.@?= "New Title" + taskPriority updated Test.@?= P0 + -- Check persistence + tasks <- loadTasks + case findTask (taskId task) tasks of + Nothing -> Test.assertFailure "Could not reload task" + Just reloaded -> do + taskTitle reloaded Test.@?= "New Title" + taskPriority reloaded Test.@?= P0, Test.unit "task lookup is case insensitive" <| do task <- createTask "Case sensitive" WorkTask Nothing Nothing P2 [] Nothing let tid = taskId task @@ -493,6 +574,21 @@ cliTests = Right args -> do args `Cli.has` Cli.command "create" Test.@?= True Cli.getArg args (Cli.longOption "priority") Test.@?= Just "1", + Test.unit "edit command" <| do + let result = Docopt.parseArgs help ["edit", "t-abc123"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'edit': " <> show err + Right args -> do + args `Cli.has` Cli.command "edit" Test.@?= True + Cli.getArg args (Cli.argument "id") Test.@?= Just "t-abc123", + Test.unit "edit with options" <| do + let result = Docopt.parseArgs help ["edit", "t-abc123", "--title=New Title", "--priority=0"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'edit' with options: " <> show err + Right args -> do + args `Cli.has` Cli.command "edit" Test.@?= True + Cli.getArg args (Cli.longOption "title") Test.@?= Just "New Title" + Cli.getArg args (Cli.longOption "priority") Test.@?= Just "0", Test.unit "list command" <| do let result = Docopt.parseArgs help ["list"] case result of diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index b17c2aa..58744fa 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -377,6 +377,34 @@ updateTaskStatus tid newStatus newDeps = TIO.writeFile tasksFile "" traverse_ saveTaskInternal updatedTasks +-- Edit a task by applying a modification function +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 listTasks :: Maybe TaskType -> Maybe Text -> Maybe Status -> Maybe Text -> IO [Task] listTasks maybeType maybeParent maybeStatus maybeNamespace = do |
