Skip to content
Merged
Show file tree
Hide file tree
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
63 changes: 60 additions & 3 deletions .agents/scripts/issue-sync-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -860,9 +860,9 @@ cmd_pull() {

verify_gh_cli || return 1

# Get all open issues with t-number prefixes
# Get all open issues with t-number prefixes (include assignees for assignee: sync)
local issues_json
issues_json=$(gh issue list --repo "$repo_slug" --state open --limit 200 --json number,title 2>/dev/null || echo "[]")
issues_json=$(gh issue list --repo "$repo_slug" --state open --limit 200 --json number,title,assignees 2>/dev/null || echo "[]")

local synced=0
while IFS= read -r issue_line; do
Expand Down Expand Up @@ -933,7 +933,64 @@ cmd_pull() {
synced=$((synced + 1))
done < <(echo "$closed_json" | jq -c '.[]' 2>/dev/null || true)

print_info "Pull complete: $synced refs synced to TODO.md"
# Sync GitHub Issue assignees → TODO.md assignee: field (t165 bi-directional sync)
local assignee_synced=0
while IFS= read -r issue_line; do
local issue_number
issue_number=$(echo "$issue_line" | jq -r '.number' 2>/dev/null || echo "")
local issue_title
issue_title=$(echo "$issue_line" | jq -r '.title' 2>/dev/null || echo "")
local assignee_login
assignee_login=$(echo "$issue_line" | jq -r '.assignees[0].login // empty' 2>/dev/null || echo "")

local task_id
task_id=$(echo "$issue_title" | grep -oE '^t[0-9]+(\.[0-9]+)*' || echo "")
if [[ -z "$task_id" || -z "$assignee_login" ]]; then
continue
fi

# Check if task exists in TODO.md
if ! grep -qE "^- \[.\] ${task_id} " "$todo_file" 2>/dev/null; then
continue
fi

# Check if TODO.md already has an assignee: on this task
local task_line_content
task_line_content=$(grep -E "^- \[.\] ${task_id} " "$todo_file" | head -1 || echo "")
local existing_assignee
existing_assignee=$(echo "$task_line_content" | grep -oE 'assignee:[A-Za-z0-9._@-]+' | head -1 | sed 's/^assignee://' || echo "")

if [[ -n "$existing_assignee" ]]; then
# Already has an assignee — TODO.md is authoritative, don't overwrite
continue
fi

# No assignee in TODO.md but issue has assignee — sync it
if [[ "$DRY_RUN" == "true" ]]; then
print_info "[DRY-RUN] Would add assignee:$assignee_login to $task_id (from GH#$issue_number)"
assignee_synced=$((assignee_synced + 1))
continue
fi

# Add assignee:login before logged: or at end of line
local line_num
line_num=$(grep -nE "^- \[.\] ${task_id} " "$todo_file" | head -1 | cut -d: -f1)
if [[ -n "$line_num" ]]; then
local current_line
current_line=$(sed -n "${line_num}p" "$todo_file")
local new_line
if echo "$current_line" | grep -qE 'logged:'; then
new_line=$(echo "$current_line" | sed -E "s/( logged:)/ assignee:${assignee_login}\1/")
else
new_line="${current_line} assignee:${assignee_login}"
fi
sed_inplace "${line_num}s|.*|${new_line}|" "$todo_file"
log_verbose "Synced assignee:$assignee_login to $task_id (from GH#$issue_number)"
assignee_synced=$((assignee_synced + 1))
fi
done < <(echo "$issues_json" | jq -c '.[]' 2>/dev/null || true)
Comment on lines +975 to +991
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

sed replacement with unescaped new_line can corrupt TODO.md lines containing |, &, or \.

Line 987 uses the pipe | as the sed delimiter and interpolates ${new_line} directly into the replacement string. If the original task line contains | (e.g., in a URL or table syntax), & (sed backreference), or \, the sed command will either fail or silently mangle the line. This is a data-corruption risk in your source of truth file.

A safer approach is to avoid sed for the full-line replacement and use a line-addressed write instead:

Proposed fix using awk for safe line replacement
-            sed_inplace "${line_num}s|.*|${new_line}|" "$todo_file"
+            awk -v ln="$line_num" -v replacement="$new_line" 'NR==ln{print replacement; next}1' "$todo_file" > "${todo_file}.tmp" && mv "${todo_file}.tmp" "$todo_file"

Alternatively, if you want to keep using sed_inplace, escape the sed metacharacters in new_line first:

+            escaped_line=$(printf '%s\n' "$new_line" | sed 's/[&/|\\]/\\&/g')
-            sed_inplace "${line_num}s|.*|${new_line}|" "$todo_file"
+            sed_inplace "${line_num}s|.*|${escaped_line}|" "$todo_file"
📝 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
# Add assignee:login before logged: or at end of line
local line_num
line_num=$(grep -nE "^- \[.\] ${task_id} " "$todo_file" | head -1 | cut -d: -f1)
if [[ -n "$line_num" ]]; then
local current_line
current_line=$(sed -n "${line_num}p" "$todo_file")
local new_line
if echo "$current_line" | grep -qE 'logged:'; then
new_line=$(echo "$current_line" | sed -E "s/( logged:)/ assignee:${assignee_login}\1/")
else
new_line="${current_line} assignee:${assignee_login}"
fi
sed_inplace "${line_num}s|.*|${new_line}|" "$todo_file"
log_verbose "Synced assignee:$assignee_login to $task_id (from GH#$issue_number)"
assignee_synced=$((assignee_synced + 1))
fi
done < <(echo "$issues_json" | jq -c '.[]' 2>/dev/null || true)
# Add assignee:login before logged: or at end of line
local line_num
line_num=$(grep -nE "^- \[.\] ${task_id} " "$todo_file" | head -1 | cut -d: -f1)
if [[ -n "$line_num" ]]; then
local current_line
current_line=$(sed -n "${line_num}p" "$todo_file")
local new_line
if echo "$current_line" | grep -qE 'logged:'; then
new_line=$(echo "$current_line" | sed -E "s/( logged:)/ assignee:${assignee_login}\1/")
else
new_line="${current_line} assignee:${assignee_login}"
fi
escaped_line=$(printf '%s\n' "$new_line" | sed 's/[&/|\\]/\\&/g')
sed_inplace "${line_num}s|.*|${escaped_line}|" "$todo_file"
log_verbose "Synced assignee:$assignee_login to $task_id (from GH#$issue_number)"
assignee_synced=$((assignee_synced + 1))
fi
done < <(echo "$issues_json" | jq -c '.[]' 2>/dev/null || true)
🤖 Prompt for AI Agents
In @.agents/scripts/issue-sync-helper.sh around lines 975 - 991, The sed
replacement interpolates ${new_line} directly into sed_inplace which will mangle
lines containing |, &, or backslashes; fix by either (A) replacing the line
using a line-addressed tool instead of embedding the raw string into a sed
substitution (for example rewrite the file by processing todo_file with awk or
perl and emit new_line at the matching line_num), or (B) if you must keep
sed_inplace, add a helper to escape sed replacement metacharacters (escape &, \
and whichever delimiter you use) before calling sed_inplace and use a delimiter
that won't appear in content; update the block that computes new_line and the
sed_inplace call (references: variables new_line, current_line, line_num,
todo_file, and function sed_inplace) to use the safe path.


print_info "Pull complete: $synced refs synced, $assignee_synced assignees synced to TODO.md"
return 0
}

Expand Down
39 changes: 14 additions & 25 deletions .agents/scripts/pre-edit-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -203,35 +203,24 @@ source "${SCRIPT_DIR}/shared-constants.sh"
echo "FEATURE_BRANCH_WARNING=$current_branch"
exit 3
else
# Check if task is claimed by someone else on GitHub (t164)
# Check if task is claimed by someone else via TODO.md assignee: field (t165)
# Note: no 'local' — this runs at script top-level, not inside a function
task_id_from_branch=""
task_id_from_branch=$(echo "$current_branch" | grep -oE 't[0-9]+' | head -1 || true)
if [[ -n "$task_id_from_branch" ]]; then
supervisor_script="$SCRIPT_DIR/supervisor-helper.sh"
if [[ -x "$supervisor_script" ]]; then
project_root=""
project_root=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
todo_file="$project_root/TODO.md"
issue_number=""
if [[ -f "$todo_file" ]]; then
task_line=""
task_line=$(grep -E "^\- \[.\] ${task_id_from_branch} " "$todo_file" | head -1 || true)
issue_number=$(echo "$task_line" | grep -oE 'ref:GH#[0-9]+' | head -1 | sed 's/ref:GH#//' || true)
fi
if [[ -n "$issue_number" ]] && command -v gh &>/dev/null; then
repo_slug=""
repo_slug=$(git remote get-url origin 2>/dev/null | sed 's|.*github.com[:/]||;s|\.git$||' || true)
if [[ -n "$repo_slug" ]]; then
current_assignee=""
current_assignee=$(gh api "repos/$repo_slug/issues/$issue_number" --jq '.assignee.login // empty' 2>/dev/null || true)
if [[ -n "$current_assignee" ]]; then
my_login=""
my_login=$(gh api user --jq '.login' 2>/dev/null || true)
if [[ "$current_assignee" != "$my_login" ]]; then
echo -e "${YELLOW}WARNING${NC}: Task $task_id_from_branch is claimed by @$current_assignee (GH#$issue_number)"
fi
fi
project_root=""
project_root=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
todo_file="$project_root/TODO.md"
if [[ -f "$todo_file" ]]; then
task_line=""
task_line=$(grep -E "^\- \[.\] ${task_id_from_branch} " "$todo_file" | head -1 || true)
task_assignee=""
task_assignee=$(echo "$task_line" | grep -oE 'assignee:[A-Za-z0-9._@-]+' | head -1 | sed 's/^assignee://' || true)
if [[ -n "$task_assignee" ]]; then
# Must match get_aidevops_identity() in supervisor-helper.sh
my_identity="${AIDEVOPS_IDENTITY:-$(whoami 2>/dev/null || echo unknown)@$(hostname -s 2>/dev/null || echo local)}"
if [[ "$task_assignee" != "$my_identity" ]]; then
echo -e "${YELLOW}WARNING${NC}: Task $task_id_from_branch is claimed by assignee:$task_assignee"
fi
fi
fi
Expand Down
Loading