Skip to content

GitHub API Implementation (REST + GraphQL) #93

@jmgilman

Description

@jmgilman

Work Unit 004: GitHub API Implementation (REST + GraphQL)

Behavioral Goal

As a developer using sow in Claude Code web VMs, I need GitHub operations (issues, PRs, branch linking) to work via the GitHub API instead of requiring the gh CLI, so that I can perform all repository management tasks in ephemeral browser-based environments where the gh CLI is not available.

Success Criteria:

  • GitHubAPI struct implements the GitHubClient interface completely
  • All GitHub operations work via REST API or GraphQL without gh CLI
  • Token authentication via GITHUB_TOKEN environment variable
  • Owner/repo information extracted from git remote URL
  • Factory returns GitHubAPI when GITHUB_TOKEN is set
  • All interface methods return identical data structures to GitHubCLI
  • Rate limit errors are handled gracefully with clear user guidance
  • Operations behave identically from consumer perspective (CLI vs API is transparent)
  • Error messages are clear and actionable

Existing Code Context

This work unit builds upon the GitHubClient interface extracted in work unit 002. The interface abstraction allows us to provide an API-based implementation that functions identically to the existing gh CLI-based implementation from the consumer's perspective.

Foundation from Work Unit 002

Work unit 002 created the following foundation that this work unit depends on:

GitHubClient Interface (cli/internal/sow/github_client.go):

type GitHubClient interface {
    CheckAvailability() error
    ListIssues(label, state string) ([]Issue, error)
    GetIssue(number int) (*Issue, error)
    CreateIssue(title, body string, labels []string) (*Issue, error)
    GetLinkedBranches(number int) ([]LinkedBranch, error)
    CreateLinkedBranch(issueNumber int, branchName string, checkout bool) (string, error)
    CreatePullRequest(title, body string, draft bool) (number int, url string, error)
    UpdatePullRequest(number int, title, body string) error
    MarkPullRequestReady(number int) error
}

Data Structures (defined in github_client.go or github_cli.go):

  • Issue - GitHub issue with number, title, body, state, URL, labels
  • LinkedBranch - Branch name and URL for issue-linked branches

Factory Pattern (cli/internal/sow/github_factory.go):
Currently returns an error when GITHUB_TOKEN is present, with a placeholder for this work unit:

func NewGitHubClient() (GitHubClient, error) {
    if token := os.Getenv("GITHUB_TOKEN"); token != "" {
        // Work unit 004: Return GitHubAPI client
        return nil, errors.New("API client not yet implemented")
    }
    return NewGitHubCLI(exec.NewLocal("gh")), nil
}

GitHubCLI Implementation Patterns to Follow

The existing GitHubCLI implementation at cli/internal/sow/github_cli.go (renamed from github.go) demonstrates the patterns that GitHubAPI should match:

Error Handling Pattern:

  • Custom error types with descriptive messages
  • Unwrap() support for error chains
  • Examples from GitHubCLI:
    • ErrGHNotInstalled - Clear message with installation URL
    • ErrGHNotAuthenticated - Actionable guidance ("Run: gh auth login")
    • ErrGHCommand - Includes command that failed and stderr output

Data Parsing Pattern:

  • JSON unmarshaling for structured data (issues, PRs)
  • String parsing for simple outputs (branch names from URLs)
  • Graceful handling of empty results (e.g., no linked branches returns empty slice, not error)

Method Structure Pattern:
All methods in GitHubCLI follow this structure:

  1. Check availability via Ensure() (calls CheckInstalled and CheckAuthenticated)
  2. Execute operation via executor interface
  3. Parse response
  4. Return typed data or error

Example from GitHubCLI:

func (g *GitHubCLI) GetIssue(number int) (*Issue, error) {
    if err := g.Ensure(); err != nil {
        return nil, err
    }

    stdout, stderr, err := g.gh.Run("issue", "view", fmt.Sprintf("%d", number), ...)
    if err != nil {
        return nil, ErrGHCommand{...}
    }

    var issue Issue
    if err := json.Unmarshal([]byte(stdout), &issue); err != nil {
        return nil, fmt.Errorf("failed to parse issue: %w", err)
    }

    return &issue, nil
}

Key Files Created by Work Unit 002

  • cli/internal/sow/github_client.go - Interface definition and data types
  • cli/internal/sow/github_cli.go - CLI implementation (existing code, renamed)
  • cli/internal/sow/github_factory.go - Factory with GITHUB_TOKEN detection
  • cli/internal/sow/github_mock.go - Mock implementation for testing

Key Files This Work Unit Will Create

  • cli/internal/sow/github_api.go - GitHubAPI implementation (new)
  • cli/internal/sow/github_api_test.go - API tests with mock HTTP (new)

Key Files This Work Unit Will Modify

  • cli/internal/sow/github_factory.go - Update to return GitHubAPI when GITHUB_TOKEN present
  • cli/go.mod - Add go-github, githubv4, and ensure oauth2 dependencies

Design Context

The design document at .sow/knowledge/designs/claude-code-web-integration.md (Section 4: GitHub Integration) specifies the dual GitHub client architecture. This work unit implements the API client half of that design.

API Mapping from Design Document

From design document lines 248-258, the API mapping table shows how each gh CLI command translates to API calls:

Operation gh CLI Command GitHub API Endpoint
List issues gh issue list REST /repos/{owner}/{repo}/issues
Get issue gh issue view N REST /repos/{owner}/{repo}/issues/{number}
Create issue gh issue create REST POST /repos/{owner}/{repo}/issues
Create PR gh pr create REST POST /repos/{owner}/{repo}/pulls
Create draft PR gh pr create --draft REST POST /repos/{owner}/{repo}/pulls with draft: true
Update PR gh pr edit N REST PATCH /repos/{owner}/{repo}/pulls/{number}
Mark PR ready gh pr ready N REST PATCH /repos/{owner}/{repo}/pulls/{number} with draft: false
Link branch gh issue develop GraphQL createLinkedBranch mutation
Get linked branches gh issue develop --list GraphQL query on issue's linkedBranches connection

Key Insight: Most operations use REST API (go-github library), but branch linking requires GraphQL (githubv4 library) because the linked branches feature is only available via GitHub's GraphQL API.

Authentication from Design Document

From design document lines 224-228:

  • Token-based authentication via GITHUB_TOKEN environment variable
  • Use golang.org/x/oauth2 for token handling (already in dependencies)
  • HTTP client configured with oauth2.StaticTokenSource
  • Extract owner/repo from git remote URL (no manual configuration)

Rate Limiting from Discovery Document

From discovery document lines 698-703:

  • Authenticated tokens: 5000 requests/hour
  • Unauthenticated: 60 requests/hour
  • User-provided GITHUB_TOKEN ensures high rate limit
  • Rate limit errors should suggest checking token validity
  • Future enhancement: response caching (not in this work unit)

Implementation Approach

The API implementation follows the same interface contract as GitHubCLI but uses HTTP requests instead of executing shell commands.

Architecture Overview

GitHubAPI struct
├── token: string (from GITHUB_TOKEN)
├── owner: string (from git remote)
├── repo: string (from git remote)
├── restClient: *github.Client (go-github)
└── graphqlClient: *githubv4.Client (githubv4)

Both clients are initialized with the same oauth2 HTTP client to share authentication.

Step 1: Dependencies

Add to cli/go.mod:

require (
    github.com/google/go-github/v66 v66.0.0
    github.com/shurcooL/githubv4 v0.0.0-20231126234147-1cffa1f02456
    golang.org/x/oauth2 v0.XX.0 // likely already present as indirect
)

Library choices:

  • go-github/v66 - Official Google-maintained REST client, comprehensive, well-tested
  • githubv4 - Most popular GraphQL client for GitHub, simple API
  • oauth2 - Standard library for OAuth2, handles token refresh

Step 2: Core Structure

Create cli/internal/sow/github_api.go:

package sow

import (
    "context"
    "net/http"

    "github.com/google/go-github/v66/github"
    "github.com/shurcooL/githubv4"
    "golang.org/x/oauth2"
)

// GitHubAPI implements GitHubClient using GitHub REST and GraphQL APIs.
//
// This implementation is used in Claude Code web VMs where the gh CLI
// is not available. It requires a GITHUB_TOKEN environment variable.
type GitHubAPI struct {
    token         string
    owner         string
    repo          string
    restClient    *github.Client
    graphqlClient *githubv4.Client
}

// NewGitHubAPI creates a GitHub client backed by REST and GraphQL APIs.
//
// Parameters:
//   - token: GitHub personal access token (from GITHUB_TOKEN env var)
//   - owner: Repository owner (extracted from git remote)
//   - repo: Repository name (extracted from git remote)
//
// The token must have repo scope for full functionality.
func NewGitHubAPI(token, owner, repo string) *GitHubAPI {
    // Create oauth2 HTTP client
    ctx := context.Background()
    ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
    httpClient := oauth2.NewClient(ctx, ts)

    return &GitHubAPI{
        token:         token,
        owner:         owner,
        repo:          repo,
        restClient:    github.NewClient(httpClient),
        graphqlClient: githubv4.NewClient(httpClient),
    }
}

// Interface compliance check
var _ GitHubClient = (*GitHubAPI)(nil)

Step 3: Error Types

Add API-specific error types to match GitHubCLI patterns:

// ErrGitHubAPI is returned when a GitHub API call fails.
type ErrGitHubAPI struct {
    Operation string
    Message   string
    Err       error
}

func (e ErrGitHubAPI) Error() string {
    if e.Message != "" {
        return fmt.Sprintf("GitHub API %s failed: %s", e.Operation, e.Message)
    }
    return fmt.Sprintf("GitHub API %s failed: %v", e.Operation, e.Err)
}

func (e ErrGitHubAPI) Unwrap() error {
    return e.Err
}

// ErrNoGitHubToken is returned when GITHUB_TOKEN is not set.
type ErrNoGitHubToken struct{}

func (e ErrNoGitHubToken) Error() string {
    return "GITHUB_TOKEN environment variable not set. Create a token at: https://github.com/settings/tokens"
}

// ErrInvalidGitHubToken is returned when the token is invalid or lacks permissions.
type ErrInvalidGitHubToken struct {
    Reason string
}

func (e ErrInvalidGitHubToken) Error() string {
    return fmt.Sprintf("GitHub token invalid or lacks permissions: %s", e.Reason)
}

Step 4: Implement Interface Methods

CheckAvailability:
Validates token and connectivity by making a simple API call:

func (g *GitHubAPI) CheckAvailability() error {
    ctx := context.Background()

    // Verify token by fetching repository info
    _, resp, err := g.restClient.Repositories.Get(ctx, g.owner, g.repo)
    if err != nil {
        // Check for authentication errors
        if resp != nil && resp.StatusCode == 401 {
            return ErrInvalidGitHubToken{"Token authentication failed"}
        }
        return ErrGitHubAPI{
            Operation: "check availability",
            Err:       err,
        }
    }

    return nil
}

REST API Methods (ListIssues, GetIssue, CreateIssue, CreatePullRequest, UpdatePullRequest, MarkPullRequestReady):

  • Use g.restClient.Issues.* and g.restClient.PullRequests.* methods
  • Convert between go-github types and sow Issue/PR types
  • Handle pagination for ListIssues (limit to 1000 like gh CLI)
  • Parse response data into expected structures

GraphQL Methods (GetLinkedBranches, CreateLinkedBranch):

  • Use g.graphqlClient.Query() and g.graphqlClient.Mutate()
  • Define GraphQL query/mutation structs
  • Handle the linkedBranches connection
  • Parse GraphQL responses into LinkedBranch structs

Step 5: Update Factory

Modify cli/internal/sow/github_factory.go:

func NewGitHubClient() (GitHubClient, error) {
    if token := os.Getenv("GITHUB_TOKEN"); token != "" {
        // Extract owner/repo from git remote
        owner, repo, err := getRepoInfo()
        if err != nil {
            return nil, fmt.Errorf("failed to extract repo info for API client: %w", err)
        }

        return NewGitHubAPI(token, owner, repo), nil
    }

    // Default to CLI client
    return NewGitHubCLI(exec.NewLocal("gh")), nil
}

// getRepoInfo extracts owner and repo from git remote URL.
func getRepoInfo() (owner, repo string, err error) {
    // Execute: git remote get-url origin
    // Parse URL (supports https and ssh formats):
    //   https://github.com/owner/repo.git
    //   [email protected]:owner/repo.git
    // Return owner, repo (without .git suffix)
}

Step 6: Testing Strategy

Unit Tests with Mock HTTP:

  • Use httptest.NewServer() to mock GitHub API responses
  • Test each interface method independently
  • Verify correct API endpoints are called
  • Test error handling (401, 404, rate limits, etc.)

Example test structure:

func TestGitHubAPI_GetIssue(t *testing.T) {
    // Create mock HTTP server
    mux := http.NewServeMux()
    mux.HandleFunc("/repos/owner/repo/issues/123", func(w http.ResponseWriter, r *http.Request) {
        // Verify Authorization header
        // Return mock issue JSON
        fmt.Fprint(w, `{"number": 123, "title": "Test Issue", ...}`)
    })
    server := httptest.NewServer(mux)
    defer server.Close()

    // Create client pointing to mock server
    client := newTestGitHubAPI(server.URL, "owner", "repo")

    // Execute and assert
    issue, err := client.GetIssue(123)
    // Assertions...
}

GraphQL Testing:
Similar approach but mock GraphQL endpoint with query/mutation validation.

Integration with Factory:
Test that factory returns GitHubAPI when GITHUB_TOKEN is set:

func TestNewGitHubClient_WithToken_ReturnsAPI(t *testing.T) {
    os.Setenv("GITHUB_TOKEN", "test-token")
    defer os.Unsetenv("GITHUB_TOKEN")

    // Mock git remote get-url

    client, err := NewGitHubClient()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if _, ok := client.(*GitHubAPI); !ok {
        t.Errorf("expected *GitHubAPI, got %T", client)
    }
}

Dependencies

Depends On

Work Unit 002 (GitHub Client Interface Extraction) - REQUIRED

  • Provides the GitHubClient interface that this work unit implements
  • Provides Issue and LinkedBranch data structures
  • Provides factory pattern (NewGitHubClient) to update
  • Provides error handling patterns to match

Why this dependency exists: The API client must implement the same interface as the CLI client to ensure they're interchangeable. Without the interface definition, we risk creating incompatible implementations.

Depended On By

Work Unit 005 (In-Place Project Initialization) and other work units may indirectly benefit from this work, but there's no strict dependency. The API client enables sow to work in web VMs, which is foundational to the entire Claude Code web integration.

Acceptance Criteria

Objective, measurable criteria that reviewers will verify:

  1. GitHubAPI struct exists: Defined in cli/internal/sow/github_api.go with token, owner, repo, restClient, graphqlClient fields
  2. Interface compliance: var _ GitHubClient = (*GitHubAPI)(nil) compiles without error
  3. All methods implemented: Every GitHubClient interface method has a working implementation
  4. REST operations work: ListIssues, GetIssue, CreateIssue, CreatePullRequest, UpdatePullRequest, MarkPullRequestReady use go-github
  5. GraphQL operations work: GetLinkedBranches, CreateLinkedBranch use githubv4 with proper queries/mutations
  6. Token authentication: GITHUB_TOKEN environment variable is used for auth via oauth2
  7. Repository extraction: Owner/repo extracted from git remote URL (supports both https and ssh formats)
  8. Factory integration: NewGitHubClient() returns GitHubAPI when GITHUB_TOKEN is set, GitHubCLI otherwise
  9. Error handling: Rate limits, auth failures, network errors return clear, actionable error messages
  10. Data structure compatibility: All methods return identical Issue/LinkedBranch structures to GitHubCLI
  11. Dependencies added: go.mod includes go-github/v66 and githubv4
  12. Comprehensive tests: Unit tests with mock HTTP cover all interface methods and error cases
  13. CheckAvailability validates token: Makes an API call to verify token is valid and has repo access
  14. Documentation: All exported types and methods have clear godoc comments with usage examples

Testing Requirements

Unit Tests (github_api_test.go)

Interface Compliance:

func TestGitHubAPI_ImplementsInterface(t *testing.T) {
    var _ GitHubClient = (*GitHubAPI)(nil)
}

REST API Tests (with httptest.Server):

  • TestGitHubAPI_CheckAvailability
  • TestGitHubAPI_ListIssues
  • TestGitHubAPI_GetIssue
  • TestGitHubAPI_CreateIssue
  • TestGitHubAPI_CreatePullRequest_Draft
  • TestGitHubAPI_CreatePullRequest_Ready
  • TestGitHubAPI_UpdatePullRequest
  • TestGitHubAPI_MarkPullRequestReady

GraphQL Tests (with mock GraphQL endpoint):

  • TestGitHubAPI_GetLinkedBranches
  • TestGitHubAPI_GetLinkedBranches_Empty (no branches linked)
  • TestGitHubAPI_CreateLinkedBranch

Error Handling Tests:

  • TestGitHubAPI_CheckAvailability_InvalidToken (401 response)
  • TestGitHubAPI_GetIssue_NotFound (404 response)
  • TestGitHubAPI_RateLimitError (403 with rate limit headers)
  • TestGitHubAPI_NetworkError

Repository Extraction Tests:

Factory Tests (github_factory_test.go)

Update existing factory tests to verify GitHubAPI is returned:

  • TestNewGitHubClient_WithToken_ReturnsAPI
  • TestNewGitHubClient_WithToken_InvalidRepo (can't extract owner/repo)
  • TestNewGitHubClient_WithoutToken_ReturnsCLI (existing test)

Coverage Target

  • All interface methods: 100%
  • Error paths: 100%
  • Repository extraction: 100%
  • Overall: >90% for github_api.go

Implementation Notes

go-github Usage Patterns

Creating Issues:

issueRequest := &github.IssueRequest{
    Title:  github.String(title),
    Body:   github.String(body),
    Labels: &labels,
}
issue, resp, err := g.restClient.Issues.Create(ctx, g.owner, g.repo, issueRequest)

Listing Issues with Filters:

opts := &github.IssueListByRepoOptions{
    State:  state,
    Labels: []string{label},
    ListOptions: github.ListOptions{PerPage: 100}, // Pagination
}
issues, resp, err := g.restClient.Issues.ListByRepo(ctx, g.owner, g.repo, opts)

Creating Pull Requests:

pr := &github.NewPullRequest{
    Title: github.String(title),
    Body:  github.String(body),
    Draft: github.Bool(draft),
    Head:  github.String(headBranch),
    Base:  github.String(baseBranch),
}
pr, resp, err := g.restClient.PullRequests.Create(ctx, g.owner, g.repo, pr)

Updating Pull Requests:

update := &github.PullRequest{
    Title: github.String(title),
    Body:  github.String(body),
}
pr, resp, err := g.restClient.PullRequests.Edit(ctx, g.owner, g.repo, number, update)

Marking PR Ready:

update := &github.PullRequest{
    Draft: github.Bool(false),
}
pr, resp, err := g.restClient.PullRequests.Edit(ctx, g.owner, g.repo, number, update)

githubv4 Usage for Branch Linking

Query for Linked Branches:

var query struct {
    Repository struct {
        Issue struct {
            LinkedBranches struct {
                Nodes []struct {
                    Ref struct {
                        Name string
                    }
                }
            } `graphql:"linkedBranches(first: 100)"`
        } `graphql:"issue(number: $issueNumber)"`
    } `graphql:"repository(owner: $owner, name: $repo)"`
}

variables := map[string]interface{}{
    "owner":       githubv4.String(g.owner),
    "repo":        githubv4.String(g.repo),
    "issueNumber": githubv4.Int(issueNumber),
}

err := g.graphqlClient.Query(context.Background(), &query, variables)

Mutation for Creating Linked Branch:

var mutation struct {
    CreateLinkedBranch struct {
        LinkedBranch struct {
            Ref struct {
                Name string
            }
        }
    } `graphql:"createLinkedBranch(input: $input)"`
}

input := githubv4.CreateLinkedBranchInput{
    RepositoryID: repoID,
    IssueID:      issueID,
    Name:         githubv4.String(branchName),
}

err := g.graphqlClient.Mutate(context.Background(), &mutation, input, nil)

Note: GraphQL requires node IDs (not just numbers), so you may need to query for repository and issue IDs first.

Rate Limit Handling

Check rate limit from response headers:

if resp.StatusCode == 403 {
    if resp.Rate.Remaining == 0 {
        return ErrGitHubAPI{
            Operation: operation,
            Message:   fmt.Sprintf("Rate limit exceeded. Resets at %v", resp.Rate.Reset),
        }
    }
}

Suggest checking token if rate limit hit:

  • User tokens: 5000 req/hour
  • If hitting rate limits, may indicate token is invalid or missing scopes

Repository Extraction from Git Remote

func getRepoInfo() (owner, repo string, err error) {
    // Execute: git remote get-url origin
    cmd := exec.Command("git", "remote", "get-url", "origin")
    output, err := cmd.Output()
    if err != nil {
        return "", "", fmt.Errorf("failed to get git remote: %w", err)
    }

    url := strings.TrimSpace(string(output))

    // Parse https://github.com/owner/repo.git
    if strings.HasPrefix(url, "https://") {
        parts := strings.Split(url, "/")
        if len(parts) < 2 {
            return "", "", fmt.Errorf("invalid remote URL: %s", url)
        }
        owner = parts[len(parts)-2]
        repo = strings.TrimSuffix(parts[len(parts)-1], ".git")
        return owner, repo, nil
    }

    // Parse [email protected]:owner/repo.git
    if strings.HasPrefix(url, "git@") {
        parts := strings.SplitN(url, ":", 2)
        if len(parts) != 2 {
            return "", "", fmt.Errorf("invalid SSH remote URL: %s", url)
        }
        pathParts := strings.Split(parts[1], "/")
        if len(pathParts) != 2 {
            return "", "", fmt.Errorf("invalid SSH remote path: %s", parts[1])
        }
        owner = pathParts[0]
        repo = strings.TrimSuffix(pathParts[1], ".git")
        return owner, repo, nil
    }

    return "", "", fmt.Errorf("unsupported remote URL format: %s", url)
}

Context Handling

All API calls should use context.Background() for simplicity. Future enhancement could add timeout/cancellation support via context.

Pagination Considerations

For ListIssues, handle pagination to match gh CLI behavior (limit 1000):

var allIssues []*github.Issue
opts := &github.IssueListByRepoOptions{
    State:  state,
    Labels: []string{label},
    ListOptions: github.ListOptions{PerPage: 100},
}

for {
    issues, resp, err := g.restClient.Issues.ListByRepo(ctx, g.owner, g.repo, opts)
    if err != nil {
        return nil, err
    }

    allIssues = append(allIssues, issues...)

    if resp.NextPage == 0 || len(allIssues) >= 1000 {
        break
    }

    opts.Page = resp.NextPage
}

Data Type Conversion

Convert between go-github types and sow types:

func toSowIssue(ghIssue *github.Issue) *Issue {
    issue := &Issue{
        Number: ghIssue.GetNumber(),
        Title:  ghIssue.GetTitle(),
        Body:   ghIssue.GetBody(),
        State:  ghIssue.GetState(),
        URL:    ghIssue.GetHTMLURL(),
        Labels: make([]struct{Name string}, len(ghIssue.Labels)),
    }

    for i, label := range ghIssue.Labels {
        issue.Labels[i].Name = label.GetName()
    }

    return issue
}

Security Considerations

  • Never log the token value
  • Use oauth2 library for token management (handles header formatting)
  • Validate token has required scopes (repo scope for full functionality)
  • Token should be read-only from environment (not stored in struct permanently)

Out of Scope

Not included in this work unit:

  • Response caching (future enhancement for rate limit optimization)
  • Token refresh or OAuth flow (user provides static token via env var)
  • SSH key authentication (only token-based auth)
  • GitHub Enterprise support (only github.com)
  • Webhook operations or other advanced GitHub features
  • Retry logic for transient failures (future enhancement)
  • Background operations or job queuing
  • Token permission validation beyond CheckAvailability

Summary

This work unit implements the API-based GitHub client that enables sow to function in Claude Code web VMs. By implementing the GitHubClient interface established in work unit 002, the API client is a drop-in replacement for the CLI client with automatic environment detection via the factory pattern.

Key Deliverables:

  1. GitHubAPI implementation using go-github (REST) and githubv4 (GraphQL)
  2. Token-based authentication via GITHUB_TOKEN
  3. Automatic owner/repo extraction from git remote
  4. Factory integration with environment auto-detection
  5. Error handling matching GitHubCLI patterns
  6. Comprehensive test coverage with mock HTTP

Implementation Strategy:

  • Follow GitHubCLI patterns for consistency
  • Use established go-github and githubv4 libraries
  • Test with mock HTTP servers (no real API calls in tests)
  • Focus on behavioral compatibility with CLI client
  • Provide clear, actionable error messages

This implementation completes the dual GitHub client architecture, allowing sow to work seamlessly in both local development (with gh CLI) and web VMs (with GitHub API).

Metadata

Metadata

Assignees

No one assigned

    Labels

    sowIssues managed by sow breakdown workflow

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions