Skip to content
Open
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
87 changes: 60 additions & 27 deletions lib/task_sources.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ check_beads_available() {
#
# Parameters:
# $1 (filterStatus) - Status filter (optional, default: "open")
# $2 (limit) - Max issues to fetch (optional, default: "0" = all)
#
# Outputs:
# Tasks in markdown checkbox format, one per line
Expand All @@ -42,6 +43,7 @@ check_beads_available() {
#
fetch_beads_tasks() {
local filterStatus="${1:-open}"
local limit="${2:-0}"
local tasks=""

# Check if beads is available
Expand All @@ -50,7 +52,7 @@ fetch_beads_tasks() {
fi

# Build bd list command arguments
local bdArgs=("list" "--json")
local bdArgs=("list" "--json" "--limit" "$limit")
if [[ "$filterStatus" == "open" ]]; then
bdArgs+=("--status" "open")
elif [[ "$filterStatus" == "in_progress" ]]; then
Expand Down Expand Up @@ -78,7 +80,7 @@ fetch_beads_tasks() {
# Fallback: try plain text output if JSON failed or produced no results
if [[ -z "$tasks" ]]; then
# Build fallback args (reuse status logic, but without --json)
local fallbackArgs=("list")
local fallbackArgs=("list" "--limit" "$limit")
if [[ "$filterStatus" == "open" ]]; then
fallbackArgs+=("--status" "open")
elif [[ "$filterStatus" == "in_progress" ]]; then
Expand Down Expand Up @@ -109,11 +111,16 @@ fetch_beads_tasks() {

# get_beads_count - Get count of open beads issues
#
# Parameters:
# $1 (limit) - Max issues to fetch (optional, default: "0" = all)
#
# Returns:
# 0 and echoes the count
# 1 if beads unavailable
#
get_beads_count() {
local limit="${1:-0}"

if ! check_beads_available; then
echo "0"
return 1
Expand All @@ -122,9 +129,9 @@ get_beads_count() {
local count
if command -v jq &>/dev/null; then
# Note: Use 'select(.status == "closed" | not)' to avoid bash escaping issues with '!='
count=$(bd list --json 2>/dev/null | jq '[.[] | select(.status == "closed" | not)] | length' 2>/dev/null || echo "0")
count=$(bd list --json --limit "$limit" 2>/dev/null | jq '[.[] | select(.status == "closed" | not)] | length' 2>/dev/null || echo "0")
else
count=$(bd list 2>/dev/null | wc -l | tr -d ' ')
count=$(bd list --limit "$limit" 2>/dev/null | wc -l | tr -d ' ')
fi

echo "${count:-0}"
Expand Down Expand Up @@ -163,8 +170,8 @@ check_github_available() {
# fetch_github_tasks - Fetch issues from GitHub
#
# Parameters:
# $1 (label) - Label to filter by (optional, default: "ralph-task")
# $2 (limit) - Maximum number of issues (optional, default: 50)
# $1 (label) - Label to filter by (optional)
# $2 (limit) - Maximum number of issues (optional, default: "0" = all)
#
# Outputs:
# Tasks in markdown checkbox format
Expand All @@ -174,34 +181,58 @@ check_github_available() {
# 0 - Success
# 1 - Error
#
# Note:
# `gh issue list --limit` enforces a hard cap of 1000 regardless of the
# value supplied. When limit=0 ("all"), we fall back to `gh api --paginate`
# against the issues endpoint so large repos are not silently truncated.
# The issues endpoint returns pull requests as well, so those are filtered
# out via `.pull_request | not`.
#
fetch_github_tasks() {
local label="${1:-}"
local limit="${2:-50}"
local limit="${2:-0}"
local tasks=""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
local json_output

# Check if GitHub is available
if ! check_github_available; then
return 1
fi

# Build gh command
local gh_args=("issue" "list" "--state" "open" "--limit" "$limit" "--json" "number,title,labels")
if [[ -n "$label" ]]; then
gh_args+=("--label" "$label")
fi

# Fetch issues
local json_output
if ! json_output=$(gh "${gh_args[@]}" 2>/dev/null); then
return 1
fi

# Parse JSON and format as markdown tasks
if command -v jq &>/dev/null; then
tasks=$(echo "$json_output" | jq -r '
.[] |
"- [ ] [#\(.number)] \(.title)"
' 2>/dev/null)
if [[ "$limit" == "0" ]]; then
# Fetch all open issues via the REST API with automatic pagination.
# `-f` passes URL-encoded string fields so labels with spaces or
# special characters are handled correctly.
local api_args=("api" "--method" "GET" "repos/{owner}/{repo}/issues"
"-f" "state=open" "-f" "per_page=100" "--paginate")
if [[ -n "$label" ]]; then
api_args+=("-f" "labels=$label")
fi
if ! json_output=$(gh "${api_args[@]}" 2>/dev/null); then
return 1
fi
# Parse and format, filtering out pull requests
if command -v jq &>/dev/null; then
tasks=$(echo "$json_output" | jq -r '
.[] | select(.pull_request | not) |
"- [ ] [#\(.number)] \(.title)"
' 2>/dev/null)
fi
else
# Bounded fetch via `gh issue list` which already excludes PRs
local gh_args=("issue" "list" "--state" "open" "--limit" "$limit" "--json" "number,title,labels")
if [[ -n "$label" ]]; then
gh_args+=("--label" "$label")
fi
if ! json_output=$(gh "${gh_args[@]}" 2>/dev/null); then
return 1
fi
if command -v jq &>/dev/null; then
tasks=$(echo "$json_output" | jq -r '
.[] |
"- [ ] [#\(.number)] \(.title)"
' 2>/dev/null)
fi
Comment on lines +221 to +235
Copy link
Copy Markdown
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

🌐 Web query:

In GitHub CLI, is gh issue list --limit Nhard-capped at 1000 even whenN > 1000? If so, what is the recommended way to fetch exactly the first N open issues when N may exceed 1000?

💡 Result:

No, gh issue list --limit N is not hard-capped at 1000 when N > 1000 for basic usage without filters that trigger the Search API (like --label or --search). It uses the GitHub REST API's List issues endpoint, which supports pagination up to the specified limit via GraphQL or repeated REST calls. However, certain filters switch to the Search API, which caps at 1000 results total. The recommended way to fetch exactly the first N open issues (sorted by most recent first, default) when N may exceed 1000 is to use gh issue list --state open --limit N. For repos with more than 1000 open issues, set N large enough to cover all (e.g., estimate or use a very high number like 10000), as there is no built-in unbounded fetch; users must specify a limit intentionally. To fetch ALL open issues reliably, use gh api graphql --paginate with a query like: query($owner: String!, $repo: String!, $cursor: String) { repository(owner: $owner, name: $repo) { issues(first: 100, after: $cursor, states: OPEN, orderBy: {field: CREATED_AT, direction: DESC}) { pageInfo { hasNextPage endCursor } nodes { number title } } } } This handles cursor pagination without limits other than API constraints.

Citations:


Guard against silent result truncation when --label filtering is combined with high limits.

When --label is present in the query (lines 224-226), gh issue list switches to the Search API, which is hard-capped at 1000 results regardless of the --limit value. A call requesting --limit 5000 with a label filter will silently return only 1000 issues. Either validate that limit <= 1000 when a label filter is active, or use paginated gh api graphql with cursor pagination to fetch beyond 1000 results when needed.

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

In `@lib/task_sources.sh` around lines 221 - 235, The code can silently truncate
results when a label filter is used because `gh issue list` switches to the
Search API which is capped at 1000; update the logic around the gh_args
construction (the label variable and limit variable usage) to validate that when
label is non-empty the requested limit is <=1000 and fail fast with a clear
error (return non-zero and message) if it exceeds that, or alternatively replace
the bounded `gh issue list` branch with a paginated `gh api graphql`
cursor-based fetch to honor larger limits; implement the simpler fix by adding a
check after the `if [[ -n "$label" ]]; then` block that inspects `limit` and
returns 1 with an explanatory message if limit > 1000.

fi

if [[ -n "$tasks" ]]; then
Expand Down Expand Up @@ -490,6 +521,7 @@ prioritize_tasks() {
# $1 (sources) - Space-separated list of sources: beads, github, prd
# $2 (prd_file) - Path to PRD file (required if prd in sources)
# $3 (github_label) - GitHub label filter (optional)
# $4 (limit) - Max issues to fetch per source (optional, default: "0" = all)
#
# Outputs:
# Combined tasks in markdown format
Expand All @@ -502,14 +534,15 @@ import_tasks_from_sources() {
local sources=$1
local prd_file="${2:-}"
local github_label="${3:-}"
local limit="${4:-0}"

local all_tasks=""
local source_count=0

# Import from beads
if echo "$sources" | grep -qw "beads"; then
local beads_tasks
if beads_tasks=$(fetch_beads_tasks); then
if beads_tasks=$(fetch_beads_tasks "open" "$limit"); then
if [[ -n "$beads_tasks" ]]; then
all_tasks="${all_tasks}
# Tasks from beads
Expand All @@ -523,7 +556,7 @@ ${beads_tasks}
# Import from GitHub
if echo "$sources" | grep -qw "github"; then
local github_tasks
if github_tasks=$(fetch_github_tasks "$github_label"); then
if github_tasks=$(fetch_github_tasks "$github_label" "$limit"); then
if [[ -n "$github_tasks" ]]; then
all_tasks="${all_tasks}
# Tasks from GitHub
Expand Down
15 changes: 13 additions & 2 deletions ralph_enable.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ SKIP_TASKS=false
TASK_SOURCE=""
PRD_FILE=""
GITHUB_LABEL=""
IMPORT_LIMIT="0"
NON_INTERACTIVE=false
SHOW_HELP=false

Expand All @@ -63,6 +64,7 @@ Options:
--from <source> Import tasks from: beads, github, prd
--prd <file> PRD file to convert (when --from prd)
--label <label> GitHub label filter (when --from github)
--limit <n> Max issues to import (default: 0 = all)
--force Overwrite existing .ralph/ configuration
--skip-tasks Skip task import, use default templates
--non-interactive Run with defaults (no prompts)
Expand Down Expand Up @@ -142,6 +144,15 @@ parse_arguments() {
exit $ENABLE_INVALID_ARGS
fi
;;
--limit)
if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then
IMPORT_LIMIT="$2"
shift 2
else
echo "Error: --limit requires a non-negative integer" >&2
exit $ENABLE_INVALID_ARGS
fi
;;
--force)
FORCE_OVERWRITE=true
shift
Expand Down Expand Up @@ -403,7 +414,7 @@ phase_file_generation() {

if echo "$SELECTED_SOURCES" | grep -qw "beads"; then
local beads_tasks
if beads_tasks=$(fetch_beads_tasks); then
if beads_tasks=$(fetch_beads_tasks "open" "$IMPORT_LIMIT"); then
imported_tasks="${imported_tasks}${beads_tasks}
"
print_success "Imported tasks from beads"
Expand All @@ -412,7 +423,7 @@ phase_file_generation() {

if echo "$SELECTED_SOURCES" | grep -qw "github"; then
local github_tasks
if github_tasks=$(fetch_github_tasks "$CONFIG_GITHUB_LABEL"); then
if github_tasks=$(fetch_github_tasks "$CONFIG_GITHUB_LABEL" "$IMPORT_LIMIT"); then
imported_tasks="${imported_tasks}${github_tasks}
"
print_success "Imported tasks from GitHub"
Expand Down
24 changes: 19 additions & 5 deletions ralph_enable_ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ FORCE_OVERWRITE=false
TASK_SOURCE=""
PRD_FILE=""
GITHUB_LABEL="ralph-task"
IMPORT_LIMIT="0"
PROJECT_NAME=""
PROJECT_TYPE=""
OUTPUT_JSON=false
Expand All @@ -73,6 +74,7 @@ Options:
--from <source> Import tasks from: beads, github, prd, none
--prd <file> PRD file to convert (when --from prd)
--label <label> GitHub label filter (default: ralph-task)
--limit <n> Max issues to import (default: 0 = all)
--project-name <name> Override detected project name
--project-type <type> Override detected type (typescript, python, etc.)
--force Overwrite existing .ralph/ configuration
Expand Down Expand Up @@ -155,6 +157,15 @@ parse_arguments() {
exit $ENABLE_INVALID_ARGS
fi
;;
--limit)
if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then
IMPORT_LIMIT="$2"
shift 2
else
output_error "--limit requires a non-negative integer"
exit $ENABLE_INVALID_ARGS
fi
;;
--project-name)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
PROJECT_NAME="$2"
Expand Down Expand Up @@ -322,26 +333,29 @@ main() {

# Import tasks
local imported_tasks=""
local beads_tasks=""
local github_tasks=""
local prd_tasks=""
case "$TASK_SOURCE" in
beads)
if beads_tasks=$(fetch_beads_tasks 2>/dev/null); then
if beads_tasks=$(fetch_beads_tasks "open" "$IMPORT_LIMIT" 2>/dev/null); then
imported_tasks="$beads_tasks"
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[') || TASKS_IMPORTED=0
output_message "Imported $TASKS_IMPORTED tasks from beads"
fi
;;
github)
if github_tasks=$(fetch_github_tasks "$GITHUB_LABEL" 2>/dev/null); then
if github_tasks=$(fetch_github_tasks "$GITHUB_LABEL" "$IMPORT_LIMIT" 2>/dev/null); then
imported_tasks="$github_tasks"
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[') || TASKS_IMPORTED=0
output_message "Imported $TASKS_IMPORTED tasks from GitHub"
fi
;;
prd)
if [[ -n "$PRD_FILE" && -f "$PRD_FILE" ]]; then
if prd_tasks=$(extract_prd_tasks "$PRD_FILE" 2>/dev/null); then
imported_tasks="$prd_tasks"
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[') || TASKS_IMPORTED=0
output_message "Extracted $TASKS_IMPORTED tasks from PRD"
fi
else
Expand Down
Loading
Loading