This document outlines the detailed implementation plan for expanding gh-please extension with new command groups (ai, issue, pr) and features.
See ADR 001 for architectural decisions.
# PleaseAI triggers (ai group) - Creates GitHub comments to trigger bot
gh please ai triage <issue-number> # → /please triage
gh please ai investigate <issue-number> # → /please investigate
gh please ai fix <issue-number> # → /please fix
gh please ai review <pr-number> # → /please review
gh please ai apply <pr-number> # → /please apply
# Issue management (issue group) - Direct GraphQL/REST API calls
gh please issue sub-issue create <parent> --title "..." [--body "..."]
gh please issue sub-issue add <parent> <child>
gh please issue sub-issue list <parent>
gh please issue dependency add <issue> --blocked-by <blocker>
gh please issue dependency remove <issue> <blocker>
gh please issue dependency list <issue>
# PR management (pr group) - Direct GraphQL/REST API calls
gh please pr review-reply <comment-id> -b "text" # Review comment reply
gh please pr resolve <pr-number> [--thread <id> | --all] # Resolve threads
# Configuration
gh please init [-f] [-y]
# Deprecated (backward compatibility)
gh please review-reply <comment-id> -b "text" # → redirects to pr review-replysrc/
├── commands/
│ ├── ai/ # PleaseAI trigger commands
│ │ ├── triage.ts
│ │ ├── investigate.ts
│ │ ├── fix.ts
│ │ ├── review.ts
│ │ ├── apply.ts
│ │ └── index.ts # ai group command registration
│ ├── issue/ # Issue management commands
│ │ ├── sub-issue.ts # create, add, list subcommands
│ │ ├── dependency.ts # add, remove, list subcommands
│ │ └── index.ts # issue group command registration
│ ├── pr/ # PR management commands
│ │ ├── review-reply.ts # moved from commands/
│ │ ├── resolve.ts
│ │ └── index.ts # pr group command registration
│ └── init.ts # existing, unchanged
├── lib/
│ ├── github-api.ts # existing + comment creation
│ ├── github-graphql.ts # NEW: GraphQL client
│ ├── github-rest.ts # NEW: REST API extensions
│ ├── please-trigger.ts # NEW: PleaseAI trigger helpers
│ ├── validation.ts # existing, may extend
│ └── i18n.ts # existing
├── config/
│ └── schema.ts # existing, may extend
├── types.ts # existing + new types
└── index.ts # command registration
test/
├── commands/
│ ├── ai/
│ │ ├── triage.test.ts
│ │ ├── investigate.test.ts
│ │ ├── fix.test.ts
│ │ ├── review.test.ts
│ │ └── apply.test.ts
│ ├── issue/
│ │ ├── sub-issue.test.ts
│ │ └── dependency.test.ts
│ └── pr/
│ ├── review-reply.test.ts # existing, moved
│ └── resolve.test.ts
└── lib/
├── github-graphql.test.ts
├── github-rest.test.ts
└── please-trigger.test.ts
Goal: Build common libraries for GraphQL, REST API, and PleaseAI triggers.
src/lib/github-graphql.ts (NEW)
/**
* Execute a GraphQL query via gh CLI
*/
export async function executeGraphQL(query: string, variables: Record<string, any>): Promise<any>
/**
* Get GitHub node ID for an issue
*/
export async function getIssueNodeId(owner: string, repo: string, issueNumber: number): Promise<string>
/**
* Get GitHub node ID for a PR
*/
export async function getPrNodeId(owner: string, repo: string, prNumber: number): Promise<string>
/**
* Create a sub-issue linked to a parent issue
*/
export async function createSubIssue(
parentNodeId: string,
title: string,
body?: string
): Promise<{ nodeId: string, number: number }>
/**
* Add existing issue as sub-issue to parent
*/
export async function addSubIssue(parentNodeId: string, childNodeId: string): Promise<void>
/**
* List all sub-issues of a parent issue
*/
export async function listSubIssues(parentNodeId: string): Promise<Array<{
number: number
title: string
state: string
nodeId: string
}>>
/**
* Resolve a review thread
*/
export async function resolveReviewThread(threadNodeId: string): Promise<void>
/**
* List all review threads for a PR
*/
export async function listReviewThreads(prNodeId: string): Promise<Array<{
id: string
nodeId: string
isResolved: boolean
path: string
line: number | null
}>>src/lib/github-rest.ts (NEW)
/**
* Add a blocked_by dependency to an issue
*/
export async function addIssueDependency(
owner: string,
repo: string,
issueNumber: number,
blockedByNumber: number
): Promise<void>
/**
* Remove a blocked_by dependency from an issue
*/
export async function removeIssueDependency(
owner: string,
repo: string,
issueNumber: number,
blockerNumber: number
): Promise<void>
/**
* List all dependencies for an issue
*/
export async function listIssueDependencies(
owner: string,
repo: string,
issueNumber: number
): Promise<Array<{
number: number
title: string
state: string
}>>src/lib/please-trigger.ts (NEW)
export type PleaseTriggerType = 'triage' | 'investigate' | 'fix' | 'review' | 'apply'
/**
* Create a PleaseAI trigger comment on an issue
*/
export async function triggerPleaseAI(
type: PleaseTriggerType,
owner: string,
repo: string,
issueOrPrNumber: number
): Promise<void>
/**
* Build trigger comment body
*/
export function buildTriggerComment(type: PleaseTriggerType): stringsrc/lib/github-api.ts (EXTEND)
Add these functions:
/**
* Get current repository info (owner, repo)
*/
export async function getRepoInfo(): Promise<{ owner: string, repo: string }>
/**
* Create a comment on an issue
*/
export async function createIssueComment(
owner: string,
repo: string,
issueNumber: number,
body: string
): Promise<void>
/**
* Create a comment on a PR
*/
export async function createPrComment(
owner: string,
repo: string,
prNumber: number,
body: string
): Promise<void>src/types.ts (EXTEND)
// Existing types remain...
export interface IssueInfo {
number: number
owner: string
repo: string
nodeId?: string
}
export interface SubIssueCreateOptions {
parentNodeId: string
title: string
body?: string
}
export interface IssueDependency {
issueNumber: number
blockedBy: number[]
}
export interface ReviewThread {
id: string
nodeId: string
isResolved: boolean
path: string
line: number | null
}
export type PleaseTriggerType = 'triage' | 'investigate' | 'fix' | 'review' | 'apply'Tests: test/lib/github-graphql.test.ts, test/lib/github-rest.test.ts, test/lib/please-trigger.test.ts
Goal: Implement commands that trigger PleaseAI bot via comments.
src/commands/ai/triage.ts
import { Command } from 'commander'
import { getRepoInfo } from '../../lib/github-api'
import { triggerPleaseAI } from '../../lib/please-trigger'
export function createTriageCommand(): Command {
const command = new Command('triage')
command
.description('Trigger PleaseAI to triage an issue')
.argument('<issue-number>', 'Issue number to triage')
.action(async (issueNumberStr: string) => {
try {
const issueNumber = Number.parseInt(issueNumberStr, 10)
if (Number.Number.isNaN(issueNumber)) {
throw new TypeError('Issue number must be a valid number')
}
const { owner, repo } = await getRepoInfo()
console.log(`🤖 Triggering PleaseAI triage for issue #${issueNumber}...`)
await triggerPleaseAI('triage', owner, repo, issueNumber)
console.log(`✅ Triage request posted to issue #${issueNumber}`)
console.log(` View: https://github.com/${owner}/${repo}/issues/${issueNumber}`)
}
catch (error) {
console.error(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
process.exit(1)
}
})
return command
}Similar files:
src/commands/ai/investigate.tssrc/commands/ai/fix.tssrc/commands/ai/review.ts(uses prNumber instead)src/commands/ai/apply.ts(uses prNumber instead)
src/commands/ai/index.ts
import { Command } from 'commander'
import { createApplyCommand } from './apply'
import { createFixCommand } from './fix'
import { createInvestigateCommand } from './investigate'
import { createReviewCommand } from './review'
import { createTriageCommand } from './triage'
export function createAiCommand(): Command {
const command = new Command('ai')
command.description('Trigger PleaseAI workflows')
command.addCommand(createTriageCommand())
command.addCommand(createInvestigateCommand())
command.addCommand(createFixCommand())
command.addCommand(createReviewCommand())
command.addCommand(createApplyCommand())
return command
}Tests: test/commands/ai/*.test.ts
Goal: Implement sub-issue creation and management.
src/commands/issue/sub-issue.ts
import { Command } from 'commander'
import { getRepoInfo } from '../../lib/github-api'
import { addSubIssue, createSubIssue, getIssueNodeId, listSubIssues } from '../../lib/github-graphql'
export function createSubIssueCommand(): Command {
const command = new Command('sub-issue')
command.description('Manage sub-issues')
// Create subcommand
const createCmd = new Command('create')
.description('Create a new sub-issue')
.argument('<parent-issue>', 'Parent issue number')
.requiredOption('--title <text>', 'Sub-issue title')
.option('--body <text>', 'Sub-issue body')
.action(async (parentStr: string, options: { title: string, body?: string }) => {
try {
const parentNumber = Number.parseInt(parentStr, 10)
if (Number.isNaN(parentNumber)) {
throw new TypeError('Parent issue number must be valid')
}
const { owner, repo } = await getRepoInfo()
console.log(`🔍 Getting parent issue #${parentNumber}...`)
const parentNodeId = await getIssueNodeId(owner, repo, parentNumber)
console.log(`📝 Creating sub-issue...`)
const { number, nodeId } = await createSubIssue(parentNodeId, options.title, options.body)
console.log(`✅ Sub-issue #${number} created!`)
console.log(` View: https://github.com/${owner}/${repo}/issues/${number}`)
}
catch (error) {
console.error(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
process.exit(1)
}
})
// Add subcommand
const addCmd = new Command('add')
.description('Add existing issue as sub-issue')
.argument('<parent-issue>', 'Parent issue number')
.argument('<child-issue>', 'Child issue number to add')
.action(async (parentStr: string, childStr: string) => {
// Implementation similar to create
})
// List subcommand
const listCmd = new Command('list')
.description('List all sub-issues')
.argument('<parent-issue>', 'Parent issue number')
.action(async (parentStr: string) => {
// Fetch and display sub-issues
})
command.addCommand(createCmd)
command.addCommand(addCmd)
command.addCommand(listCmd)
return command
}src/commands/issue/index.ts
import { Command } from 'commander'
import { createDependencyCommand } from './dependency'
import { createSubIssueCommand } from './sub-issue'
export function createIssueCommand(): Command {
const command = new Command('issue')
command.description('Manage GitHub issues')
command.addCommand(createSubIssueCommand())
command.addCommand(createDependencyCommand())
return command
}Tests: test/commands/issue/sub-issue.test.ts
Goal: Implement issue dependency management (blocked_by).
src/commands/issue/dependency.ts
Similar structure to sub-issue.ts but using REST API functions:
addsubcommand: callsaddIssueDependency()removesubcommand: callsremoveIssueDependency()listsubcommand: callslistIssueDependencies()
Tests: test/commands/issue/dependency.test.ts
Goal: Move review-reply to pr group, add deprecation warning.
Move file: src/commands/review-reply.ts → src/commands/pr/review-reply.ts
src/commands/pr/index.ts (NEW)
import { Command } from 'commander'
import { createResolveCommand } from './resolve'
import { createReviewReplyCommand } from './review-reply'
export function createPrCommand(): Command {
const command = new Command('pr')
command.description('Manage pull requests')
command.addCommand(createReviewReplyCommand())
command.addCommand(createResolveCommand())
return command
}src/index.ts (MODIFY)
Add deprecated command:
// Deprecated: backward compatibility
const deprecatedReviewReply = new Command('review-reply')
.description('(Deprecated) Use \'gh please pr review-reply\' instead')
.argument('<comment-id>', 'ID of the review comment')
.option('-b, --body <text>', 'Reply body text')
.action(async (commentId: string, options: { body?: string }) => {
console.warn('⚠️ Warning: \'gh please review-reply\' is deprecated.')
console.warn(' Please use \'gh please pr review-reply\' instead.')
console.warn('')
// Import and execute the new command
const { createReviewReplyCommand } = await import('./commands/pr/review-reply')
const cmd = createReviewReplyCommand()
await cmd.parseAsync([commentId, ...(options.body ? ['-b', options.body] : [])], { from: 'user' })
})
program.addCommand(deprecatedReviewReply)Tests: Move test/lib/github-api.test.ts review-reply tests to test/commands/pr/review-reply.test.ts
Goal: Implement review thread resolution.
src/commands/pr/resolve.ts
import { Command } from 'commander'
import { getRepoInfo } from '../../lib/github-api'
import { getPrNodeId, listReviewThreads, resolveReviewThread } from '../../lib/github-graphql'
export function createResolveCommand(): Command {
const command = new Command('resolve')
command
.description('Resolve review threads on a pull request')
.argument('<pr-number>', 'Pull request number')
.option('--thread <id>', 'Specific thread ID to resolve')
.option('--all', 'Resolve all unresolved threads')
.action(async (prNumberStr: string, options: { thread?: string, all?: boolean }) => {
try {
const prNumber = Number.parseInt(prNumberStr, 10)
if (Number.isNaN(prNumber)) {
throw new TypeError('PR number must be valid')
}
if (!options.thread && !options.all) {
throw new Error('Must specify either --thread <id> or --all')
}
const { owner, repo } = await getRepoInfo()
const prNodeId = await getPrNodeId(owner, repo, prNumber)
if (options.all) {
console.log(`🔍 Fetching review threads for PR #${prNumber}...`)
const threads = await listReviewThreads(prNodeId)
const unresolved = threads.filter(t => !t.isResolved)
if (unresolved.length === 0) {
console.log(`✅ All threads are already resolved!`)
return
}
console.log(`📝 Resolving ${unresolved.length} thread(s)...`)
for (const thread of unresolved) {
await resolveReviewThread(thread.nodeId)
}
console.log(`✅ Resolved ${unresolved.length} thread(s)!`)
}
else if (options.thread) {
console.log(`📝 Resolving thread ${options.thread}...`)
await resolveReviewThread(options.thread)
console.log(`✅ Thread resolved!`)
}
console.log(` View: https://github.com/${owner}/${repo}/pull/${prNumber}`)
}
catch (error) {
console.error(`❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
process.exit(1)
}
})
return command
}Tests: test/commands/pr/resolve.test.ts
Goal: Wire all commands together in main entry point.
src/index.ts
#!/usr/bin/env bun
import { Command } from 'commander'
import { createAiCommand } from './commands/ai'
import { createInitCommand } from './commands/init'
import { createIssueCommand } from './commands/issue'
import { createPrCommand } from './commands/pr'
const program = new Command()
program
.name('gh-please')
.description('GitHub CLI extension for PleaseAI automation')
.version('0.2.0')
// Add command groups
program.addCommand(createAiCommand())
program.addCommand(createIssueCommand())
program.addCommand(createPrCommand())
program.addCommand(createInitCommand())
// Deprecated commands (backward compatibility)
const deprecatedReviewReply = new Command('review-reply')
.description('(Deprecated) Use \'gh please pr review-reply\' instead')
.argument('<comment-id>', 'ID of the review comment')
.option('-b, --body <text>', 'Reply body text')
.action(async (commentId: string, options: { body?: string }) => {
console.warn('⚠️ Warning: \'gh please review-reply\' is deprecated.')
console.warn(' Please use \'gh please pr review-reply\' instead.')
console.warn('')
const { createReviewReplyCommand } = await import('./commands/pr/review-reply')
const cmd = createReviewReplyCommand()
await cmd.parseAsync([commentId, ...(options.body ? ['-b', options.body] : [])], { from: 'user' })
})
program.addCommand(deprecatedReviewReply)
// Show help if no command
if (process.argv.length <= 2) {
program.help()
}
program.parse()Goal: Comprehensive testing and documentation updates.
- Unit tests: Individual functions in
lib/(mocked Bun.spawn) - Integration tests: Commands with mocked GitHub API responses
- Manual testing: Real GitHub repository test cases
Test Coverage Goals:
lib/functions: 90%+- Commands: 80%+
- Critical paths (GraphQL queries, error handling): 100%
CLAUDE.md - Add sections:
- New command structure overview
- Examples for each command group
- Migration guide from old
review-reply
README.md (create if needed):
- Installation instructions
- Quick start guide
- Command reference
- Examples
- ✅ Phase 1: Infrastructure (blocking for all others)
- Phase 2: AI triggers (high value, independent)
- Phase 5: PR review-reply refactor (quick win, maintains existing functionality)
- Phase 3: Sub-issue management
- Phase 4: Dependency management
- Phase 6: PR resolve
- Phase 7: Integration
- Phase 8: Testing & docs
Phase 1 (Infrastructure)
├─→ Phase 2 (AI triggers)
├─→ Phase 3 (Sub-issue)
├─→ Phase 4 (Dependency)
├─→ Phase 5 (Review-reply refactor)
│ └─→ Phase 6 (PR resolve)
└─→ Phase 7 (Integration)
└─→ Phase 8 (Testing & docs)
- Phase 1: 4-6 hours (GraphQL/REST clients, trigger helpers)
- Phase 2: 2-3 hours (5 similar commands)
- Phase 3: 2-3 hours (sub-issue commands)
- Phase 4: 2-3 hours (dependency commands)
- Phase 5: 1-2 hours (refactor + deprecation)
- Phase 6: 2-3 hours (resolve command + GraphQL)
- Phase 7: 1 hour (integration)
- Phase 8: 4-5 hours (tests + docs)
Total: ~18-26 hours
- All commands implemented and working
- Test coverage >80%
- Documentation complete
- No breaking changes to existing functionality
- Deprecation warnings working
- All GitHub issue sub-tasks completed