commit 95a04b80c4e61797eb882fe2087499fb5496443d
Author: Coder Agent <coder@agents.omni>
Date: Wed Feb 11 15:18:27 2026
Omni/Agentd+Ide: harden workflow execution and recovery
- fix agentd workflow prompt handling in containers
(path mapping, -- separator, pipefail)
- improve dev-review-release dirty workspace recovery
and retry behavior
- tune dev/reviewer/integrator workflow frontmatter and docs
for dogfood usage
- remove oisd nsfw filter from Omni/Dev/Vpn.nix
Task-Id: t-584
Task-Id: t-585
Task-Id: t-586
diff --git a/Omni/Agentd.hs b/Omni/Agentd.hs
index 6647200e..803b0ca8 100755
--- a/Omni/Agentd.hs
+++ b/Omni/Agentd.hs
@@ -58,7 +58,7 @@ import qualified Options.Applicative as Opt
import qualified System.Directory as Dir
import qualified System.Environment as Env
import qualified System.Exit as Exit
-import System.FilePath ((</>))
+import System.FilePath (makeRelative, (</>))
import qualified System.IO as IO
import qualified System.Posix.User as Posix
import qualified System.Process as Process
@@ -735,6 +735,9 @@ runWorkflow spec verbose = do
coderoot <- Env.getEnv "CODEROOT"
home <- Env.getEnv "HOME"
workspaceAbs <- Dir.makeAbsolute (specWorkspace spec)
+ hasNixStore <- Dir.doesDirectoryExist "/nix/store"
+ hasTaskDb <- Dir.doesDirectoryExist "/var/lib/omni"
+ hasRepoGit <- Dir.doesPathExist (coderoot </> ".git")
-- Build docker command
let image = toolchainImage (specToolchain spec)
@@ -746,6 +749,23 @@ runWorkflow spec verbose = do
iterFlag = maybe [] (\i -> ["--max-iter=" <> show i]) (specMaxIterations spec)
agentFlags = modelFlag ++ providerFlag ++ costFlag ++ iterFlag
+ extraMounts =
+ concat
+ [ if hasNixStore then ["-v", "/nix/store:/nix/store:ro"] else [],
+ if hasTaskDb then ["-v", "/var/lib/omni:/var/lib/omni"] else [],
+ if hasRepoGit then ["-v", coderoot <> "/.git:" <> coderoot <> "/.git"] else []
+ ]
+
+ extraEnv =
+ concat
+ [ ["-e", "PATH=/repo/_/bin:/bin"],
+ ["-e", "CODEROOT=/repo"],
+ ["-e", "GIT_CONFIG_COUNT=1"],
+ ["-e", "GIT_CONFIG_KEY_0=safe.directory"],
+ ["-e", "GIT_CONFIG_VALUE_0=*"],
+ if hasTaskDb then ["-e", "TASK_DB_PATH=/var/lib/omni/tasks.db"] else []
+ ]
+
-- Docker run command
dockerArgs =
[ "run",
@@ -756,33 +776,39 @@ runWorkflow spec verbose = do
workspaceAbs <> ":/workspace",
-- Mount repo (ro)
"-v",
- coderoot <> ":/repo:ro",
- -- Mount agent auth (rw) to default location - needs write for token refresh
- "-v",
- home <> "/.local/share/agent:/root/.local/share/agent",
- -- Set HOME so agent finds auth at /root/.local/share/agent
- "-e",
- "HOME=/root",
- -- Pass through API keys
- "-e",
- "ANTHROPIC_API_KEY",
- "-e",
- "OPENROUTER_API_KEY",
- "-e",
- "KAGI_API_KEY",
- -- Set locale for UTF-8 output
- "-e",
- "LANG=C.UTF-8",
- -- Set working directory
- "-w",
- "/workspace",
- -- Image
- Text.unpack image,
- -- Command: agent with task
- "agent"
+ coderoot <> ":/repo:ro"
]
+ ++ extraMounts
+ ++ [
+ -- Mount agent auth (rw) to default location - needs write for token refresh
+ "-v",
+ home <> "/.local/share/agent:/root/.local/share/agent",
+ -- Set HOME so agent finds auth at /root/.local/share/agent
+ "-e",
+ "HOME=/root",
+ -- Pass through API keys
+ "-e",
+ "ANTHROPIC_API_KEY",
+ "-e",
+ "OPENROUTER_API_KEY",
+ "-e",
+ "KAGI_API_KEY"
+ ]
+ ++ extraEnv
+ ++ [
+ -- Set locale for UTF-8 output
+ "-e",
+ "LANG=C.UTF-8",
+ -- Set working directory
+ "-w",
+ "/workspace",
+ -- Image
+ Text.unpack image,
+ -- Command: agent with task
+ "agent"
+ ]
++ agentFlags
- ++ [Text.unpack (specTask spec)]
+ ++ ["--", Text.unpack (specTask spec)]
when verbose <| do
TextIO.hPutStrLn stderr <| "docker " <> Text.unwords (map Text.pack dockerArgs)
@@ -818,8 +844,8 @@ runWorkflow spec verbose = do
-- | Run agent in a Docker container (background by default)
--
-- Returns the run ID (container name)
-runAgent :: Text -> Maybe Text -> Bool -> Bool -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe Text -> IO Text
-runAgent prompt mName foreground verbose maxCost maxIter timeoutSecs mProvider = do
+runAgent :: Text -> Maybe Text -> Bool -> Bool -> Maybe Int -> Maybe Int -> Maybe Int -> Maybe Text -> Maybe Text -> IO Text
+runAgent prompt mName foreground verbose maxCost maxIter timeoutSecs mProvider mToolchain = do
-- Get paths
coderoot <- Env.getEnv "CODEROOT"
home <- Env.getEnv "HOME"
@@ -844,10 +870,51 @@ runAgent prompt mName foreground verbose maxCost maxIter timeoutSecs mProvider =
-- by spawning the docker process in a shell and redirecting stdout
-- Resolve provider: CLI flag > env var > default
envProvider <- Env.lookupEnv "AGENTD_DEFAULT_PROVIDER"
+ envToolchain <- Env.lookupEnv "AGENTD_DEFAULT_TOOLCHAIN"
+ hasNixStore <- Dir.doesDirectoryExist "/nix/store"
+ hasTaskDb <- Dir.doesDirectoryExist "/var/lib/omni"
+ hasRepoGit <- Dir.doesPathExist (coderoot </> ".git")
+
+ promptForContainer <- do
+ let promptPath = Text.unpack prompt
+ isUnder parent child =
+ let parentWithSlash = if "/" `List.isSuffixOf` parent then parent else parent <> "/"
+ in child == parent || parentWithSlash `List.isPrefixOf` child
+ exists <- Dir.doesFileExist promptPath
+ if not exists
+ then pure prompt
+ else do
+ absPrompt <- Dir.makeAbsolute promptPath
+ if isUnder cwd absPrompt
+ then pure <| Text.pack <| "/workspace" </> makeRelative cwd absPrompt
+ else
+ if isUnder coderoot absPrompt
+ then pure <| Text.pack <| "/repo" </> makeRelative coderoot absPrompt
+ else pure prompt
+
let provider = fromMaybe "openrouter" (mProvider <|> (Text.pack </ envProvider))
providerFlag = ["--provider=" <> Text.unpack provider]
maxCostFlag = maybe [] (\c -> ["--max-cost=" <> show c]) maxCost
maxIterFlag = maybe [] (\i -> ["--max-iter=" <> show i]) maxIter
+ defaultImage = toolchainImage (fromMaybe "git" (mToolchain <|> (Text.pack </ envToolchain)))
+
+ extraMounts =
+ concat
+ [ if hasNixStore then ["-v", "/nix/store:/nix/store:ro"] else [],
+ if hasTaskDb then ["-v", "/var/lib/omni:/var/lib/omni"] else [],
+ if hasRepoGit then ["-v", coderoot <> "/.git:" <> coderoot <> "/.git"] else []
+ ]
+
+ extraEnv =
+ concat
+ [ ["-e", "PATH=/repo/_/bin:/bin"],
+ ["-e", "CODEROOT=/repo"],
+ ["-e", "GIT_CONFIG_COUNT=1"],
+ ["-e", "GIT_CONFIG_KEY_0=safe.directory"],
+ ["-e", "GIT_CONFIG_VALUE_0=*"],
+ if hasTaskDb then ["-e", "TASK_DB_PATH=/var/lib/omni/tasks.db"] else []
+ ]
+
-- Docker run command
dockerArgs =
[ "run",
@@ -866,41 +933,47 @@ runAgent prompt mName foreground verbose maxCost maxIter timeoutSecs mProvider =
cwd <> ":/workspace",
-- Mount repo (ro)
"-v",
- coderoot <> ":/repo:ro",
- -- Mount agent auth (ro) - XDG_DATA_HOME/agent contains auth.json
- "-v",
- home <> "/.local/share/agent:/root/.local/share/agent:ro",
- -- Set HOME so agent finds auth at /root/.local/share/agent
- "-e",
- "HOME=/root",
- -- Pass through API keys
- "-e",
- "ANTHROPIC_API_KEY",
- "-e",
- "OPENROUTER_API_KEY",
- "-e",
- "KAGI_API_KEY",
- -- Pass run ID so agent can use it
- "-e",
- "AGENT_RUN_ID=" <> Text.unpack runId,
- "-e",
- "AGENT_STEERING_FILE=" <> containerSteeringFile,
- -- Set locale for UTF-8 output
- "-e",
- "LANG=C.UTF-8",
- -- Set working directory
- "-w",
- "/workspace",
- -- Image (use base image for simple tasks)
- "agent-base",
- -- Command: agent with prompt using claude-code provider
- "agent"
+ coderoot <> ":/repo:ro"
]
+ ++ extraMounts
+ ++ [
+ -- Mount agent auth (ro) - XDG_DATA_HOME/agent contains auth.json
+ "-v",
+ home <> "/.local/share/agent:/root/.local/share/agent:ro",
+ -- Set HOME so agent finds auth at /root/.local/share/agent
+ "-e",
+ "HOME=/root",
+ -- Pass through API keys
+ "-e",
+ "ANTHROPIC_API_KEY",
+ "-e",
+ "OPENROUTER_API_KEY",
+ "-e",
+ "KAGI_API_KEY",
+ -- Pass run ID so agent can use it
+ "-e",
+ "AGENT_RUN_ID=" <> Text.unpack runId,
+ "-e",
+ "AGENT_STEERING_FILE=" <> containerSteeringFile
+ ]
+ ++ extraEnv
+ ++ [
+ -- Set locale for UTF-8 output
+ "-e",
+ "LANG=C.UTF-8",
+ -- Set working directory
+ "-w",
+ "/workspace",
+ -- Image (default git-capable toolchain)
+ Text.unpack defaultImage,
+ -- Command: agent with prompt
+ "agent"
+ ]
++ providerFlag
++ maxCostFlag
++ maxIterFlag
++ ["--json" | not foreground] -- Output JSON events in background mode
- ++ [Text.unpack prompt]
+ ++ ["--", Text.unpack promptForContainer]
when verbose <| do
TextIO.hPutStrLn stderr <| "docker " <> Text.unwords (map Text.pack dockerArgs)
@@ -917,6 +990,7 @@ runAgent prompt mName foreground verbose maxCost maxIter timeoutSecs mProvider =
teeCmd = "tee -a " <> outputPath
baseCmd = "docker " <> dockerCmd <> " 2>> " <> outputPath <> " | " <> teeCmd
shellCmd = if foreground then baseCmd else baseCmd <> " > /dev/null &"
+ shellArgs = ["-o", "pipefail", "-c", shellCmd]
for_ timeoutSecs <| \secs -> do
_ <-
@@ -929,11 +1003,11 @@ runAgent prompt mName foreground verbose maxCost maxIter timeoutSecs mProvider =
if foreground
then do
- Process.callCommand shellCmd
+ Process.callProcess "bash" shellArgs
pure runId
else do
-- Run in background: spawn shell process that runs docker and captures stdout to log files
- _ <- Process.spawnCommand shellCmd
+ _ <- Process.spawnProcess "bash" shellArgs
-- Give docker a moment to start
Concurrent.threadDelay 100000 -- 100ms
pure runId
@@ -1694,8 +1768,8 @@ main = do
content <- TextIO.getContents
if Text.null (Text.strip content) then pure Nothing else pure (Just content)
- -- Determine prompt and provider override
- (prompt, providerOverride) <- case promptArg of
+ -- Determine prompt, provider override, and toolchain override
+ (prompt, providerOverride, toolchainOverride) <- case promptArg of
Just path
| isYamlPath path -> do
-- YAML workflow file - use old workflow runner (always foreground)
@@ -1722,43 +1796,44 @@ main = do
code <- runMultiStepWorkflow wf verbose
Exit.exitWith code
| ".md" `Text.isSuffixOf` path -> do
- -- Markdown file - try container spec first
+ -- Markdown file - keep as file path so `agent` can parse frontmatter
+ -- (imports/tools/system_prompt/model/max_iterations).
content <- TextIO.readFile (Text.unpack path)
case parseSpec content of
Right spec ->
- -- Has container config - run in container
- pure (specTask spec, specProvider spec)
+ pure (path, specProvider spec, Just (specToolchain spec))
Left _specErr ->
- -- No container config - check for simpler workflow frontmatter
case Agent.parseWorkflowFrontmatter content of
- Right (Just wm, body) ->
- -- Has workflow metadata but no container config - run in container by default
- pure (Text.strip body, Agent.wmProvider wm)
+ Right (Just wm, _body) ->
+ pure (path, Agent.wmProvider wm, Agent.wmToolchain wm)
Right (Nothing, _body) ->
- -- No frontmatter at all - use whole file as prompt in container by default
- pure (content, Nothing)
+ pure (path, Nothing, Nothing)
Left _parseErr ->
- -- Parse error - use whole file as prompt in container by default
- pure (content, Nothing)
+ pure (path, Nothing, Nothing)
| otherwise ->
-- Plain text prompt argument - run in container by default
- pure (path, Nothing)
+ pure (path, Nothing, Nothing)
Nothing -> case stdinContent of
- Just content -> pure (content, Nothing)
+ Just content -> pure (content, Nothing, Nothing)
Nothing -> do
TextIO.hPutStrLn stderr "Error: prompt required (provide as argument or pipe to stdin)"
Exit.exitWith (Exit.ExitFailure 1)
let promptWithStdin = case (promptArg, stdinContent) of
+ (Just path, Just _stdinText)
+ | ".md" `Text.isSuffixOf` path ->
+ -- Keep markdown prompt as a file path so agent can parse its frontmatter.
+ prompt
(Just _, Just stdinText) ->
prompt <> "\n\n---\nInput:\n" <> Text.stripEnd stdinText
_ -> prompt
-- Merge provider: CLI flag > frontmatter > env > default
let finalProvider = mProvider <|> providerOverride
+ finalToolchain = toolchainOverride
-- Run the agent in a container
- runId <- runAgent promptWithStdin mName foreground verbose maxCost maxIter timeoutSecs finalProvider
+ runId <- runAgent promptWithStdin mName foreground verbose maxCost maxIter timeoutSecs finalProvider finalToolchain
-- Output run ID (for background runs)
unless foreground <| do
diff --git a/Omni/Agentd/SPEC.md b/Omni/Agentd/SPEC.md
index 37e98e87..505fe433 100644
--- a/Omni/Agentd/SPEC.md
+++ b/Omni/Agentd/SPEC.md
@@ -42,13 +42,16 @@ Agentd automatically mounts:
|------|-----------|------|---------|
| `<workspace>` | `/workspace` | rw | Agent's working directory |
| `<repo-root>` | `/repo` | ro | Access to AGENTS.md, skills, code |
+| `<repo-root>/.git` | `<repo-root>/.git` | rw | Resolve worktree gitdir pointers for git commands |
+| `/nix/store` | `/nix/store` | ro | Resolve repo-local symlinked tool binaries (for example `_/bin/task`) |
+| `/var/lib/omni` | `/var/lib/omni` | rw | Shared task database (`tasks.db`) |
| `~/.local/share/agent/` | `/root/.local/share/agent/` | ro | OAuth tokens |
The agent's CWD is `/workspace`.
## Body (Task)
-Everything after the closing `---` is the task. This is passed to `agent` as the prompt.
+Everything after the closing `---` is the task. For markdown workflows, agentd passes the markdown file path to `agent` so workflow frontmatter (`imports`, `tools`, `system_prompt`, etc.) is honored.
### Stdin Injection
@@ -86,6 +89,12 @@ Agentd also sets:
- `AGENT_RUN_ID`
- `AGENT_STEERING_FILE`
+- `PATH=/repo/_/bin:/bin`
+- `CODEROOT=/repo`
+- `GIT_CONFIG_COUNT=1`
+- `GIT_CONFIG_KEY_0=safe.directory`
+- `GIT_CONFIG_VALUE_0=*`
+- `TASK_DB_PATH=/var/lib/omni/tasks.db` (when `/var/lib/omni` is mounted)
OAuth tokens are accessed via the mounted data directory (`~/.local/share/agent/auth.json`).
diff --git a/Omni/Dev/Vpn.nix b/Omni/Dev/Vpn.nix
index 303653c1..d064c1a2 100644
--- a/Omni/Dev/Vpn.nix
+++ b/Omni/Dev/Vpn.nix
@@ -70,11 +70,6 @@ in {
name = "AdGuard NSFW Filter";
url = "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt";
}
- {
- enabled = true;
- name = "oisd nsfw";
- url = "https://nsfw.oisd.nl/";
- }
];
};
};
diff --git a/Omni/Ide/DEV_REVIEW_RELEASE.md b/Omni/Ide/DEV_REVIEW_RELEASE.md
index d4aa2093..ceb065d7 100644
--- a/Omni/Ide/DEV_REVIEW_RELEASE.md
+++ b/Omni/Ide/DEV_REVIEW_RELEASE.md
@@ -8,6 +8,7 @@ This workflow runs three independent agent loops in dedicated worktrees.
- New worktrees are created under `_/worktrees/t-565` by default.
- Branch cleanup is dry-run by default.
- Worktree directories are never removed automatically.
+- Loop auto-stashes dirty workspace state by default (can disable with `--no-auto-stash`).
## 1) Create dedicated worktrees
@@ -40,6 +41,7 @@ Optional flags:
- `--root _/worktrees/t-565`
- `--parent t-565` (scope to one epic)
- `--task-id t-565.2` (scope to one task)
+- `--no-auto-stash` (disable automatic dirty-workspace recovery)
- `--once`
- `--dry-run`
diff --git a/Omni/Ide/README.md b/Omni/Ide/README.md
index ff192eab..2e5ef6f6 100644
--- a/Omni/Ide/README.md
+++ b/Omni/Ide/README.md
@@ -72,6 +72,9 @@ Omni/Ide/dev-review-release.sh loop --role integrator
# Optional: scope to one epic/task while dogfooding
Omni/Ide/dev-review-release.sh loop --role dev --parent t-565
Omni/Ide/dev-review-release.sh loop --role dev --task-id t-565.2
+
+# Disable dirty-workspace auto-stash recovery
+Omni/Ide/dev-review-release.sh loop --role dev --no-auto-stash
```
The convention for all programs in the omnirepo is to run their tests if the first argument is `test`. So for example:
diff --git a/Omni/Ide/Workflows/dev.md b/Omni/Ide/Workflows/dev.md
index 7ca618f6..f0234e7b 100644
--- a/Omni/Ide/Workflows/dev.md
+++ b/Omni/Ide/Workflows/dev.md
@@ -1,9 +1,8 @@
---
model: claude-sonnet-4-5
+toolchain: haskell
max_cost_cents: 200
max_iterations: 80
-imports:
- - ../../../AGENTS.md
tools:
- read_file
- write_file
@@ -32,11 +31,13 @@ The runtime context (Task ID, workspace, branch, namespace) is appended to this
### 0) Pre-flight
-1. Check clean tree:
+1. Check tree state:
```bash
git status --porcelain
```
- If dirty: stop and report exact files.
+ If dirty:
+ - If changes appear to be in-progress work for this same task branch, continue and recover by finishing/committing them.
+ - If changes are unrelated or suspicious, stop and report exact files.
2. Ensure task branch is checked out (`t-XXX`).
- If branch exists: checkout it.
@@ -51,10 +52,11 @@ The runtime context (Task ID, workspace, branch, namespace) is appended to this
### 1) Understand
- Read task details and relevant files.
-- Load coder skill:
+- Optionally load coder skill if available:
```bash
- skill(operation="load", query="Coder")
+ skill(operation="load", query="coder")
```
+ If skill lookup fails, continue without it.
### 2) Implement
diff --git a/Omni/Ide/Workflows/integrator.md b/Omni/Ide/Workflows/integrator.md
index cf3e82aa..04afebc6 100644
--- a/Omni/Ide/Workflows/integrator.md
+++ b/Omni/Ide/Workflows/integrator.md
@@ -1,9 +1,8 @@
---
model: claude-sonnet-4-5
+toolchain: haskell
max_cost_cents: 200
max_iterations: 120
-imports:
- - ../../../AGENTS.md
tools:
- read_file
- write_file
diff --git a/Omni/Ide/Workflows/reviewer.md b/Omni/Ide/Workflows/reviewer.md
index 1decd3a8..413f5bf7 100644
--- a/Omni/Ide/Workflows/reviewer.md
+++ b/Omni/Ide/Workflows/reviewer.md
@@ -1,9 +1,8 @@
---
model: claude-opus-4-6
+toolchain: haskell
max_cost_cents: 200
max_iterations: 120
-imports:
- - ../../../AGENTS.md
tools:
- read_file
- write_file
diff --git a/Omni/Ide/dev-review-release.sh b/Omni/Ide/dev-review-release.sh
index 54cd916f..5b876a46 100755
--- a/Omni/Ide/dev-review-release.sh
+++ b/Omni/Ide/dev-review-release.sh
@@ -39,6 +39,7 @@ Loop options:
--timeout SEC agentd timeout per run (default: 1800)
--max-iter N agentd max iterations per run (default: 80)
--max-cost CENTS agentd max cost cents per run (default: 300)
+ --no-auto-stash Disable auto-stashing dirty workspace state before retry
--once Process at most one task, then exit
--dry-run Print what would run, do not invoke agentd
@@ -160,10 +161,29 @@ select_next_task() {
esac
}
+workspace_dirty_output() {
+ local workspace="$1"
+ git -C "$workspace" status --porcelain
+}
+
+workspace_has_tracked_changes() {
+ local workspace="$1"
+ local dirty
+ dirty="$(workspace_dirty_output "$workspace")"
+ [[ -n "$dirty" ]] || return 1
+ while IFS= read -r line; do
+ [[ -z "$line" ]] && continue
+ if [[ "$line" != \?\?* ]]; then
+ return 0
+ fi
+ done <<<"$dirty"
+ return 1
+}
+
ensure_clean_workspace() {
local workspace="$1"
local dirty
- dirty="$(git -C "$workspace" status --porcelain)"
+ dirty="$(workspace_dirty_output "$workspace")"
if [[ -n "$dirty" ]]; then
log "Workspace is dirty, refusing to run: $workspace"
printf '%s\n' "$dirty"
@@ -171,6 +191,36 @@ ensure_clean_workspace() {
fi
}
+auto_stash_workspace() {
+ local workspace="$1"
+ local role="$2"
+ local tid="$3"
+
+ local dirty
+ dirty="$(workspace_dirty_output "$workspace")"
+ if [[ -z "$dirty" ]]; then
+ return 0
+ fi
+
+ local stamp stash_msg stash_out
+ stamp="$(date +%Y%m%d-%H%M%S)"
+ stash_msg="dev-review-release-autostash-$role-$tid-$stamp"
+
+ if ! stash_out="$(git -C "$workspace" stash push --include-untracked -m "$stash_msg" 2>&1)"; then
+ log "Failed to auto-stash dirty workspace for $tid"
+ printf '%s\n' "$stash_out"
+ return 1
+ fi
+
+ if [[ "$stash_out" == "No local changes to save" ]]; then
+ return 0
+ fi
+
+ log "Auto-stashed dirty workspace for $tid in $workspace ($stash_msg)"
+ task comment "$tid" "Automation ($role) auto-stashed dirty workspace state ($stash_msg) before retrying." --json >/dev/null || true
+ return 0
+}
+
prepare_workspace_for_task() {
local role="$1"
local workspace="$2"
@@ -259,6 +309,7 @@ run_single_task() {
local dry_run="$8"
local task_filter="$9"
local parent_filter="${10}"
+ local auto_stash_dirty="${11}"
local tid
tid="$(select_next_task "$role" "$task_filter" "$parent_filter")"
@@ -278,7 +329,27 @@ run_single_task() {
return 0
fi
- ensure_clean_workspace "$workspace" || return 1
+ if ! ensure_clean_workspace "$workspace"; then
+ local has_tracked_changes="false"
+ if workspace_has_tracked_changes "$workspace"; then
+ has_tracked_changes="true"
+ fi
+
+ if [[ "$auto_stash_dirty" == "true" && "$role" == "dev" && "$has_tracked_changes" == "false" ]]; then
+ if auto_stash_workspace "$workspace" "$role" "$tid" && ensure_clean_workspace "$workspace"; then
+ log "Recovered dirty workspace by auto-stashing ($workspace)"
+ else
+ log "Auto-stash recovery failed for dirty workspace ($workspace)"
+ return 1
+ fi
+ else
+ if [[ "$role" == "dev" && "$has_tracked_changes" == "true" ]]; then
+ log "Workspace has tracked changes for $tid; continuing to allow recovery"
+ else
+ return 1
+ fi
+ fi
+ fi
if ! prepare_workspace_for_task "$role" "$workspace" "$tid" "$base_branch"; then
return 1
@@ -314,13 +385,15 @@ run_single_task() {
cmd+=( -p "$provider" )
fi
- set +e
- (
+ local rc=0
+ if (
cd "$workspace"
"${cmd[@]}"
- )
- local rc=$?
- set -e
+ ); then
+ rc=0
+ else
+ rc=$?
+ fi
if [[ $rc -ne 0 ]]; then
log "Run failed for $tid (run=$run_name)"
@@ -434,6 +507,7 @@ loop_cmd() {
local timeout="$DEFAULT_TIMEOUT_SECONDS"
local max_iter="$DEFAULT_MAX_ITER"
local max_cost="$DEFAULT_MAX_COST_CENTS"
+ local auto_stash_dirty="true"
local once="false"
local dry_run="false"
@@ -479,6 +553,10 @@ loop_cmd() {
max_cost="$2"
shift 2
;;
+ --no-auto-stash)
+ auto_stash_dirty="false"
+ shift
+ ;;
--once)
once="true"
shift
@@ -516,11 +594,11 @@ loop_cmd() {
fi
log "Starting $role loop"
- log "workspace=$workspace base=$base_branch interval=${interval}s dry_run=$dry_run task_filter=${task_filter:-<none>} parent_filter=${parent_filter:-<none>}"
+ log "workspace=$workspace base=$base_branch interval=${interval}s dry_run=$dry_run auto_stash_dirty=$auto_stash_dirty task_filter=${task_filter:-<none>} parent_filter=${parent_filter:-<none>}"
while true; do
set +e
- run_single_task "$role" "$workspace" "$base_branch" "$provider" "$timeout" "$max_iter" "$max_cost" "$dry_run" "$task_filter" "$parent_filter"
+ run_single_task "$role" "$workspace" "$base_branch" "$provider" "$timeout" "$max_iter" "$max_cost" "$dry_run" "$task_filter" "$parent_filter" "$auto_stash_dirty"
rc=$?
set -e