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
70 changes: 70 additions & 0 deletions cmd/task/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2133,6 +2133,76 @@ Examples:
}
rootCmd.AddCommand(upgradeCmd)

// Doctor command - diagnose the agent server's GitHub auth health.
doctorCmd := &cobra.Command{
Use: "doctor",
Short: "Diagnose agent server health (GitHub auth & rate limits)",
Long: `Checks the local GitHub CLI authentication used by agents and warns about
conditions that cause shared GraphQL bucket exhaustion across agent servers:

- gh not installed or not logged in (GitHub operations silently fail)
- an expired/revoked token (401 Bad credentials)
- authentication as a PERSONAL account, whose 5,000 pt/hr GraphQL limit is
shared per-user across every server authed as that account
- low remaining GraphQL headroom

Each agent server should authenticate with its OWN GitHub App installation
token (a bot identity), which gets an independent GraphQL bucket.

Exits non-zero on hard errors (gh missing, logged out, expired token). Pass
--strict to also exit non-zero on warnings (e.g. personal-account auth), so a
fleet sweep like 'for s in ...; do ssh $s ty doctor --strict; done' can flag
servers programmatically.`,
Run: func(cmd *cobra.Command, args []string) {
strict, _ := cmd.Flags().GetBool("strict")
fmt.Println(boldStyle.Render("TaskYou Doctor"))
fmt.Println(dimStyle.Render("Checking GitHub authentication..."))
fmt.Println()

status := github.CheckAuth(context.Background())
if status.Err != nil {
fmt.Println(warnStyle.Render("⚠ Could not fully probe gh: " + status.Err.Error()))
fmt.Println()
}

findings := status.Findings()
hasError := false
for _, f := range findings {
var icon, msg string
switch f.Severity {
case github.SeverityOK:
icon = successStyle.Render("✓")
msg = f.Message
case github.SeverityWarn:
icon = warnStyle.Render("⚠")
msg = warnStyle.Render(f.Message)
case github.SeverityError:
icon = errorStyle.Render("✗")
msg = errorStyle.Render(f.Message)
hasError = true
}
fmt.Printf("%s %s\n", icon, msg)
if f.Detail != "" {
fmt.Println(dimStyle.Render(" " + f.Detail))
}
}

fmt.Println()
if hasError || status.HasProblems() {
fmt.Println(dimStyle.Render("Tip: provision this server with its own GitHub App installation token,"))
fmt.Println(dimStyle.Render("mirroring the offerlab-devs[bot] pattern, for an independent rate-limit bucket."))
} else {
fmt.Println(successStyle.Render("All checks passed."))
}

if hasError || (strict && status.HasProblems()) {
os.Exit(1)
}
},
}
doctorCmd.Flags().Bool("strict", false, "Exit non-zero on warnings too (e.g. personal-account auth), for fleet health sweeps")
rootCmd.AddCommand(doctorCmd)

// Settings command
settingsCmd := &cobra.Command{
Use: "settings",
Expand Down
14 changes: 14 additions & 0 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,20 @@ Work on this task until completion. When you're done or need input:
- If you see a path like .task-worktrees/, you're in the right place
- The parent repo does NOT exist for you - only this worktree does

🐙 GITHUB CLI (gh) — CONSERVE THE SHARED GRAPHQL BUCKET:
GitHub's GraphQL rate limit (5,000 points/hr) is PER-USER and is shared by
every agent server authenticated as the same account. It exhausts easily.

- Prefer REST for PR reads — it has a SEPARATE 5,000/hr bucket:
gh pr view --json ... ❌ (GraphQL-backed)
gh api repos/{owner}/{repo}/pulls/{n} ✅ (REST)
- NEVER busy-poll CI with "gh pr checks" in a loop. Instead use:
gh run watch <run-id> ✅ (blocks server-side, no polling)
or poll REST check-runs with backoff:
gh api repos/{owner}/{repo}/commits/{sha}/check-runs
- If you see "GraphQL bucket is exhausted", switch to the REST equivalents
above and back off — do not retry the GraphQL call in a tight loop.

The task system will automatically detect your status.
═══════════════════════════════════════════════════════════════`
}
Expand Down
18 changes: 18 additions & 0 deletions internal/executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1933,3 +1933,21 @@ func TestCleanupWorktreeNonWorktreeTask(t *testing.T) {
t.Error("expected settings.local.json to be removed")
}
}

func TestBuildSystemInstructions_GitHubGuidance(t *testing.T) {
instructions := (&Executor{}).buildSystemInstructions()

// The GitHub guidance must steer agents away from the shared GraphQL bucket.
wants := []string{
"GITHUB CLI",
"PER-USER",
"gh pr checks",
"gh run watch",
"REST",
}
for _, want := range wants {
if !strings.Contains(instructions, want) {
t.Errorf("buildSystemInstructions() missing %q", want)
}
}
}
Loading
Loading