Ralph TUI - Create GitHub Issues
Converts PRDs to flat GitHub Issues for ralph-tui autonomous execution using GitHub Issues with native dependency tracking via the gh CLI and GraphQL API. Issues are grouped by a feature:<slug> label.
Note: This skill uses GitHub Issues as the task tracker. If you prefer local-only tracking with beads-rust (
br), use theralph-tui-create-beads-rustskill instead.
The Job
Take a PRD (markdown file or text) and create GitHub Issues using gh commands:
- Extract Quality Gates from the PRD's "Quality Gates" section
- Create one issue per user story (with quality gates appended), all sharing a
feature:<slug>label - Add native dependencies between issues via GraphQL
addBlockedBymutation - Output ready for
ralph-tui run --tracker github --labels "feature:<slug>"
Step 1: Extract Quality Gates
Look for the "Quality Gates" section in the PRD:
## Quality Gates
These commands must pass for every user story:
- `bun run typecheck` - Type checking
- `bun run lint` - Linting
For UI stories, also include:
- Verify in browser using dev-browser skill
Extract:
- Universal gates: Commands that apply to ALL stories (e.g.,
bun run typecheck) - UI gates: Commands that apply only to UI stories (e.g., browser verification)
If no Quality Gates section exists: Ask the user what commands should pass, or use a sensible default like bun run typecheck.
Output Format
Issues use gh issue create with HEREDOC syntax to safely handle special characters.
Creating issues (one per user story)
gh issue create --title "US-001: [Story Title]" --body "$(cat <<'EOF'
## Description
[Story description]
## Acceptance Criteria
- [ ] Story-specific criterion 1
- [ ] Story-specific criterion 2
- [ ] bun run typecheck passes
- [ ] bun run lint passes
## Blocked by
- Blocked by #<earlier-issue-number> (if any dependency)
- Or "None - can start immediately"
EOF
)" --label "feature:<slug>" --label "priority:1"
- Parse the issue number from the output URL (last path segment)
- Get the node ID for GraphQL operations:
gh issue view <number> --json id --jq .id
CRITICAL: Always use
<<'EOF'(single-quoted) for the HEREDOC delimiter. This prevents shell interpretation of backticks,$variables, and()in descriptions.
Adding native dependencies (GraphQL)
When one issue is blocked by another, add the dependency:
gh api graphql -f query='mutation($issueId: ID!, $blockerId: ID!) {
addBlockedBy(input: { issueId: $issueId, blockingIssueId: $blockerId }) {
clientMutationId
}
}' -f issueId="<blocked-node-id>" -f blockerId="<blocker-node-id>"
Syntax: The issueId is the issue that is blocked; the blockingIssueId is the blocker.
Feature Label
All issues from the same PRD share a feature:<slug> label for grouping. The slug should be a kebab-case summary of the feature name.
Examples:
feature:friends-outreachfeature:auth-middlewarefeature:dashboard-filters
This label is how ralph-tui scopes which issues to work on.
Priority Labels
Each issue gets a priority:N label where N is 0-4. Priority is assigned by dependency order then document order:
| Priority | Meaning |
|---|---|
priority:0 |
Critical - Drop everything |
priority:1 |
High - Do soon |
priority:2 |
Medium - Normal work |
priority:3 |
Low - When time permits |
priority:4 |
Backlog - Someday/maybe |
Foundation stories (schema, first in chain) get lower numbers; later dependent stories get higher numbers.
Story Size: The #1 Rule
Each story must be completable in ONE ralph-tui iteration (~one agent context window).
ralph-tui spawns a fresh agent instance per iteration with no memory of previous work. If a story is too big, the agent runs out of context before finishing.
Right-sized stories:
- Add a database column + migration
- Add a UI component to an existing page
- Update a server action with new logic
- Add a filter dropdown to a list
Too big (split these):
- "Build the entire dashboard" --> Split into: schema, queries, UI components, filters
- "Add authentication" --> Split into: schema, middleware, login UI, session handling
- "Refactor the API" --> Split into one story per endpoint or pattern
Rule of thumb: If you can't describe the change in 2-3 sentences, it's too big.
Story Ordering: Dependencies First
Stories execute in dependency order. Earlier stories must not depend on later ones.
Correct order:
- Schema/database changes (migrations)
- Server actions / backend logic
- UI components that use the backend
- Dashboard/summary views that aggregate data
Wrong order:
- UI component (depends on schema that doesn't exist yet)
- Schema change
Acceptance Criteria: Quality Gates + Story-Specific
Each issue's body should include acceptance criteria with:
- Story-specific criteria from the PRD (what this story accomplishes)
- Quality gates from the PRD's Quality Gates section (appended at the end)
Good criteria (verifiable):
- "Add
investorTypecolumn to investor table with default 'cold'" - "Filter dropdown has options: All, Cold, Friend"
- "Clicking toggle shows confirmation dialog"
Bad criteria (vague):
- "Works correctly"
- "User can do X easily"
- "Good UX"
- "Handles edge cases"
Conversion Rules
- Extract Quality Gates from PRD first
- Each user story --> one GitHub issue
- First story: No dependencies (creates foundation)
- Subsequent stories: Depend on their predecessors (UI depends on backend, etc.)
- Priority: Based on dependency order, then document order (
priority:0=critical,priority:2=medium,priority:4=backlog) - All stories: Created as open issues
- Acceptance criteria: Story criteria + quality gates appended
- UI stories: Also append UI-specific gates (browser verification)
- Blocked by section: Every issue body includes a "Blocked by" section referencing blocker issue numbers (or "None")
- Feature label: Every issue gets the same
feature:<slug>label
Splitting Large PRDs
If a PRD has big features, split them:
Original:
"Add friends outreach track with different messaging"
Split into:
- US-001: Add investorType field to database
- US-002: Add type toggle to investor list UI
- US-003: Create friend-specific phase progression logic
- US-004: Create friend message templates
- US-005: Wire up task generation for friends
- US-006: Add filter by type
- US-007: Update new investor form
- US-008: Update dashboard counts
Each is one focused change that can be completed and verified independently.
Example
Input PRD:
# PRD: Friends Outreach
Add ability to mark investors as "friends" for warm outreach.
## Quality Gates
These commands must pass for every user story:
- `bun run typecheck` - Type checking
- `bun run lint` - Linting
For UI stories, also include:
- Verify in browser using dev-browser skill
## User Stories
### US-001: Add investorType field to investor table
**Description:** As a developer, I need to categorize investors as 'cold' or 'friend'.
**Acceptance Criteria:**
- [ ] Add investorType column: 'cold' | 'friend' (default 'cold')
- [ ] Generate and run migration successfully
### US-002: Add type toggle to investor list rows
**Description:** As Ryan, I want to toggle investor type directly from the list.
**Acceptance Criteria:**
- [ ] Each row has Cold | Friend toggle
- [ ] Switching shows confirmation dialog
- [ ] On confirm: updates type in database
### US-003: Filter investors by type
**Description:** As Ryan, I want to filter the list to see just friends or cold.
**Acceptance Criteria:**
- [ ] Filter dropdown: All | Cold | Friend
- [ ] Filter persists in URL params
Output GitHub Issues:
# --- US-001: No deps (first - creates schema) ---
gh issue create --title "US-001: Add investorType field to investor table" --body "$(cat <<'EOF'
## Description
As a developer, I need to categorize investors as 'cold' or 'friend'.
## Acceptance Criteria
- [ ] Add investorType column: 'cold' | 'friend' (default 'cold')
- [ ] Generate and run migration successfully
- [ ] bun run typecheck passes
- [ ] bun run lint passes
## Blocked by
None - can start immediately
EOF
)" --label "feature:friends-outreach" --label "priority:1"
# Parse issue number, e.g., #11
# US001_NUMBER=11
# US001_NODE_ID=$(gh issue view 11 --json id --jq .id)
# --- US-002: UI story (gets browser verification too) ---
gh issue create --title "US-002: Add type toggle to investor list rows" --body "$(cat <<'EOF'
## Description
As Ryan, I want to toggle investor type directly from the list.
## Acceptance Criteria
- [ ] Each row has Cold | Friend toggle
- [ ] Switching shows confirmation dialog
- [ ] On confirm: updates type in database
- [ ] bun run typecheck passes
- [ ] bun run lint passes
- [ ] Verify in browser using dev-browser skill
## Blocked by
- Blocked by #11
EOF
)" --label "feature:friends-outreach" --label "priority:2"
# Parse issue number, e.g., #12
# US002_NUMBER=12
# US002_NODE_ID=$(gh issue view 12 --json id --jq .id)
# Add dependency: US-002 is blocked by US-001
gh api graphql -f query='mutation($issueId: ID!, $blockerId: ID!) {
addBlockedBy(input: { issueId: $issueId, blockingIssueId: $blockerId }) {
clientMutationId
}
}' -f issueId="$US002_NODE_ID" -f blockerId="$US001_NODE_ID"
# --- US-003: UI story ---
gh issue create --title "US-003: Filter investors by type" --body "$(cat <<'EOF'
## Description
As Ryan, I want to filter the list to see just friends or cold.
## Acceptance Criteria
- [ ] Filter dropdown: All | Cold | Friend
- [ ] Filter persists in URL params
- [ ] bun run typecheck passes
- [ ] bun run lint passes
- [ ] Verify in browser using dev-browser skill
## Blocked by
- Blocked by #12
EOF
)" --label "feature:friends-outreach" --label "priority:3"
# Parse issue number, e.g., #13
# US003_NUMBER=13
# US003_NODE_ID=$(gh issue view 13 --json id --jq .id)
# Add dependency: US-003 is blocked by US-002
gh api graphql -f query='mutation($issueId: ID!, $blockerId: ID!) {
addBlockedBy(input: { issueId: $issueId, blockingIssueId: $blockerId }) {
clientMutationId
}
}' -f issueId="$US003_NODE_ID" -f blockerId="$US002_NODE_ID"
Running ralph-tui
After creation, run ralph-tui:
# Work on issues with a specific feature label
ralph-tui run --tracker github --labels "feature:friends-outreach"
# Or let it pick the best task automatically
ralph-tui run --tracker github
ralph-tui will:
- Work on issues matching the label filter (or select the best available task)
- Close each issue when complete
- Output
<promise>COMPLETE</promise>when all issues are done
Checklist Before Creating Issues
- Extracted Quality Gates from PRD (or asked user if missing)
- Each story is completable in one iteration (small enough)
- Stories are ordered by dependency (schema --> backend --> UI)
- Quality gates appended to every issue's acceptance criteria
- UI stories have browser verification (if specified in Quality Gates)
- Acceptance criteria are verifiable (not vague)
- No story depends on a later story (only earlier stories)
- Dependencies added via
addBlockedByGraphQL mutation - "Blocked by #N" text included in issue bodies
- Priority labels (
priority:N) applied to all issues - All issues share the same
feature:<slug>label
Gotchas
addBlockedBy mutation return field
The AddBlockedByPayload type does not have a blockedByIssue field. Using blockedByIssue { id } as the return selection will fail with:
Field 'blockedByIssue' doesn't exist on type 'AddBlockedByPayload'
Use clientMutationId instead:
mutation($issueId: ID!, $blockerId: ID!) {
addBlockedBy(input: { issueId: $issueId, blockingIssueId: $blockerId }) {
clientMutationId
}
}
General rule: When unsure about a GitHub GraphQL mutation's return payload fields,
clientMutationIdis always a safe fallback — it exists on every mutation payload type.
Unicode / encoding issues with inline GraphQL
When passing GraphQL queries inline via -f query='...', the $ signs in variable declarations can get corrupted by shell or Unicode encoding issues, producing:
Expected VAR_SIGN, actual: UNKNOWN_CHAR ("") at [1, 22]
Fix: Write the query to a temp file and use -F query=@file:
cat > /tmp/blocked_by.graphql << 'GQLEOF'
mutation($issueId: ID!, $blockerId: ID!) {
addBlockedBy(input: { issueId: $issueId, blockingIssueId: $blockerId }) {
clientMutationId
}
}
GQLEOF
gh api graphql -F query=@/tmp/blocked_by.graphql -f issueId="<node-id>" -f blockerId="<node-id>"
When to use this: If any inline
-f query='...'call fails withUNKNOWN_CHAR, switch to the temp file approach. This is especially common when queries are constructed or interpolated programmatically.
Differences from beads-rust
| Concept | beads-rust (br) |
GitHub Issues (gh) |
|---|---|---|
| Create story | br create --parent=ID |
gh issue create --label "feature:<slug>" --label "priority:N" |
| Group issues | Epic parent (--type=epic) |
feature:<slug> label |
| Add dependency | br dep add <issue> <blocker> |
addBlockedBy GraphQL mutation |
| Get issue ID | Returned by br create |
Parse URL + gh issue view --json id |
| Close | br close <id> |
gh issue close <number> |
| Storage | .beads/*.db + JSONL |
GitHub (remote) |
| Sync | br sync --flush-only |
Not needed (issues are remote) |
| Tracker flag | --tracker beads-rust |
--tracker github |