commit 96d9eb0f5847ed4aebdb86365c4c5cad1d9e9286
Author: Coder Agent <coder@agents.omni>
Date: Wed Feb 18 13:43:40 2026
Pipeline: comment-based agent-human communication
Add DevNeedsInput variant to DevResult for when agents exit cleanly
without committing (i.e., they posted a question comment instead).
Changes:
- Core.hs: Add DevNeedsInput Text to DevResult type
- Dev.hs: Include all task comments in dev prompt; add needs-input
instructions (rule 8); checkForCommitOrInput returns DevNeedsInput
on clean exit with no commit
- Pipeline.hs: Handle DevNeedsInput in harvest phase (sets NeedsHelp);
add re-spawn logic that detects new Human comments on NeedsHelp tasks
and transitions them back to Open for dev; add 4 new tests for comment
rendering, needs-input instructions, and no-comments case
- State.hs: Add getLastDevRunTime query for comment timestamp comparison
- dev.md: Add 'Asking for Clarification' section with protocol
Task-Id: t-634
diff --git a/Omni/Ide/Workflows/dev.md b/Omni/Ide/Workflows/dev.md
index f0234e7b..11a3d576 100644
--- a/Omni/Ide/Workflows/dev.md
+++ b/Omni/Ide/Workflows/dev.md
@@ -98,6 +98,22 @@ After successful verification and commit/amend:
task update t-XXX review --json
```
+## Asking for Clarification
+
+If the task description is ambiguous or missing critical details needed to implement:
+
+1. Post a comment with your specific questions:
+ ```bash
+ task comment t-XXX 'Your specific questions here' --author agent:engineer --json
+ ```
+2. Exit cleanly (exit code 0) **without committing**.
+3. The pipeline will detect the clean exit with no commit, set the task to `NeedsHelp`, and wait for a human response.
+4. Once the human answers via a comment, the pipeline re-spawns you with the full comment history.
+
+**When to ask vs. proceed:**
+- Ask when the task is genuinely ambiguous (multiple valid interpretations) or missing key details (e.g., no target file, unclear scope).
+- Do NOT ask for trivial clarifications you can reasonably infer from context, code, or conventions.
+
## Failure Handling
If you cannot complete safely:
diff --git a/Omni/Pipeline.hs b/Omni/Pipeline.hs
index 7d5d4c56..bae4ff48 100755
--- a/Omni/Pipeline.hs
+++ b/Omni/Pipeline.hs
@@ -91,7 +91,90 @@ test =
}
prompt = Dev.buildDevPrompt "live" task 1 (Just "Build failed: missing import")
Test.assertBool "prompt contains feedback" (T.isInfixOf "missing import" prompt)
- Test.assertBool "prompt mentions previous rejection" (T.isInfixOf "previous patchset was rejected" (T.toLower prompt))
+ Test.assertBool "prompt mentions previous rejection" (T.isInfixOf "previous patchset was rejected" (T.toLower prompt)),
+ --
+ Test.unit "buildDevPrompt includes comments" <| do
+ let task =
+ Task.Task
+ { Task.taskId = "t-777",
+ Task.taskTitle = "Task with comments",
+ Task.taskType = Task.WorkTask,
+ Task.taskParent = Nothing,
+ Task.taskNamespace = Just "Omni/Foo.hs",
+ Task.taskStatus = Task.NeedsHelp,
+ Task.taskPatchsetCount = 0,
+ Task.taskPriority = Task.P2,
+ Task.taskComplexity = Nothing,
+ Task.taskDependencies = [],
+ Task.taskDescription = "Do the thing",
+ Task.taskComments =
+ [ Task.Comment
+ { Task.commentText = "What format should the output be?",
+ Task.commentAuthor = Task.Agent Task.Engineer,
+ Task.commentCreatedAt = Read.read "2026-01-01 01:00:00 UTC"
+ },
+ Task.Comment
+ { Task.commentText = "Use JSON output",
+ Task.commentAuthor = Task.Human,
+ Task.commentCreatedAt = Read.read "2026-01-01 02:00:00 UTC"
+ }
+ ],
+ Task.taskTags = Task.Tags [],
+ Task.taskCreatedAt = Read.read "2026-01-01 00:00:00 UTC",
+ Task.taskUpdatedAt = Read.read "2026-01-01 02:00:00 UTC"
+ }
+ prompt = Dev.buildDevPrompt "live" task 0 Nothing
+ Test.assertBool "prompt contains Comments section" (T.isInfixOf "### Comments" prompt)
+ Test.assertBool "prompt contains agent question" (T.isInfixOf "What format should the output be?" prompt)
+ Test.assertBool "prompt contains human answer" (T.isInfixOf "Use JSON output" prompt)
+ Test.assertBool "prompt contains human author tag" (T.isInfixOf "[human]" prompt)
+ Test.assertBool "prompt contains agent author tag" (T.isInfixOf "[agent:engineer]" prompt),
+ --
+ Test.unit "buildDevPrompt includes needs-input instructions" <| do
+ let task =
+ Task.Task
+ { Task.taskId = "t-666",
+ Task.taskTitle = "Ambiguous task",
+ Task.taskType = Task.WorkTask,
+ Task.taskParent = Nothing,
+ Task.taskNamespace = Nothing,
+ Task.taskStatus = Task.Open,
+ Task.taskPatchsetCount = 0,
+ Task.taskPriority = Task.P2,
+ Task.taskComplexity = Nothing,
+ Task.taskDependencies = [],
+ Task.taskDescription = "Do something",
+ Task.taskComments = [],
+ Task.taskTags = Task.Tags [],
+ Task.taskCreatedAt = Read.read "2026-01-01 00:00:00 UTC",
+ Task.taskUpdatedAt = Read.read "2026-01-01 00:00:00 UTC"
+ }
+ prompt = Dev.buildDevPrompt "live" task 0 Nothing
+ Test.assertBool "prompt contains needs-input instruction" (T.isInfixOf "ambiguous or missing critical details" prompt)
+ Test.assertBool "prompt mentions task comment command" (T.isInfixOf "task comment" prompt)
+ Test.assertBool "prompt mentions exit 0" (T.isInfixOf "exit 0 without committing" prompt),
+ --
+ Test.unit "buildDevPrompt omits Comments section when no comments" <| do
+ let task =
+ Task.Task
+ { Task.taskId = "t-555",
+ Task.taskTitle = "No comments task",
+ Task.taskType = Task.WorkTask,
+ Task.taskParent = Nothing,
+ Task.taskNamespace = Just "Omni/Bar.hs",
+ Task.taskStatus = Task.Open,
+ Task.taskPatchsetCount = 0,
+ Task.taskPriority = Task.P2,
+ Task.taskComplexity = Nothing,
+ Task.taskDependencies = [],
+ Task.taskDescription = "Simple task",
+ Task.taskComments = [],
+ Task.taskTags = Task.Tags [],
+ Task.taskCreatedAt = Read.read "2026-01-01 00:00:00 UTC",
+ Task.taskUpdatedAt = Read.read "2026-01-01 00:00:00 UTC"
+ }
+ prompt = Dev.buildDevPrompt "live" task 0 Nothing
+ Test.assertBool "prompt does not contain Comments section" (not (T.isInfixOf "### Comments" prompt))
]
-- ============================================================================
@@ -229,6 +312,14 @@ runOneCycle cfg db pool activeDevs = do
STM.atomically <| STM.modifyTVar' activeDevs (Map.delete tid)
-- Immediately verify
doVerify cfg db ad
+ Core.DevNeedsInput reason -> do
+ logMsg ("Dev needs input for " <> tid <> ": " <> reason)
+ cost <- Dev.getRunCost (Core.adRunId ad)
+ State.markRunFinished db (Core.adRunDbId ad) Core.Success (Just cost) (Just reason)
+ _ <- Task.addComment tid ("Pipeline: agent needs input (run=" <> Core.adRunId ad <> ")") Task.System
+ -- Remove from active, set task to NeedsHelp
+ STM.atomically <| STM.modifyTVar' activeDevs (Map.delete tid)
+ Task.updateTaskStatus tid Task.NeedsHelp []
Core.DevFailed err -> do
logMsg ("Dev failed for " <> tid <> ": " <> err)
cost <- Dev.getRunCost (Core.adRunId ad)
@@ -238,8 +329,33 @@ runOneCycle cfg db pool activeDevs = do
STM.atomically <| STM.modifyTVar' activeDevs (Map.delete tid)
Task.updateTaskStatus tid Task.Open []
- -- 3. DEVELOP: spawn agents for Open tasks
- let open = byStatus Task.Open
+ -- 3. RE-SPAWN: check NeedsHelp tasks for new human comments
+ let needsHelp = byStatus Task.NeedsHelp
+ forM_ needsHelp <| \task -> do
+ let tid = Task.taskId task
+ lastRun <- State.getLastDevRunTime db tid
+ let hasNewHumanComment = case lastRun of
+ Nothing -> False
+ Just runTime ->
+ any
+ ( \c ->
+ Task.commentAuthor c == Task.Human
+ && Task.commentCreatedAt c > runTime
+ )
+ (Task.taskComments task)
+ when hasNewHumanComment <| do
+ logMsg ("Human responded to " <> tid <> ", re-spawning dev")
+ Task.updateTaskStatus tid Task.Open []
+ _ <- Task.addComment tid "Pipeline: human responded, re-opening for dev" Task.System
+ pure ()
+
+ -- Reload tasks after NeedsHelp transitions
+ allTasks' <- if null needsHelp then pure allTasks else Task.loadTasks
+ let workTasks' = filter isEligible allTasks'
+ byStatus' s = filter (\t -> Task.taskStatus t == s) workTasks'
+
+ -- 4. DEVELOP: spawn agents for Open tasks
+ let open = byStatus' Task.Open
slots <- Workspace.slotsAvailable pool
currentActive <- STM.readTVarIO activeDevs
let availableSlots = slots - Map.size currentActive
diff --git a/Omni/Pipeline/Core.hs b/Omni/Pipeline/Core.hs
index 3ec054ae..9d21bb5c 100644
--- a/Omni/Pipeline/Core.hs
+++ b/Omni/Pipeline/Core.hs
@@ -135,6 +135,7 @@ data DevResult
= StillRunning
| DevSuccess (Maybe Text) -- new commit SHA, if changed
| DevFailed Text -- error message
+ | DevNeedsInput Text -- agent exited cleanly but posted a question (no commit)
deriving (Show, Eq)
-- | Workspace path for the integration worktree.
diff --git a/Omni/Pipeline/Dev.hs b/Omni/Pipeline/Dev.hs
index 5e9371b7..7ce5adf4 100644
--- a/Omni/Pipeline/Dev.hs
+++ b/Omni/Pipeline/Dev.hs
@@ -37,6 +37,11 @@ buildDevPrompt baseBranch task patchset maybeReviewFeedback =
"5. **Do NOT change task status.** The pipeline handles transitions.",
"6. **Do NOT push.**",
"7. Follow repository conventions from AGENTS.md.",
+ "8. If the task description is ambiguous or missing critical details needed to implement,",
+ " post a comment with your questions using:",
+ " `task comment " <> Task.taskId task <> " 'your questions' --author agent:engineer --json`",
+ " and exit 0 without committing. The pipeline will set the task to NeedsHelp",
+ " and re-spawn you once a human answers.",
"",
"## Task",
"- ID: " <> Task.taskId task,
@@ -51,6 +56,7 @@ buildDevPrompt baseBranch task patchset maybeReviewFeedback =
"### Description",
Task.taskDescription task,
"",
+ comments,
feedback,
"### Constraints",
"- Work only in the provided workspace.",
@@ -59,6 +65,27 @@ buildDevPrompt baseBranch task patchset maybeReviewFeedback =
]
where
ns = fromMaybe "(none)" (Task.taskNamespace task)
+ comments =
+ if null (Task.taskComments task)
+ then ""
+ else
+ T.unlines
+ ( [ "### Comments",
+ ""
+ ]
+ <> map formatComment (Task.taskComments task)
+ <> [""]
+ )
+ formatComment c =
+ "[" <> T.pack (show (Task.commentCreatedAt c)) <> "] "
+ <> "[" <> formatAuthor (Task.commentAuthor c) <> "] "
+ <> Task.commentText c
+ formatAuthor Task.Human = "human"
+ formatAuthor Task.System = "system"
+ formatAuthor (Task.Agent Task.Engineer) = "agent:engineer"
+ formatAuthor (Task.Agent Task.Reviewer) = "agent:reviewer"
+ formatAuthor (Task.Agent Task.ProductMgr) = "agent:product"
+ formatAuthor (Task.Agent Task.Designer) = "agent:designer"
feedback = case maybeReviewFeedback of
Nothing -> ""
Just fb ->
@@ -196,7 +223,7 @@ checkDevStatus ad = do
then do
exitStr <- T.strip </ safeReadFile exitFile
if exitStr == "0"
- then checkForCommit ad
+ then checkForCommitOrInput ad
else do
logTail <- T.takeEnd 1000 </ safeReadFile logFile
pure (Core.DevFailed ("Agent exited with code " <> exitStr <> ":\n" <> logTail))
@@ -215,6 +242,19 @@ checkDevStatus ad = do
-- No PID file yet — still starting
pure Core.StillRunning
+-- | After agent completes with exit 0, check if a new commit was produced.
+-- If no commit, return DevNeedsInput (agent asked a question instead).
+checkForCommitOrInput :: Core.ActiveDev -> IO Core.DevResult
+checkForCommitOrInput ad = do
+ afterSha <- Git.branchSha (Core.adWorkspace ad) (Core.adTaskId ad)
+ case (Core.adBeforeSha ad, afterSha) of
+ (_, Nothing) -> pure (Core.DevFailed "Task branch not found after agent run")
+ (before, Just after)
+ | before == Just after ->
+ -- Agent exited 0 but no commit — treat as needs-input
+ pure (Core.DevNeedsInput "Agent exited cleanly without committing (likely posted a question)")
+ | otherwise -> pure (Core.DevSuccess (Just after))
+
-- | After agent completes, check if a new commit was produced.
checkForCommit :: Core.ActiveDev -> IO Core.DevResult
checkForCommit ad = do
diff --git a/Omni/Pipeline/State.hs b/Omni/Pipeline/State.hs
index 082d776a..c09f98a8 100644
--- a/Omni/Pipeline/State.hs
+++ b/Omni/Pipeline/State.hs
@@ -143,6 +143,20 @@ hasActiveRun conn taskId = do
[SQL.Only n] -> pure (n > 0)
_ -> pure False
+-- | Get the most recent started_at for any dev run on a task.
+-- Used to detect whether new human comments arrived after the last agent run.
+getLastDevRunTime :: SQL.Connection -> Text -> IO (Maybe UTCTime)
+getLastDevRunTime conn taskId = do
+ results <-
+ SQL.query
+ conn
+ "SELECT started_at FROM pipeline_runs WHERE task_id = ? AND phase = 'dev' ORDER BY started_at DESC LIMIT 1"
+ (SQL.Only taskId) ::
+ IO [SQL.Only UTCTime]
+ case results of
+ [SQL.Only t] -> pure (Just t)
+ _ -> pure Nothing
+
-- | Get the most recent finished_at for a failed dev run on a given patchset.
-- Used for exponential backoff calculation.
getLastFailureTime :: SQL.Connection -> Text -> Int -> IO (Maybe UTCTime)