Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 143 additions & 5 deletions .agents/scripts/supervisor-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@
# Optimistic locking: claim = commit+push TODO.md; push failure = race lost
# GH Issue creation: opt-in via --with-issue flag or SUPERVISOR_AUTO_ISSUE=true
#
# Model resolution (t132.5):
# Priority: task explicit model > subagent frontmatter > fallback chain > static default
# Tier names (haiku/sonnet/opus/flash/pro/grok) mapped to concrete provider/model strings
# resolve_task_model() orchestrates: resolve_model_from_frontmatter() + resolve_model()
# Integrates with: fallback-chain-helper.sh (t132.4), model-availability-helper.sh (t132.3)
#
# Database: ~/.aidevops/.agent-workspace/supervisor/supervisor.db
#
# Integration:
Expand Down Expand Up @@ -2692,6 +2698,12 @@ resolve_model() {
return 0
fi

# If tier is already a full provider/model string (contains /), return as-is
if [[ "$tier" == *"/"* ]]; then
echo "$tier"
return 0
fi

# Try fallback-chain-helper.sh for full chain resolution (t132.4)
# This walks the configured chain including gateway providers
local chain_helper="${SCRIPT_DIR}/fallback-chain-helper.sh"
Expand All @@ -2718,19 +2730,140 @@ resolve_model() {
log_verbose "model-availability-helper.sh could not resolve tier '$tier', using static default"
fi

# Static fallback (no availability helper or resolution failed)
# Static fallback: map tier names to concrete models (t132.5)
case "$tier" in
coding)
opus|coding)
echo "anthropic/claude-opus-4-6"
;;
eval|health)
sonnet|eval|health)
echo "anthropic/claude-sonnet-4-5"
;;
haiku)
echo "anthropic/claude-3-5-haiku-20241022"
;;
flash)
echo "google/gemini-2.5-flash-preview-05-20"
;;
pro)
echo "google/gemini-2.5-pro-preview-06-05"
;;
grok)
echo "xai/grok-3"
;;
*)
# Unknown tier — treat as coding tier default
echo "anthropic/claude-opus-4-6"
;;
esac

return 0
}

#######################################
# Read model: field from subagent YAML frontmatter (t132.5)
# Searches deployed agents dir and repo .agents/ dir
# Returns the model value or empty string if not found
#######################################
resolve_model_from_frontmatter() {
local subagent_name="$1"
local repo="${2:-.}"
Comment on lines +2768 to +2769

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The repository style guide (line 11) instruction "declare and assign separately for exit code safety" is a best practice. For consistency and to strictly follow the style guide's text, please separate declaration and assignment for local variables initialized from positional parameters.

Suggested change
local subagent_name="$1"
local repo="${2:-.}"
local subagent_name
subagent_name="$1"
local repo
repo="${2:-.}"
References
  1. Line 11: Use local var="$1" pattern in functions (declare and assign separately for exit code safety). (link)


# Search paths for subagent files
local -a search_paths=(
"${HOME}/.aidevops/agents"
"${repo}/.agents"
)

local agents_dir subagent_file model_value
for agents_dir in "${search_paths[@]}"; do
[[ -d "$agents_dir" ]] || continue

# Try exact path first (e.g., "tools/ai-assistants/models/opus.md")
subagent_file="${agents_dir}/${subagent_name}"
[[ -f "$subagent_file" ]] || subagent_file="${agents_dir}/${subagent_name}.md"

if [[ -f "$subagent_file" ]]; then
# Extract model: from YAML frontmatter (between --- delimiters)
model_value=$(sed -n '/^---$/,/^---$/{ /^model:/{ s/^model:[[:space:]]*//; p; q; } }' "$subagent_file" 2>/dev/null) || true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of 2>/dev/null here and on line 2799 suppresses potentially important error messages from sed, such as file permission errors. This violates the repository style guide (line 50), which states that 2>/dev/null should only be used when redirecting to log files. Please remove the error suppression to aid in debugging. The || true guard already prevents script termination on failure.

Suggested change
model_value=$(sed -n '/^---$/,/^---$/{ /^model:/{ s/^model:[[:space:]]*//; p; q; } }' "$subagent_file" 2>/dev/null) || true
model_value=$(sed -n '/^---$/,/^---$/{ /^model:/{ s/^model:[[:space:]]*//; p; q; } }' "$subagent_file") || true
References
  1. Line 50: 2>/dev/null is acceptable ONLY when redirecting to log files, not blanket suppression. (link)

if [[ -n "$model_value" ]]; then
echo "$model_value"
return 0
fi
fi

# Try finding by name in subdirectories
# shellcheck disable=SC2044
local found_file
found_file=$(find "$agents_dir" -name "${subagent_name}.md" -type f 2>/dev/null | head -1) || true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of 2>/dev/null suppresses potentially important error messages from find, such as directory permission errors. This violates the repository style guide (line 50), which states that 2>/dev/null should only be used when redirecting to log files. Please remove the error suppression to aid in debugging. The || true guard already prevents script termination on failure.

Suggested change
found_file=$(find "$agents_dir" -name "${subagent_name}.md" -type f 2>/dev/null | head -1) || true
found_file=$(find "$agents_dir" -name "${subagent_name}.md" -type f | head -1) || true
References
  1. Line 50: 2>/dev/null is acceptable ONLY when redirecting to log files, not blanket suppression. (link)

if [[ -n "$found_file" && -f "$found_file" ]]; then
model_value=$(sed -n '/^---$/,/^---$/{ /^model:/{ s/^model:[[:space:]]*//; p; q; } }' "$found_file" 2>/dev/null) || true
if [[ -n "$model_value" ]]; then
echo "$model_value"
return 0
fi
fi
done

return 1
}

#######################################
# Resolve the model for a task (t132.5)
# Priority: 1) Task's explicit model (if not default)
# 2) Subagent frontmatter model:
# 3) resolve_model() with tier/fallback chain
# Returns the resolved provider/model string
#######################################
resolve_task_model() {
local task_id="$1"
local task_model="${2:-}"
local task_repo="${3:-.}"
local ai_cli="${4:-opencode}"

local default_model="anthropic/claude-opus-4-6"

# 1) If task has an explicit non-default model, use it
if [[ -n "$task_model" && "$task_model" != "$default_model" ]]; then
# Could be a tier name or full model string — resolve_model handles both
local resolved
resolved=$(resolve_model "$task_model" "$ai_cli")
if [[ -n "$resolved" ]]; then
log_info "Model for $task_id: $resolved (from task config)"
echo "$resolved"
return 0
fi
fi

# 2) Try to find a model-specific subagent definition matching the task
# Look for tools/ai-assistants/models/*.md files that match the task's
# model tier or the task description keywords
local model_agents_dir="${HOME}/.aidevops/agents/tools/ai-assistants/models"
if [[ -d "$model_agents_dir" ]]; then
# If task_model is a tier name, check for a matching model agent
if [[ -n "$task_model" && ! "$task_model" == *"/"* ]]; then
local tier_agent="${model_agents_dir}/${task_model}.md"
if [[ -f "$tier_agent" ]]; then
local frontmatter_model
frontmatter_model=$(resolve_model_from_frontmatter "tools/ai-assistants/models/${task_model}" "$task_repo") || true
if [[ -n "$frontmatter_model" ]]; then
local resolved
resolved=$(resolve_model "$frontmatter_model" "$ai_cli")
log_info "Model for $task_id: $resolved (from subagent frontmatter: ${task_model}.md)"
echo "$resolved"
return 0
fi
fi
fi
fi

# 3) Fall back to resolve_model with default tier
local resolved
resolved=$(resolve_model "coding" "$ai_cli")
log_info "Model for $task_id: $resolved (default coding tier)"
echo "$resolved"
return 0
}
Comment on lines +2817 to +2865

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic for resolving the model from "subagent frontmatter" is complex and brittle. It relies on a hardcoded path (${HOME}/.aidevops/agents/tools/ai-assistants/models) and a special convention of "model agents" named after tiers, instead of using the frontmatter of the actual subagent executing the task.

A more robust and general approach would be to use the frontmatter of the subagent assigned to the task. The subagent name is available as $tsubagent in cmd_dispatch and can be passed to this function.

I recommend refactoring this function to accept the subagent name and use it with resolve_model_from_frontmatter. This would simplify the logic, remove the hardcoded path, and align better with the feature's description. The call in cmd_dispatch would then be updated to resolve_task_model "$task_id" "$tmodel" "$tsubagent" ....

resolve_task_model() {
    local task_id
    task_id="$1"
    local task_model
    task_model="${2:-}"
    local subagent_name
    subagent_name="${3:-}"
    local task_repo
    task_repo="${4:-.}"
    local ai_cli
    ai_cli="${5:-opencode}"

    local default_model="anthropic/claude-opus-4-6"
    local resolved

    # 1) If task has an explicit non-default model, use it
    if [[ -n "$task_model" && "$task_model" != "$default_model" ]]; then
        resolved=$(resolve_model "$task_model" "$ai_cli")
        if [[ -n "$resolved" ]]; then
            log_info "Model for $task_id: $resolved (from task config)"
            echo "$resolved"
            return 0
        fi
    fi

    # 2) Try to find model from the task's subagent frontmatter
    if [[ -n "$subagent_name" ]]; then
        local frontmatter_model
        frontmatter_model=$(resolve_model_from_frontmatter "$subagent_name" "$task_repo") || true
        if [[ -n "$frontmatter_model" ]]; then
            resolved=$(resolve_model "$frontmatter_model" "$ai_cli")
            if [[ -n "$resolved" ]]; then
                log_info "Model for $task_id: $resolved (from subagent frontmatter: $subagent_name)"
                echo "$resolved"
                return 0
            fi
        fi
    fi

    # 3) Fall back to resolve_model with default tier
    resolved=$(resolve_model "coding" "$ai_cli")
    log_info "Model for $task_id: $resolved (default coding tier)"
    echo "$resolved"
    return 0
}


#######################################
# Pre-dispatch model health check (t132.3)
# Two-tier probe strategy:
Expand Down Expand Up @@ -3513,7 +3646,7 @@ cmd_dispatch() {
echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "worktree=$worktree_path"
echo "branch=$branch_name"
echo "model=${tmodel:-default}"
echo "model=${resolved_model:-${tmodel:-default}}"
echo "ai_cli=$(resolve_ai_cli 2>/dev/null || echo unknown)"
echo "dispatch_mode=$(detect_dispatch_mode 2>/dev/null || echo unknown)"
echo "=== END DISPATCH METADATA ==="
Expand All @@ -3537,16 +3670,21 @@ cmd_dispatch() {
log_info "Injecting ${#memory_context} bytes of memory context for $task_id"
fi

# Resolve model via frontmatter + fallback chain (t132.5)
local resolved_model
resolved_model=$(resolve_task_model "$task_id" "$tmodel" "${trepo:-.}" "$ai_cli")

log_info "Dispatching $task_id via $ai_cli ($dispatch_mode mode)"
log_info "Worktree: $worktree_path"
log_info "Model: $resolved_model"
log_info "Log: $log_file"

# Build and execute dispatch command
# Use NUL-delimited read to preserve multi-line prompts as single arguments
local -a cmd_parts=()
while IFS= read -r -d '' part; do
cmd_parts+=("$part")
done < <(build_dispatch_cmd "$task_id" "$worktree_path" "$log_file" "$ai_cli" "$memory_context" "$tmodel" "$tdesc")
done < <(build_dispatch_cmd "$task_id" "$worktree_path" "$log_file" "$ai_cli" "$memory_context" "$resolved_model" "$tdesc")

# Ensure PID directory exists before dispatch
mkdir -p "$SUPERVISOR_DIR/pids"
Expand Down
Loading