Skip to content

t1324: AI-based issue duplicate detection and auto-dispatch assessment#2241

Merged
marcusquinn merged 1 commit intomainfrom
feature/ai-issue-assessment
Feb 24, 2026
Merged

t1324: AI-based issue duplicate detection and auto-dispatch assessment#2241
marcusquinn merged 1 commit intomainfrom
feature/ai-issue-assessment

Conversation

@marcusquinn
Copy link
Owner

@marcusquinn marcusquinn commented Feb 24, 2026

Summary

  • Semantic duplicate detection: AI compares new issue titles against all open issues before creation — catches different task IDs for the same work (e.g., t10 vs t023), rephrased descriptions that deterministic prefix matching misses
  • Auto-dispatch eligibility assessment: AI evaluates task description, brief quality, acceptance criteria, and scope to determine if a task is ready for autonomous dispatch
  • claim-task-id.sh delegation: Now delegates to issue-sync-helper.sh for rich issue bodies and proper labels instead of creating bare issues

Changes

.agents/scripts/supervisor/issue-sync.sh

  • ai_detect_duplicate_issue(): Fetches open issues, sends to AI with new title for semantic comparison. Returns duplicate issue number if found.
  • ai_assess_auto_dispatch(): Gathers task description, tags, brief content. AI assesses dispatchability and recommends labels.
  • create_github_issue(): Added AI duplicate check after deterministic title-prefix check.

.agents/scripts/issue-sync-helper.sh

  • _ai_check_duplicate(): Standalone duplicate detection (no supervisor module dependencies). Uses claude/opencode with 15s timeout.
  • cmd_push(): Calls _ai_check_duplicate() before creating issues. Links to existing issue if duplicate found.

.agents/scripts/claim-task-id.sh

  • create_github_issue(): Delegates to issue-sync-helper.sh push when available for rich bodies + labels. Falls back to bare creation.

Design

  • All AI calls use sonnet tier with 15s timeout — fast enough for inline pipeline use
  • Graceful degradation: if AI unavailable, falls back to existing deterministic behavior
  • Audit trail: all decisions logged to AI_LIFECYCLE_LOG_DIR
  • Follows proven pattern from check_task_staleness() (gather facts → AI decides → act)

Summary by CodeRabbit

  • New Features

    • AI-powered semantic duplicate detection for GitHub issues—automatically identifies and links duplicate issues beyond exact title matches
    • Intelligent task dispatch eligibility assessment—evaluates whether tasks qualify for autonomous AI handling
  • Improvements

    • Enhanced issue creation workflow with pre-check deduplication
    • Added audit logging for duplicate detection and dispatch decisions

… (t1324)

Replace fragile deterministic checks with AI intelligence for two
issue management capabilities:

1. Semantic duplicate detection (ai_detect_duplicate_issue):
   - Before creating a GitHub issue, AI compares the new title against
     all open issues to find semantic duplicates
   - Catches cases deterministic prefix matching misses: different task
     IDs for the same work (e.g., t10 vs t023), rephrased descriptions
   - Integrated into: issue-sync.sh create_github_issue(),
     issue-sync-helper.sh cmd_push(), claim-task-id.sh
   - When duplicate found: links to existing issue instead of creating

2. Auto-dispatch eligibility assessment (ai_assess_auto_dispatch):
   - AI evaluates task description, brief, acceptance criteria, and scope
     to determine if a task is ready for autonomous dispatch
   - Assesses: clear deliverable, testable criteria, bounded scope,
     actionability, dependency status, task type
   - Returns recommended labels including auto-dispatch when appropriate

3. claim-task-id.sh delegation to issue-sync-helper.sh:
   - Previously created bare issues (no labels, minimal body)
   - Now delegates to issue-sync-helper.sh push for rich bodies and
     proper labels, falling back to bare creation if helper unavailable

All AI calls use sonnet tier with 15s timeout, audit-logged to
AI_LIFECYCLE_LOG_DIR. Graceful degradation: if AI unavailable, falls
back to existing deterministic behavior.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Walkthrough

These changes introduce AI-driven semantic duplicate detection across the GitHub issue creation pipeline. The claim-task-id.sh script delegates enriched issue creation to issue-sync-helper.sh when possible, while new AI functions detect semantic duplicates and assess auto-dispatch eligibility. Fallback mechanisms preserve backward compatibility.

Changes

Cohort / File(s) Summary
Issue creation delegation
.agents/scripts/claim-task-id.sh
Updated create_github_issue() signature to accept labels and repo_path parameters; delegates to issue-sync-helper.sh for richer creation; includes fallback to bare gh issue create with output parsing and warning logs.
AI duplicate detection helper
.agents/scripts/issue-sync-helper.sh
Introduces _ai_check_duplicate() function for AI-based semantic duplicate detection; invokes AI CLI (claude preferred, opencode fallback); integrated into push flow to detect duplicates before issue creation; limited to GitHub platform usage.
AI-driven issue management
.agents/scripts/supervisor/issue-sync.sh
Adds two new functions: ai_detect_duplicate_issue() for semantic duplicate detection (returns JSON with duplicate info and exit codes), and ai_assess_auto_dispatch() for auto-dispatch eligibility assessment; enhances issue creation with pre-create deduplication checks; includes auditing hooks and lifecycle logging for AI decisions.

Sequence Diagram(s)

sequenceDiagram
    participant Task as Task Submission
    participant Helper as issue-sync-helper
    participant AI as AI Model
    participant GitHub as GitHub API
    participant Supervisor as supervisor/issue-sync

    Task->>Supervisor: Submit new issue
    Supervisor->>Supervisor: Fast deterministic check
    alt Deterministic match found
        Supervisor->>GitHub: Link to existing issue
    else No deterministic match
        Supervisor->>Supervisor: ai_detect_duplicate_issue()
        Supervisor->>AI: Compare title vs open issues
        AI-->>Supervisor: Semantic duplicate verdict (JSON)
        alt Semantic duplicate detected
            Supervisor->>GitHub: Link to existing issue
        else No duplicate
            Supervisor->>Helper: Delegate rich creation
            Helper->>Helper: _ai_check_duplicate()
            Helper->>AI: Verify no duplicate
            AI-->>Helper: Confirmation
            Helper->>GitHub: Create issue
            Helper-->>Supervisor: Issue number
        end
    end
    Supervisor->>Supervisor: ai_assess_auto_dispatch()
    Supervisor->>AI: Evaluate dispatch eligibility
    AI-->>Supervisor: Dispatch decision (JSON)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Poem

🤖 Duplicates beware! The AI awakens,
Comparing titles with semantic grace,
Old issues found before new ones taken,
Smart dispatch decides the fastest pace.
Zero duplicates left in their place! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main changes: AI-based issue duplicate detection and auto-dispatch assessment. It accurately reflects the primary objectives of adding semantic duplicate detection and auto-dispatch eligibility assessment across three script files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/ai-issue-assessment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @marcusquinn, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the issue management workflow by integrating AI capabilities for smarter issue creation and task dispatch. It introduces semantic duplicate detection to prevent redundant issues and an AI-driven assessment for determining if a task is suitable for autonomous processing. These changes aim to improve the efficiency and accuracy of issue tracking and automation, ensuring that new tasks are unique and properly prepared for further action.

Highlights

  • AI-based Semantic Duplicate Detection: Implemented AI to compare new issue titles against all open issues, identifying semantic duplicates even with different task IDs or rephrased descriptions, preventing redundant work.
  • AI-powered Auto-Dispatch Assessment: Introduced AI evaluation of task descriptions, brief quality, acceptance criteria, and scope to determine if a task is ready for autonomous dispatch, replacing deterministic tag matching.
  • Enhanced Issue Creation Delegation: The claim-task-id.sh script now delegates issue creation to issue-sync-helper.sh for richer issue bodies, proper label application (including auto-dispatch), and AI-based duplicate detection.
Changelog
  • .agents/scripts/claim-task-id.sh
    • Updated create_github_issue to delegate issue creation to issue-sync-helper.sh for enhanced features like AI duplicate detection and label application, with a fallback to bare creation.
  • .agents/scripts/issue-sync-helper.sh
    • Added _ai_check_duplicate() function for standalone AI-based semantic duplicate detection using claude or opencode.
    • Integrated _ai_check_duplicate() into cmd_push() to perform duplicate checks before creating new issues, linking to existing issues if duplicates are found.
  • .agents/scripts/supervisor/issue-sync.sh
    • Clarified comment for the existing deterministic duplicate check in create_github_issue.
    • Integrated AI-based semantic duplicate detection into create_github_issue using the new ai_detect_duplicate_issue() function.
    • Added ai_detect_duplicate_issue() function to implement AI logic for semantic duplicate detection, including fetching existing issues and prompting an AI model.
    • Added ai_assess_auto_dispatch() function to implement AI logic for assessing task dispatch eligibility based on various criteria and suggesting appropriate labels.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 58 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Tue Feb 24 23:50:39 UTC 2026: Code review monitoring started
Tue Feb 24 23:50:40 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 58

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 58
  • VULNERABILITIES: 0

Generated on: Tue Feb 24 23:50:42 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

@sonarqubecloud
Copy link

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces valuable AI-driven features for duplicate issue detection and auto-dispatch assessment. The implementation is well-structured, delegating to helper scripts and providing fallbacks. However, there's a recurring issue of suppressing stderr with 2>/dev/null across all new shell scripts, which violates repository conventions and can hide critical errors. Additionally, there's a bug in how JSON is extracted from AI responses, which will fail for complex objects. Addressing these will improve the robustness and debuggability of the new features.

local issue_sync_helper="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/issue-sync-helper.sh"
if [[ -n "$task_id" && -x "$issue_sync_helper" && -f "$repo_path/TODO.md" ]]; then
local push_output
push_output=$("$issue_sync_helper" push "$task_id" 2>/dev/null || echo "")

Choose a reason for hiding this comment

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

high

Suppressing stderr with 2>/dev/null can hide important errors from the issue-sync-helper.sh script, such as authentication failures, syntax errors, or missing dependencies. This makes debugging difficult. The || echo "" already prevents the script from exiting on failure. Please remove 2>/dev/null to allow errors to be logged.

Suggested change
push_output=$("$issue_sync_helper" push "$task_id" 2>/dev/null || echo "")
push_output=$("$issue_sync_helper" push "$task_id" || echo "")
References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

Comment on lines +76 to +158
_ai_check_duplicate() {
local new_title="$1"
local repo_slug="$2"

# Resolve AI CLI (claude preferred, opencode fallback)
local ai_cli=""
if command -v claude &>/dev/null; then
ai_cli="claude"
elif command -v opencode &>/dev/null; then
ai_cli="opencode"
else
return 1 # No AI available
fi

# Fetch recent open issues
local existing_issues
existing_issues=$(gh issue list --repo "$repo_slug" --state open --limit 50 \
--json number,title,labels \
--jq '.[] | "#\(.number): \(.title) [\(.labels | map(.name) | join(","))]"' \
2>/dev/null || echo "")

if [[ -z "$existing_issues" ]]; then
return 1
fi

local prompt
prompt="You are a duplicate issue detector. Determine if a new issue is a semantic duplicate of any existing open issue.

NEW ISSUE TITLE: ${new_title}

EXISTING OPEN ISSUES:
${existing_issues}

RULES:
- A duplicate means the same work described differently (different task IDs, rephrased titles).
- Different task IDs (e.g., t10 vs t023) for the same work ARE duplicates.
- Related but different-scope issues are NOT duplicates.
- Auto-generated dashboard/status issues are never duplicates of task issues.
- When uncertain, prefer false (not duplicate).

Respond with ONLY a JSON object:
{\"duplicate\": true|false, \"duplicate_of\": \"#NNN or empty\", \"reason\": \"one sentence\"}"

local ai_result=""
if [[ "$ai_cli" == "opencode" ]]; then
ai_result=$(timeout 15 opencode run \
-m "anthropic/claude-sonnet-4-20250514" \
--format default \
--title "dedup-$$" \
"$prompt" 2>/dev/null || echo "")
ai_result=$(printf '%s' "$ai_result" | sed 's/\x1b\[[0-9;]*[mGKHF]//g; s/\x1b\[[0-9;]*[A-Za-z]//g; s/\x1b\]//g; s/\x07//g')
else
ai_result=$(timeout 15 claude \
-p "$prompt" \
--model "claude-sonnet-4-20250514" \
--output-format text 2>/dev/null || echo "")
fi

if [[ -z "$ai_result" ]]; then
return 1
fi

local json_block
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1)
if [[ -z "$json_block" ]]; then
return 1
fi

local is_duplicate
is_duplicate=$(printf '%s' "$json_block" | jq -r '.duplicate // false' 2>/dev/null || echo "false")
local duplicate_of
duplicate_of=$(printf '%s' "$json_block" | jq -r '.duplicate_of // ""' 2>/dev/null | tr -d '#')

if [[ "$is_duplicate" == "true" && -n "$duplicate_of" && "$duplicate_of" =~ ^[0-9]+$ ]]; then
local reason
reason=$(printf '%s' "$json_block" | jq -r '.reason // ""' 2>/dev/null || echo "")
print_warning "Semantic duplicate: $new_title duplicates #${duplicate_of} — $reason"
echo "$duplicate_of"
return 0
fi

return 1
}

Choose a reason for hiding this comment

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

high

The _ai_check_duplicate function consistently suppresses stderr using 2>/dev/null for gh, opencode, claude, and jq commands. This is against repository guidelines and makes debugging extremely difficult by hiding critical errors like authentication failures, API issues, or malformed JSON responses. Please remove all instances of 2>/dev/null in this function. The existing || echo ... fallbacks are sufficient to prevent script termination.

References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

# Only runs on GitHub (gh CLI required for issue listing)
if [[ "$_DETECTED_PLATFORM" == "github" ]]; then
local dup_issue_num
if dup_issue_num=$(_ai_check_duplicate "$title" "$repo_slug" 2>/dev/null); then

Choose a reason for hiding this comment

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

high

Suppressing stderr here prevents the print_warning message from _ai_check_duplicate (line 152) from being displayed, which contains useful context about why a duplicate was detected. It also hides any other potential errors from that function. Please remove 2>/dev/null.

Suggested change
if dup_issue_num=$(_ai_check_duplicate "$title" "$repo_slug" 2>/dev/null); then
if dup_issue_num=$(_ai_check_duplicate "$title" "$repo_slug"); then
References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

Comment on lines +2249 to +2254
if dedup_result=$(ai_detect_duplicate_issue "$new_title" "" "$repo_slug" 2>/dev/null); then
local dup_number
dup_number=$(printf '%s' "$dedup_result" | jq -r '.duplicate_of // ""' 2>/dev/null | tr -d '#')
if [[ -n "$dup_number" && "$dup_number" =~ ^[0-9]+$ ]]; then
local dup_reason
dup_reason=$(printf '%s' "$dedup_result" | jq -r '.reason // ""' 2>/dev/null)

Choose a reason for hiding this comment

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

high

Suppressing stderr for ai_detect_duplicate_issue and subsequent jq calls hides potential errors from the AI call and JSON parsing. This makes debugging failures in the duplicate detection logic very difficult. Please remove 2>/dev/null from these lines.

References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

Comment on lines +2317 to +2430
ai_detect_duplicate_issue() {
local new_title="$1"
local new_body="${2:-}"
local repo_slug="$3"

# Fetch recent open issues for comparison
local existing_issues
existing_issues=$(gh issue list --repo "$repo_slug" --state open --limit 50 \
--json number,title,labels \
--jq '.[] | "#\(.number): \(.title) [\(.labels | map(.name) | join(","))]"' \
2>/dev/null || echo "")

if [[ -z "$existing_issues" ]]; then
log_verbose "ai_detect_duplicate_issue: no open issues to compare against"
return 1
fi

local ai_cli
ai_cli=$(resolve_ai_cli 2>/dev/null) || {
log_verbose "ai_detect_duplicate_issue: AI unavailable, skipping duplicate check"
return 1
}

local ai_model
ai_model=$(resolve_model "sonnet" "$ai_cli" 2>/dev/null) || {
log_verbose "ai_detect_duplicate_issue: model resolution failed, skipping"
return 1
}

local body_context=""
if [[ -n "$new_body" ]]; then
# Truncate body to avoid token overflow
body_context="

BODY (first 500 chars):
$(printf '%s' "$new_body" | head -c 500)"
fi

local prompt
prompt="You are a duplicate issue detector for a DevOps task management system. Determine if a new issue is a semantic duplicate of any existing open issue.

NEW ISSUE:
Title: ${new_title}${body_context}

EXISTING OPEN ISSUES:
${existing_issues}

RULES:
- A duplicate means the same work described differently (different task IDs, rephrased titles, same underlying fix/feature).
- Different task IDs (e.g., t10 vs t023) for the same work ARE duplicates.
- Issues that are related but address different aspects are NOT duplicates.
- Auto-generated dashboard/status issues (e.g., [Supervisor:*]) are never duplicates of task issues.
- If the new issue title starts with a task ID (tNNN:) and an existing issue has a DIFFERENT task ID but describes the SAME work, that is a duplicate.
- When uncertain, prefer false (not duplicate) — closing a non-duplicate is worse than having a duplicate.

Respond with ONLY a JSON object:
{\"duplicate\": true|false, \"duplicate_of\": \"#NNN or empty\", \"reason\": \"one sentence\"}"

local ai_result=""
if [[ "$ai_cli" == "opencode" ]]; then
ai_result=$(portable_timeout 15 opencode run \
-m "$ai_model" \
--format default \
--title "dedup-issue-$$" \
"$prompt" 2>/dev/null || echo "")
ai_result=$(printf '%s' "$ai_result" | sed 's/\x1b\[[0-9;]*[mGKHF]//g; s/\x1b\[[0-9;]*[A-Za-z]//g; s/\x1b\]//g; s/\x07//g')
else
local claude_model="${ai_model#*/}"
ai_result=$(portable_timeout 15 claude \
-p "$prompt" \
--model "$claude_model" \
--output-format text 2>/dev/null || echo "")
fi

if [[ -z "$ai_result" ]]; then
log_verbose "ai_detect_duplicate_issue: empty AI response"
return 2
fi

local json_block
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1)
if [[ -z "$json_block" ]]; then
log_verbose "ai_detect_duplicate_issue: no JSON in response"
return 2
fi

local is_duplicate
is_duplicate=$(printf '%s' "$json_block" | jq -r '.duplicate // false' 2>/dev/null || echo "false")
local duplicate_of
duplicate_of=$(printf '%s' "$json_block" | jq -r '.duplicate_of // ""' 2>/dev/null || echo "")
local reason
reason=$(printf '%s' "$json_block" | jq -r '.reason // ""' 2>/dev/null || echo "")

# Log for audit trail
mkdir -p "${AI_LIFECYCLE_LOG_DIR:-/tmp}" 2>/dev/null || true
local timestamp
timestamp=$(date -u '+%Y%m%d-%H%M%S')
{
echo "# Duplicate Check @ $timestamp"
echo "New: $new_title"
echo "Duplicate: $is_duplicate"
echo "Of: $duplicate_of"
echo "Reason: $reason"
} >"${AI_LIFECYCLE_LOG_DIR:-/tmp}/dedup-issue-${timestamp}.md" 2>/dev/null || true

if [[ "$is_duplicate" == "true" && -n "$duplicate_of" ]]; then
log_info "ai_detect_duplicate_issue: DUPLICATE of $duplicate_of — $reason"
printf '%s' "$json_block"
return 0
fi

log_verbose "ai_detect_duplicate_issue: not a duplicate — $reason"
return 1
}

Choose a reason for hiding this comment

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

high

This function consistently suppresses stderr using 2>/dev/null for gh, resolve_ai_cli, resolve_model, opencode, claude, jq, mkdir, and file redirection. This is against repository guidelines and makes debugging extremely difficult by hiding critical errors. Please remove all instances of 2>/dev/null in this function. The existing || ... fallbacks are sufficient to prevent script termination where used. For file operations, || true can be used if necessary.

References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

Comment on lines +2447 to +2571
ai_assess_auto_dispatch() {
local task_id="$1"
local task_line="$2"
local project_root="$3"

# Gather context: task description, tags, brief if available
local description
description=$(printf '%s' "$task_line" | sed -E 's/^[[:space:]]*- \[.\] [^ ]+ //' || echo "")

local tags
tags=$(printf '%s' "$task_line" | grep -oE '#[a-zA-Z0-9_-]+' | tr '\n' ' ' || echo "")

local estimate
estimate=$(printf '%s' "$task_line" | grep -oE '~[0-9]+[hm]' | head -1 || echo "")

local brief_content=""
local brief_file="$project_root/todo/tasks/${task_id}-brief.md"
if [[ -f "$brief_file" ]]; then
# Read first 1000 chars of brief
brief_content=$(head -c 1000 "$brief_file" 2>/dev/null || echo "")
fi

local ai_cli
ai_cli=$(resolve_ai_cli 2>/dev/null) || {
log_verbose "ai_assess_auto_dispatch: AI unavailable, skipping assessment"
return 2
}

local ai_model
ai_model=$(resolve_model "sonnet" "$ai_cli" 2>/dev/null) || {
log_verbose "ai_assess_auto_dispatch: model resolution failed, skipping"
return 2
}

local brief_section=""
if [[ -n "$brief_content" ]]; then
brief_section="

TASK BRIEF (first 1000 chars):
${brief_content}"
else
brief_section="

TASK BRIEF: Not found (no brief file at todo/tasks/${task_id}-brief.md)"
fi

local prompt
prompt="You are a task dispatch eligibility assessor for a DevOps automation system. Determine whether a task is ready for autonomous AI dispatch (no human supervision).

TASK: ${task_id}
DESCRIPTION: ${description}
TAGS: ${tags}
ESTIMATE: ${estimate}${brief_section}

ASSESSMENT CRITERIA:
1. CLEAR DELIVERABLE: Is the task's output well-defined? (e.g., 'fix X in file Y' vs 'investigate something')
2. ACCEPTANCE CRITERIA: Does the brief have testable acceptance criteria? (2+ criteria = strong signal)
3. SCOPE: Is the task bounded enough for a single AI worker session? (>4h estimate = risky)
4. ACTIONABILITY: Are there enough specifics (file paths, function names, error messages) for an AI to act on?
5. DEPENDENCIES: Are all prerequisites met? (no unresolved blocked-by, no -needed tags)
6. TYPE: Investigation, research, and design tasks are NOT auto-dispatchable (need human judgment).

LABEL ASSESSMENT:
Also determine which GitHub labels should be applied based on the task content:
- Map #tags to labels (e.g., #bugfix -> fix, #feature -> enhancement, #docs -> documentation)
- Add 'auto-dispatch' label ONLY if the task is dispatchable
- Add appropriate status label (status:available if no assignee, status:claimed if assignee present)

Respond with ONLY a JSON object:
{\"dispatchable\": true|false, \"labels\": [\"label1\", \"label2\"], \"reason\": \"one sentence\"}"

local ai_result=""
if [[ "$ai_cli" == "opencode" ]]; then
ai_result=$(portable_timeout 15 opencode run \
-m "$ai_model" \
--format default \
--title "dispatch-assess-$$" \
"$prompt" 2>/dev/null || echo "")
ai_result=$(printf '%s' "$ai_result" | sed 's/\x1b\[[0-9;]*[mGKHF]//g; s/\x1b\[[0-9;]*[A-Za-z]//g; s/\x1b\]//g; s/\x07//g')
else
local claude_model="${ai_model#*/}"
ai_result=$(portable_timeout 15 claude \
-p "$prompt" \
--model "$claude_model" \
--output-format text 2>/dev/null || echo "")
fi

if [[ -z "$ai_result" ]]; then
log_verbose "ai_assess_auto_dispatch: empty AI response"
return 2
fi

local json_block
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]*\}' | head -1)
if [[ -z "$json_block" ]]; then
log_verbose "ai_assess_auto_dispatch: no JSON in response"
return 2
fi

local is_dispatchable
is_dispatchable=$(printf '%s' "$json_block" | jq -r '.dispatchable // false' 2>/dev/null || echo "false")
local reason
reason=$(printf '%s' "$json_block" | jq -r '.reason // ""' 2>/dev/null || echo "")

# Log for audit trail
mkdir -p "${AI_LIFECYCLE_LOG_DIR:-/tmp}" 2>/dev/null || true
local timestamp
timestamp=$(date -u '+%Y%m%d-%H%M%S')
{
echo "# Dispatch Assessment: $task_id @ $timestamp"
echo "Dispatchable: $is_dispatchable"
echo "Reason: $reason"
echo "Response: $json_block"
} >"${AI_LIFECYCLE_LOG_DIR:-/tmp}/dispatch-assess-${task_id}-${timestamp}.md" 2>/dev/null || true

if [[ "$is_dispatchable" == "true" ]]; then
log_info "ai_assess_auto_dispatch: $task_id IS dispatchable — $reason"
printf '%s' "$json_block"
return 0
fi

log_info "ai_assess_auto_dispatch: $task_id NOT dispatchable — $reason"
printf '%s' "$json_block"
return 1
}

Choose a reason for hiding this comment

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

high

This function consistently suppresses stderr using 2>/dev/null for head, resolve_ai_cli, resolve_model, opencode, claude, jq, mkdir, and file redirection. This is against repository guidelines and makes debugging extremely difficult by hiding critical errors. Please remove all instances of 2>/dev/null in this function. The existing || ... fallbacks are sufficient to prevent script termination where used. For file operations, || true can be used if necessary.

References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

fi

local json_block
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]*\}' | head -1)

Choose a reason for hiding this comment

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

high

The regex \{[^}]*\} is not robust enough to parse the JSON response expected from the AI. The prompt asks for a JSON object that includes a labels array, e.g., {"labels": ["label1", "label2"]}. This regex will incorrectly stop at the first closing brace } it encounters inside the array definition, resulting in a truncated and invalid JSON block. This will cause subsequent jq parsing to fail.

Suggested change
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]*\}' | head -1)
json_block=$(printf '%s' "$ai_result" | sed -n 's/.*\({\(.*\)}\).*/\1/p' | head -1)

@marcusquinn marcusquinn merged commit 2be1df5 into main Feb 24, 2026
12 of 13 checks passed
@marcusquinn marcusquinn deleted the feature/ai-issue-assessment branch February 24, 2026 23:53
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.agents/scripts/claim-task-id.sh:
- Around line 440-451: The script fails to detect the helper's "semantic
duplicate of `#NNN`" message so fallback creates a duplicate issue; update the
parsing logic that sets issue_num from push_output (the block using issue_num
and push_output) to search for either "already has issue #[0-9]+" OR "semantic
duplicate of #[0-9]+" (or more simply extract the first occurrence of "#[0-9]+"
from push_output) and then strip the "#" to populate issue_num before falling
back to bare creation.
- Around line 415-418: Run .agents/scripts/linters-local.sh before committing to
validate shell scripts (ShellCheck with proper source, secrets/syntax/pattern
checks) and fix any issues it reports; in particular update
supervisor/issue-sync.sh where the expression using sed 's/`[^`]*`//g' (around
the code handling issue body formatting) should be converted to bash parameter
expansion (e.g., use ${variable//pattern/replace}) and correct the sed quoting
to avoid SC2001/SC2016 warnings—locate the transformation around the function
handling issue body creation (referenced near create_github_issue and the
issue-sync helper invocation) and replace the sed invocation with the equivalent
safe parameter expansion and re-run linters until clean.

In @.agents/scripts/issue-sync-helper.sh:
- Around line 138-142: The grep pipeline that sets json_block from ai_result can
fail under set -euo pipefail and prematurely exit; modify the pipeline that
assigns json_block (the command using printf '%s' "$ai_result" | grep -oE
'\{[^}]+\}' | head -1) to tolerate no-match failures by appending a protective
fallback (e.g., add "|| echo \"\"" or "|| true" to the pipeline) so the
subsequent if [[ -z "$json_block" ]] check runs; update the assignment to ensure
json_block is set to an empty string on grep failure while keeping the grep/head
logic intact.
- Around line 121-132: Add the portable_timeout() function to
shared-constants.sh (copy the implementation from agent-test-helper.sh lines
166–203) so GNU timeout, gtimeout, and background+kill fallback are available,
then replace the raw timeout invocations in this script (the two calls that set
ai_result around the opencode/claude invocations) with portable_timeout to
ensure the dedup AI check uses the portable timeout wrapper.

In @.agents/scripts/supervisor/issue-sync.sh:
- Around line 2249-2259: The semantic-duplicate branch currently echoes
dup_number and returns early, skipping the task→issue persistence and DB
issue_url update; instead, after detecting a duplicate (inside the
ai_detect_duplicate_issue handling where dup_number and dup_reason are extracted
and task_id is available), call or reuse the same link-persistence routine used
for newly-created issues (the code path that persists the task→issue link and
updates the DB issue_url) or set the necessary variables so execution falls
through to that routine—do not return immediately; also log the linkage and
ensure the DB update (issue_url) is performed for task_id linking to
#${dup_number}.
- Around line 2396-2401: The json extraction pipeline can cause the shell to
exit under errexit/pipefail when sourced; update the command substitution that
sets json_block (in ai_detect_duplicate_issue and the other json-extraction
block) to append "|| true" to the pipeline so the substitution never fails,
allowing the existing [[ -z "$json_block" ]] check and return 2 handling to run
normally; apply the same change to the second occurrence referenced in the
comment so both json_block assignments are guarded.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9d5325 and 642a0af.

📒 Files selected for processing (3)
  • .agents/scripts/claim-task-id.sh
  • .agents/scripts/issue-sync-helper.sh
  • .agents/scripts/supervisor/issue-sync.sh

Comment on lines +415 to 418
# t1324: Delegates to issue-sync-helper.sh push when available for rich
# issue bodies, proper labels (including auto-dispatch), and duplicate
# detection. Falls back to bare gh issue create if helper not found.
create_github_issue() {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if linters-local.sh exists and what it contains
fd -t f "linters-local.sh" .agents/scripts/

Repository: marcusquinn/aidevops

Length of output: 96


🏁 Script executed:

# Run ShellCheck on the modified files
shellcheck .agents/scripts/claim-task-id.sh \
           .agents/scripts/issue-sync-helper.sh \
           .agents/scripts/supervisor/issue-sync.sh 2>&1 | head -100

Repository: marcusquinn/aidevops

Length of output: 1298


🏁 Script executed:

# Examine the create_github_issue function and output parsing logic
sed -n '415,470p' .agents/scripts/claim-task-id.sh

Repository: marcusquinn/aidevops

Length of output: 2023


🏁 Script executed:

# Check the issue-sync-helper.sh duplicate detection message at line 1350
sed -n '1340,1360p' .agents/scripts/issue-sync-helper.sh

Repository: marcusquinn/aidevops

Length of output: 759


🏁 Script executed:

# Check what linters-local.sh validates and requires
cat .agents/scripts/linters-local.sh

Repository: marcusquinn/aidevops

Length of output: 21422


Run linters-local.sh before committing to validate shell scripts and catch quality regressions.

Per coding guidelines, execute .agents/scripts/linters-local.sh for comprehensive pre-commit validation. This includes ShellCheck with proper source following and additional quality checks (secrets, syntax, patterns). The script ensures zero technical debt standards.

Additionally, supervisor/issue-sync.sh line 1377 has style improvements: replace sed 's/[^]*//g'with bash parameter expansion${variable//search/replace}` and fix sed quoting (SC2001/SC2016).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/scripts/claim-task-id.sh around lines 415 - 418, Run
.agents/scripts/linters-local.sh before committing to validate shell scripts
(ShellCheck with proper source, secrets/syntax/pattern checks) and fix any
issues it reports; in particular update supervisor/issue-sync.sh where the
expression using sed 's/`[^`]*`//g' (around the code handling issue body
formatting) should be converted to bash parameter expansion (e.g., use
${variable//pattern/replace}) and correct the sed quoting to avoid SC2001/SC2016
warnings—locate the transformation around the function handling issue body
creation (referenced near create_github_issue and the issue-sync helper
invocation) and replace the sed invocation with the equivalent safe parameter
expansion and re-run linters until clean.

Comment on lines +440 to +451
# Also check if it found an existing issue (already has ref)
if [[ -z "$issue_num" ]]; then
issue_num=$(printf '%s' "$push_output" | grep -oE 'already has issue #[0-9]+' | grep -oE '[0-9]+' | head -1 || echo "")
fi

if [[ -n "$issue_num" ]]; then
log_info "Issue created via issue-sync-helper.sh: #$issue_num"
echo "$issue_num"
return 0
fi
log_warn "issue-sync-helper.sh push returned no issue number, falling back to bare creation"
fi
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Helper duplicate output isn’t parsed, so fallback can create a second issue.

Line 442 only matches already has issue #NNN, but helper duplicate flow emits `semantic duplicate of `#NNN. That leaves issue_num empty and triggers bare creation at Line 450.

Suggested fix
-		# Also check if it found an existing issue (already has ref)
-		if [[ -z "$issue_num" ]]; then
-			issue_num=$(printf '%s' "$push_output" | grep -oE 'already has issue #[0-9]+' | grep -oE '[0-9]+' | head -1 || echo "")
-		fi
+		# Also check existing-issue flows (already linked / semantic duplicate)
+		if [[ -z "$issue_num" ]]; then
+			issue_num=$(printf '%s' "$push_output" |
+				grep -oE '(already has issue|semantic duplicate of) #[0-9]+' |
+				grep -oE '[0-9]+' | head -1 || echo "")
+		fi
As per coding guidelines `.agents/scripts/*.sh`: Automation scripts - focus on: Reliability and robustness.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Also check if it found an existing issue (already has ref)
if [[ -z "$issue_num" ]]; then
issue_num=$(printf '%s' "$push_output" | grep -oE 'already has issue #[0-9]+' | grep -oE '[0-9]+' | head -1 || echo "")
fi
if [[ -n "$issue_num" ]]; then
log_info "Issue created via issue-sync-helper.sh: #$issue_num"
echo "$issue_num"
return 0
fi
log_warn "issue-sync-helper.sh push returned no issue number, falling back to bare creation"
fi
# Also check existing-issue flows (already linked / semantic duplicate)
if [[ -z "$issue_num" ]]; then
issue_num=$(printf '%s' "$push_output" |
grep -oE '(already has issue|semantic duplicate of) #[0-9]+' |
grep -oE '[0-9]+' | head -1 || echo "")
fi
if [[ -n "$issue_num" ]]; then
log_info "Issue created via issue-sync-helper.sh: #$issue_num"
echo "$issue_num"
return 0
fi
log_warn "issue-sync-helper.sh push returned no issue number, falling back to bare creation"
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/scripts/claim-task-id.sh around lines 440 - 451, The script fails to
detect the helper's "semantic duplicate of `#NNN`" message so fallback creates a
duplicate issue; update the parsing logic that sets issue_num from push_output
(the block using issue_num and push_output) to search for either "already has
issue #[0-9]+" OR "semantic duplicate of #[0-9]+" (or more simply extract the
first occurrence of "#[0-9]+" from push_output) and then strip the "#" to
populate issue_num before falling back to bare creation.

Comment on lines +121 to +132
ai_result=$(timeout 15 opencode run \
-m "anthropic/claude-sonnet-4-20250514" \
--format default \
--title "dedup-$$" \
"$prompt" 2>/dev/null || echo "")
ai_result=$(printf '%s' "$ai_result" | sed 's/\x1b\[[0-9;]*[mGKHF]//g; s/\x1b\[[0-9;]*[A-Za-z]//g; s/\x1b\]//g; s/\x07//g')
else
ai_result=$(timeout 15 claude \
-p "$prompt" \
--model "claude-sonnet-4-20250514" \
--output-format text 2>/dev/null || echo "")
fi
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if portable_timeout is defined anywhere in the codebase
rg "portable_timeout" --type sh

Repository: marcusquinn/aidevops

Length of output: 4267


🏁 Script executed:

# Check what's in the .agents/scripts directory
ls -la .agents/scripts/ 2>/dev/null || echo "Directory not found"

Repository: marcusquinn/aidevops

Length of output: 20320


🏁 Script executed:

# View the actual issue-sync-helper.sh file to understand context
wc -l .agents/scripts/issue-sync-helper.sh 2>/dev/null || echo "File not found"

Repository: marcusquinn/aidevops

Length of output: 105


🏁 Script executed:

# Check for timeout usage and any utility function definitions
rg "timeout\s" .agents/scripts/issue-sync-helper.sh -A 2 -B 2

Repository: marcusquinn/aidevops

Length of output: 704


🏁 Script executed:

# Look for any helper functions or utilities defined in the script
head -50 .agents/scripts/issue-sync-helper.sh

Repository: marcusquinn/aidevops

Length of output: 2296


🏁 Script executed:

# Check if portable_timeout is sourced or defined in issue-sync-helper.sh
cat -n .agents/scripts/issue-sync-helper.sh | head -100 | grep -E "(source|portable_timeout|\. )"

Repository: marcusquinn/aidevops

Length of output: 161


🏁 Script executed:

# Check what's in issue-sync-lib.sh to see if portable_timeout is there
grep -n "portable_timeout" .agents/scripts/issue-sync-lib.sh

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

# Check what shared-constants.sh contains
head -50 .agents/scripts/shared-constants.sh | grep -E "(source|portable_timeout|\. )"

Repository: marcusquinn/aidevops

Length of output: 184


🏁 Script executed:

# Look at the actual lines 121-132 in context
sed -n '115,140p' .agents/scripts/issue-sync-helper.sh | cat -n

Repository: marcusquinn/aidevops

Length of output: 1053


🏁 Script executed:

# Check if portable_timeout is defined anywhere in the main scripts directory
grep -n "^portable_timeout()" .agents/scripts/*.sh

Repository: marcusquinn/aidevops

Length of output: 125


🏁 Script executed:

# Check if portable_timeout is defined in shared-constants.sh
grep -n "portable_timeout" .agents/scripts/shared-constants.sh

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

# Check the full issue-sync-lib.sh to see if portable_timeout is defined
wc -l .agents/scripts/issue-sync-lib.sh

Repository: marcusquinn/aidevops

Length of output: 102


🏁 Script executed:

# Search for portable_timeout definition in issue-sync-lib.sh
grep -A 10 "^portable_timeout" .agents/scripts/issue-sync-lib.sh

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

# Get the portable_timeout function from agent-test-helper.sh to understand its signature
sed -n '166,200p' .agents/scripts/agent-test-helper.sh

Repository: marcusquinn/aidevops

Length of output: 773


🏁 Script executed:

# Check if supervisor scripts source a common file with portable_timeout
head -40 .agents/scripts/supervisor/_common.sh | grep -A 10 "^portable_timeout"

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

# Verify the exact lines 121-132 in issue-sync-helper.sh with more context
sed -n '110,145p' .agents/scripts/issue-sync-helper.sh | cat -n

Repository: marcusquinn/aidevops

Length of output: 1614


🏁 Script executed:

# Check if issue-sync-lib.sh sources _common.sh
grep -E "(source|\..*_common)" .agents/scripts/issue-sync-lib.sh | head -20

Repository: marcusquinn/aidevops

Length of output: 449


🏁 Script executed:

# Confirm this is the _ai_check_duplicate function mentioned in the review
grep -n "_ai_check_duplicate" .agents/scripts/issue-sync-helper.sh

Repository: marcusquinn/aidevops

Length of output: 178


🏁 Script executed:

# Look at the beginning of issue-sync-lib.sh to see what it sources
head -80 .agents/scripts/issue-sync-lib.sh | cat -n

Repository: marcusquinn/aidevops

Length of output: 3362


🏁 Script executed:

# Check if issue-sync-lib.sh or shared-constants.sh should be sourcing _common.sh
grep -E "(supervisor|_common)" .agents/scripts/issue-sync-lib.sh .agents/scripts/shared-constants.sh

Repository: marcusquinn/aidevops

Length of output: 639


🏁 Script executed:

# Verify that portable_timeout is not available in issue-sync-helper.sh context
# by checking if it can be called
bash -c 'source .agents/scripts/shared-constants.sh; source .agents/scripts/issue-sync-lib.sh; declare -f portable_timeout' 2>&1 | head -5

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

# Check how other issue-sync scripts in supervisor/ handle this
grep -B 5 "portable_timeout" .agents/scripts/supervisor/issue-sync.sh | head -30

Repository: marcusquinn/aidevops

Length of output: 1235


🏁 Script executed:

# Check if shared-constants.sh or issue-sync-lib.sh should be providing portable_timeout
grep -n "portable_timeout" .agents/scripts/shared-constants.sh

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

# Verify the _ai_check_duplicate function location and see if it has access to portable_timeout
sed -n '76,90p' .agents/scripts/issue-sync-helper.sh | cat -n

Repository: marcusquinn/aidevops

Length of output: 500


🏁 Script executed:

# Check how supervisor/issue-sync.sh makes portable_timeout available
head -50 .agents/scripts/supervisor/issue-sync.sh | grep -E "(source|portable)"

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

# Determine if issue-sync-lib.sh should be sourcing supervisor/_common.sh or if portable_timeout should be in shared-constants.sh
grep -A 5 "^source" .agents/scripts/issue-sync-lib.sh

Repository: marcusquinn/aidevops

Length of output: 296


🏁 Script executed:

# Confirm: does the current issue-sync-helper.sh have timeout but not portable_timeout?
grep -n "timeout" .agents/scripts/issue-sync-helper.sh | grep -v "#"

Repository: marcusquinn/aidevops

Length of output: 213


🏁 Script executed:

# Look at the overall structure of supervisor/issue-sync.sh to understand how it sources portable_timeout
head -60 .agents/scripts/supervisor/issue-sync.sh | cat -n

Repository: marcusquinn/aidevops

Length of output: 3385


🏁 Script executed:

# Check if there's a way issue-sync-helper.sh should be getting portable_timeout
# Look at what shared-constants.sh actually contains
grep -n "^[a-z_]*() {" .agents/scripts/shared-constants.sh | head -20

Repository: marcusquinn/aidevops

Length of output: 558


🏁 Script executed:

# Check if supervisor/_common.sh is the right source for portable_timeout and see if issue-sync should use it
head -30 .agents/scripts/supervisor/_common.sh | cat -n

Repository: marcusquinn/aidevops

Length of output: 1159


🏁 Script executed:

# Verify: does issue-sync-helper.sh work at all currently, or is this a new addition?
git log -1 --oneline .agents/scripts/issue-sync-helper.sh 2>/dev/null || echo "Git not available"

Repository: marcusquinn/aidevops

Length of output: 149


🏁 Script executed:

# Verify the new feature commit changed timeout to something
git show 642a0af --stat 2>/dev/null | head -20

Repository: marcusquinn/aidevops

Length of output: 1110


🏁 Script executed:

# Check if there are other scripts in .agents/scripts (not supervisor/) that use portable_timeout
grep -l "portable_timeout" .agents/scripts/*.sh

Repository: marcusquinn/aidevops

Length of output: 100


🏁 Script executed:

# See if portable_timeout should be added to shared-constants.sh
grep -A 15 "^portable_timeout" .agents/scripts/supervisor/_common.sh | head -40

Repository: marcusquinn/aidevops

Length of output: 374


🏁 Script executed:

# Check if portable_timeout should be in shared-constants.sh by looking at agent-test-helper.sh
sed -n '160,180p' .agents/scripts/agent-test-helper.sh | cat -n

Repository: marcusquinn/aidevops

Length of output: 688


🏁 Script executed:

# Verify if there's a way issue-sync-helper.sh is supposed to work currently
# by checking if it has its own timeout handling or workaround
grep -B 5 -A 15 "^_ai_check_duplicate" .agents/scripts/issue-sync-helper.sh | head -40

Repository: marcusquinn/aidevops

Length of output: 572


🏁 Script executed:

# Check what portable_timeout implementation looks like in agent-test-helper.sh completely
sed -n '166,220p' .agents/scripts/agent-test-helper.sh | cat -n

Repository: marcusquinn/aidevops

Length of output: 1616


🏁 Script executed:

# Final check: confirm that issue-sync-helper.sh cannot call portable_timeout
bash -c '
source .agents/scripts/shared-constants.sh
source .agents/scripts/issue-sync-lib.sh
declare -f portable_timeout >/dev/null 2>&1 && echo "portable_timeout is available" || echo "portable_timeout is NOT available"
'

Repository: marcusquinn/aidevops

Length of output: 97


Add portable_timeout to shared-constants.sh, then replace raw timeout calls on lines 121 and 128.

The timeout command is unavailable on macOS runners by default, causing the dedup AI check to silently fail. A portable_timeout function already exists in agent-test-helper.sh that provides GNU timeout, gtimeout, and a background+kill fallback — it should be added to shared-constants.sh (which this script already sources) and used here instead.

Suggested fix
  1. Add portable_timeout() to shared-constants.sh (copy from agent-test-helper.sh lines 166–203)
  2. Update lines 121 and 128:
-		ai_result=$(timeout 15 opencode run \
+		ai_result=$(portable_timeout 15 opencode run \
 			-m "anthropic/claude-sonnet-4-20250514" \
 			--format default \
 			--title "dedup-$$" \
 			"$prompt" 2>/dev/null || echo "")
@@
-		ai_result=$(timeout 15 claude \
+		ai_result=$(portable_timeout 15 claude \
 			-p "$prompt" \
 			--model "claude-sonnet-4-20250514" \
 			--output-format text 2>/dev/null || echo "")

Per coding guidelines .agents/scripts/*.sh: Automation scripts must ensure reliability and robustness across platforms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/scripts/issue-sync-helper.sh around lines 121 - 132, Add the
portable_timeout() function to shared-constants.sh (copy the implementation from
agent-test-helper.sh lines 166–203) so GNU timeout, gtimeout, and
background+kill fallback are available, then replace the raw timeout invocations
in this script (the two calls that set ai_result around the opencode/claude
invocations) with portable_timeout to ensure the dedup AI check uses the
portable timeout wrapper.

Comment on lines +138 to +142
local json_block
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1)
if [[ -z "$json_block" ]]; then
return 1
fi
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the top of the file for set -euo pipefail or equivalent
head -30 .agents/scripts/issue-sync-helper.sh

# Also check lines around 138-142 for context
sed -n '130,150p' .agents/scripts/issue-sync-helper.sh

# Check if there are other instances of similar patterns in the file
rg -n 'grep.*\|.*head' .agents/scripts/issue-sync-helper.sh

Repository: marcusquinn/aidevops

Length of output: 3963


Add error handling to grep pipeline on line 139 to prevent hard exit under set -euo pipefail.

The grep command can fail when no JSON block is found, causing the script to terminate before the fallback check if [[ -z "$json_block" ]] executes. This pattern is inconsistent with similar grep operations throughout the script (lines 1268, 1317, 1465, etc.), which all include || echo "" or || true protection.

Suggested fix
-	json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1)
+	json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1 || true)
 	if [[ -z "$json_block" ]]; then
 		return 1
 	fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local json_block
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1)
if [[ -z "$json_block" ]]; then
return 1
fi
local json_block
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1 || true)
if [[ -z "$json_block" ]]; then
return 1
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/scripts/issue-sync-helper.sh around lines 138 - 142, The grep
pipeline that sets json_block from ai_result can fail under set -euo pipefail
and prematurely exit; modify the pipeline that assigns json_block (the command
using printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1) to tolerate
no-match failures by appending a protective fallback (e.g., add "|| echo \"\""
or "|| true" to the pipeline) so the subsequent if [[ -z "$json_block" ]] check
runs; update the assignment to ensure json_block is set to an empty string on
grep failure while keeping the grep/head logic intact.

Comment on lines +2249 to +2259
if dedup_result=$(ai_detect_duplicate_issue "$new_title" "" "$repo_slug" 2>/dev/null); then
local dup_number
dup_number=$(printf '%s' "$dedup_result" | jq -r '.duplicate_of // ""' 2>/dev/null | tr -d '#')
if [[ -n "$dup_number" && "$dup_number" =~ ^[0-9]+$ ]]; then
local dup_reason
dup_reason=$(printf '%s' "$dedup_result" | jq -r '.reason // ""' 2>/dev/null)
log_warn "Semantic duplicate detected: $task_id duplicates #${dup_number} — $dup_reason"
log_info "Linking $task_id to existing issue #${dup_number} instead of creating new"
echo "$dup_number"
return 0
fi
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Semantic-duplicate branch returns before persisting the task→issue link.

This early return skips the normal link persistence path (TODO ref + DB issue_url update), so the task can remain unlinked even though you found a valid existing issue.

Suggested fix
 	if dedup_result=$(ai_detect_duplicate_issue "$new_title" "" "$repo_slug" 2>/dev/null); then
 		local dup_number
 		dup_number=$(printf '%s' "$dedup_result" | jq -r '.duplicate_of // ""' 2>/dev/null | tr -d '#')
 		if [[ -n "$dup_number" && "$dup_number" =~ ^[0-9]+$ ]]; then
 			local dup_reason
 			dup_reason=$(printf '%s' "$dedup_result" | jq -r '.reason // ""' 2>/dev/null)
 			log_warn "Semantic duplicate detected: $task_id duplicates #${dup_number} — $dup_reason"
 			log_info "Linking $task_id to existing issue #${dup_number} instead of creating new"
+			local todo_file="$repo_path/TODO.md"
+			if [[ -f "$todo_file" ]]; then
+				add_gh_ref_to_todo "$task_id" "$dup_number" "$todo_file"
+				commit_and_push_todo "$repo_path" "chore: link $task_id to existing GH#${dup_number}"
+			fi
+			local escaped_id escaped_url
+			escaped_id=$(sql_escape "$task_id")
+			escaped_url=$(sql_escape "https://github.com/${repo_slug}/issues/${dup_number}")
+			db "$SUPERVISOR_DB" "UPDATE tasks SET issue_url = '$escaped_url' WHERE id = '$escaped_id';"
 			echo "$dup_number"
 			return 0
 		fi
 	fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/scripts/supervisor/issue-sync.sh around lines 2249 - 2259, The
semantic-duplicate branch currently echoes dup_number and returns early,
skipping the task→issue persistence and DB issue_url update; instead, after
detecting a duplicate (inside the ai_detect_duplicate_issue handling where
dup_number and dup_reason are extracted and task_id is available), call or reuse
the same link-persistence routine used for newly-created issues (the code path
that persists the task→issue link and updates the DB issue_url) or set the
necessary variables so execution falls through to that routine—do not return
immediately; also log the linkage and ensure the DB update (issue_url) is
performed for task_id linking to #${dup_number}.

Comment on lines +2396 to +2401
local json_block
json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1)
if [[ -z "$json_block" ]]; then
log_verbose "ai_detect_duplicate_issue: no JSON in response"
return 2
fi
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check file size and read critical sections
wc -l .agents/scripts/supervisor/issue-sync.sh

# Read the shebang and initial shell options
head -20 .agents/scripts/supervisor/issue-sync.sh

# Search for set -e, set -o pipefail, or set -euo pipefail at the beginning
head -50 .agents/scripts/supervisor/issue-sync.sh | grep -E '(set -|set \+)'

Repository: marcusquinn/aidevops

Length of output: 860


🏁 Script executed:

#!/bin/bash

# Read the specific lines mentioned: 2396-2401
sed -n '2390,2410p' .agents/scripts/supervisor/issue-sync.sh

# Read the secondary location: 2539-2544
sed -n '2535,2550p' .agents/scripts/supervisor/issue-sync.sh

Repository: marcusquinn/aidevops

Length of output: 1240


🏁 Script executed:

#!/bin/bash

# Check entire script for all set options
grep -n "^set " .agents/scripts/supervisor/issue-sync.sh | head -20

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

#!/bin/bash

# Check if ShellCheck is available, if not install it
if ! command -v shellcheck &> /dev/null; then
    apt-get update -qq && apt-get install -y shellcheck > /dev/null 2>&1
fi

# Run ShellCheck on the file focusing on the specific areas
shellcheck -S warning .agents/scripts/supervisor/issue-sync.sh 2>&1 | grep -A 3 "2396\|2539\|SC2143\|pipefail" || echo "No specific warnings found"

Repository: marcusquinn/aidevops

Length of output: 90


🏁 Script executed:

#!/bin/bash

# Run full ShellCheck to see all issues
shellcheck .agents/scripts/supervisor/issue-sync.sh 2>&1 | head -40

Repository: marcusquinn/aidevops

Length of output: 610


🏁 Script executed:

#!/bin/bash

# Check if the script is sourced by other scripts
grep -r "source.*issue-sync.sh\|\..*issue-sync.sh" .agents --include="*.sh" 2>/dev/null || echo "No sourcing found"

Repository: marcusquinn/aidevops

Length of output: 355


Guard JSON extraction to avoid unexpected exits when sourced into errexit/pipefail contexts.

This script is sourced by supervisor-helper.sh and backfill-blocked-comments.sh. If those parent contexts enable set -o pipefail, a failed grep in the pipeline will cause the command substitution to fail and exit hard, bypassing your intended return 2 on empty JSON. Add || true to ensure the pipeline always succeeds and lets the empty-check logic handle the error case.

Applies to lines 2396–2401 and 2539–2544.

Suggested fix
-	json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1)
+	json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]+\}' | head -1 || true)
 	if [[ -z "$json_block" ]]; then
 		log_verbose "ai_detect_duplicate_issue: no JSON in response"
 		return 2
 	fi
@@
-	json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]*\}' | head -1)
+	json_block=$(printf '%s' "$ai_result" | grep -oE '\{[^}]*\}' | head -1 || true)
 	if [[ -z "$json_block" ]]; then
 		log_verbose "ai_assess_auto_dispatch: no JSON in response"
 		return 2
 	fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/scripts/supervisor/issue-sync.sh around lines 2396 - 2401, The json
extraction pipeline can cause the shell to exit under errexit/pipefail when
sourced; update the command substitution that sets json_block (in
ai_detect_duplicate_issue and the other json-extraction block) to append "||
true" to the pipeline so the substitution never fails, allowing the existing [[
-z "$json_block" ]] check and return 2 handling to run normally; apply the same
change to the second occurrence referenced in the comment so both json_block
assignments are guarded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant