summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOmni Worker <bot@omni.agent>2025-11-22 05:55:10 -0500
committerBen Sima <ben@bensima.com>2025-11-22 12:46:07 -0500
commit7c649a257aa79f01bcaf989191a065acf8105132 (patch)
tree840bf87ce42111e36c5b8e5441ddffa8617f3dcd
parent1fcf654b30c5ca9e1d680805df40435e6fe20e4e (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.jsonl11
-rw-r--r--.tasks/tasks.jsonl2
-rw-r--r--Omni/Task.hs104
-rw-r--r--Omni/Task/Core.hs28
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