Skip to content

Operational Tooling Specification

Authors
Matt Cockayne, Claude (claude-opus-4-6) (AI drafting assistant)
Date
21 March 2026
Status
DRAFT

Overview

Two related CLI features improve the operational experience for both human users and CI/CD pipelines:

  1. Structured JSON output (--output json): Machine-readable output for CI integration, scripting, and programmatic consumption. Currently all output is human-formatted text with no structured alternative.

  2. Doctor/diagnose command (doctor): A single command that validates configuration, tests VCS connectivity, verifies API keys, and reports runtime environment details. Reduces support burden by giving users a self-service diagnostic tool.

Note: Shell completions are not included in this spec โ€” Cobra provides these out of the box via its built-in completion subcommand, which is already available to all GTB-based tools.


Design Decisions

Global --output flag: The output format flag is added to the root command as a persistent flag, making it available to all subcommands. Commands that support structured output check this flag and format accordingly. Commands that don't support it ignore it gracefully.

Doctor as checklist: The doctor command runs a series of checks and reports pass/fail/warn for each. This is modelled after brew doctor and flutter doctor. Each check is independent โ€” one failure doesn't prevent others from running.


Public API Changes

New: Output Format Types

// OutputFormat represents the output format for commands.
type OutputFormat string

const (
    OutputFormatText OutputFormat = "text"
    OutputFormatJSON OutputFormat = "json"
)

New: doctor Check Types

// CheckResult represents the outcome of a single doctor check.
type CheckResult struct {
    Name    string       `json:"name"`
    Status  CheckStatus  `json:"status"`
    Message string       `json:"message"`
    Details string       `json:"details,omitempty"`
}

// CheckStatus is the outcome of a check.
type CheckStatus string

const (
    CheckPass CheckStatus = "pass"
    CheckWarn CheckStatus = "warn"
    CheckFail CheckStatus = "fail"
    CheckSkip CheckStatus = "skip"
)

// DoctorReport contains all check results.
type DoctorReport struct {
    Tool    string        `json:"tool"`
    Version string        `json:"version"`
    Checks  []CheckResult `json:"checks"`
}

Internal Implementation

Structured JSON Output

Global Flag

func setupRootFlags(rootCmd *cobra.Command, props *p.Props, state *rootState) {
    rootCmd.PersistentFlags().StringVar(&state.outputFormat, "output", "text", "output format (text, json)")
}

Output Helper

// pkg/output/output.go

// Writer handles formatted output based on the configured format.
type Writer struct {
    format OutputFormat
    w      io.Writer
}

// NewWriter creates an output writer for the given format.
func NewWriter(w io.Writer, format OutputFormat) *Writer {
    return &Writer{format: format, w: w}
}

// Write outputs data in the configured format.
// For JSON format, data is marshalled to JSON.
// For text format, the textFunc is called to produce human-readable output.
func (o *Writer) Write(data any, textFunc func(io.Writer)) error {
    switch o.format {
    case OutputFormatJSON:
        enc := json.NewEncoder(o.w)
        enc.SetIndent("", "  ")
        return enc.Encode(data)
    default:
        textFunc(o.w)
        return nil
    }
}

Example Command Usage

func runVersionCmd(cmd *cobra.Command, props *p.Props) error {
    format, _ := cmd.Flags().GetString("output")
    out := output.NewWriter(os.Stdout, output.OutputFormat(format))

    data := struct {
        Tool    string `json:"tool"`
        Version string `json:"version"`
    }{
        Tool:    props.Tool.Name,
        Version: props.Version.String(),
    }

    return out.Write(data, func(w io.Writer) {
        fmt.Fprintf(w, "%s version %s\n", data.Tool, data.Version)
    })
}

Doctor Command

// pkg/cmd/doctor/doctor.go

func NewCmdDoctor(props *p.Props) *cobra.Command {
    cmd := &cobra.Command{
        Use:   "doctor",
        Short: "Check environment and configuration health",
        RunE: func(cmd *cobra.Command, args []string) error {
            format, _ := cmd.Flags().GetString("output")
            out := output.NewWriter(os.Stdout, output.OutputFormat(format))

            report := runChecks(cmd.Context(), props)
            return out.Write(report, func(w io.Writer) {
                printReport(w, report)
            })
        },
    }
    return cmd
}

func runChecks(ctx context.Context, props *p.Props) *DoctorReport {
    report := &DoctorReport{
        Tool:    props.Tool.Name,
        Version: props.Version.String(),
    }

    checks := []func(context.Context, *p.Props) CheckResult{
        checkGoVersion,
        checkConfig,
        checkGitConnectivity,
        checkAPIKeys,
        checkDiskSpace,
        checkPermissions,
    }

    for _, check := range checks {
        report.Checks = append(report.Checks, check(ctx, props))
    }

    return report
}

Individual Checks

func checkGoVersion(ctx context.Context, props *p.Props) CheckResult {
    version := runtime.Version()
    if strings.HasPrefix(version, "go1.21") || strings.HasPrefix(version, "go1.22") {
        return CheckResult{Name: "Go version", Status: CheckPass, Message: version}
    }
    return CheckResult{Name: "Go version", Status: CheckWarn, Message: version, Details: "Go 1.21+ recommended"}
}

func checkConfig(ctx context.Context, props *p.Props) CheckResult {
    if props.Config == nil {
        return CheckResult{Name: "Configuration", Status: CheckFail, Message: "no configuration loaded"}
    }
    return CheckResult{Name: "Configuration", Status: CheckPass, Message: "loaded successfully"}
}

func checkGitConnectivity(ctx context.Context, props *p.Props) CheckResult {
    // Check if git is available and the repo is accessible
    cmd := exec.CommandContext(ctx, "git", "status")
    if err := cmd.Run(); err != nil {
        return CheckResult{Name: "Git", Status: CheckWarn, Message: "git not available or not in a repository"}
    }
    return CheckResult{Name: "Git", Status: CheckPass, Message: "repository accessible"}
}

func checkAPIKeys(ctx context.Context, props *p.Props) CheckResult {
    keys := map[string]string{
        "anthropic": props.Config.GetString("anthropic.api_key"),
        "openai":    props.Config.GetString("openai.api_key"),
        "gemini":    props.Config.GetString("gemini.api_key"),
    }

    configured := 0
    for _, v := range keys {
        if v != "" {
            configured++
        }
    }

    if configured == 0 {
        return CheckResult{Name: "API keys", Status: CheckWarn, Message: "no AI provider API keys configured"}
    }
    return CheckResult{Name: "API keys", Status: CheckPass, Message: fmt.Sprintf("%d provider(s) configured", configured)}
}

Report Output

func printReport(w io.Writer, report *DoctorReport) {
    fmt.Fprintf(w, "%s %s\n\n", report.Tool, report.Version)

    for _, check := range report.Checks {
        var icon string
        switch check.Status {
        case CheckPass:
            icon = "[OK]"
        case CheckWarn:
            icon = "[!!]"
        case CheckFail:
            icon = "[FAIL]"
        case CheckSkip:
            icon = "[SKIP]"
        }
        fmt.Fprintf(w, "  %s %s: %s\n", icon, check.Name, check.Message)
        if check.Details != "" {
            fmt.Fprintf(w, "       %s\n", check.Details)
        }
    }
}

Project Structure

pkg/output/
โ”œโ”€โ”€ output.go          โ† NEW: Writer, OutputFormat
โ”œโ”€โ”€ output_test.go     โ† NEW: output formatting tests
pkg/cmd/doctor/
โ”œโ”€โ”€ doctor.go          โ† NEW: doctor command, checks
โ”œโ”€โ”€ checks.go          โ† NEW: individual check implementations
โ”œโ”€โ”€ doctor_test.go     โ† NEW: check tests
pkg/cmd/root/
โ”œโ”€โ”€ root.go            โ† MODIFIED: register doctor command, --output flag

Testing Strategy

JSON Output

Test Scenario
TestWriter_JSON Struct data โ†’ valid JSON output
TestWriter_Text Text function called, data ignored
TestWriter_JSONIndent Output is indented for readability

Doctor

Test Scenario
TestCheckGoVersion_Current Current Go version โ†’ pass
TestCheckConfig_Loaded Config present โ†’ pass
TestCheckConfig_Missing Nil config โ†’ fail
TestCheckAPIKeys_None No keys configured โ†’ warn
TestCheckAPIKeys_Some One key configured โ†’ pass with count
TestDoctorReport_JSONOutput Report as JSON โ†’ valid structure
TestDoctorReport_TextOutput Report as text โ†’ formatted with icons

Coverage

  • Target: 90%+ for pkg/output/, pkg/cmd/doctor/.

Linting

  • golangci-lint run --fix must pass.
  • No new nolint directives.

Documentation

  • Godoc for pkg/output/ types.
  • Godoc for pkg/cmd/doctor/ check types and report format.
  • User-facing documentation:
  • docs/commands/doctor.md โ€” doctor command usage and check descriptions
  • Update docs/commands/ index with new commands
  • Update root command help to list new subcommands.

Backwards Compatibility

  • No breaking changes. All features are additive.
  • --output text is the default, preserving existing behaviour.
  • The doctor command doesn't conflict with existing commands.

Future Considerations

  • Additional output formats: YAML, table, CSV for different CI/CD systems.
  • Custom doctor checks: Allow plugins to register their own health checks.
  • Doctor auto-fix: Some checks could offer automatic remediation (e.g., creating missing config files).

Implementation Phases

Phase 1 โ€” Output Package

  1. Create pkg/output/ with Writer and OutputFormat
  2. Add --output persistent flag to root command
  3. Add tests

Phase 2 โ€” Doctor Command

  1. Create pkg/cmd/doctor/ with check framework
  2. Implement individual checks
  3. Support both text and JSON output
  4. Register with root command

Phase 3 โ€” Migrate Existing Commands

  1. Identify commands that benefit from JSON output (version, config dump)
  2. Add output.Writer usage to those commands
  3. Add tests for JSON output paths

Verification

go build ./...
go test -race ./pkg/output/... ./pkg/cmd/doctor/...
go test ./...
golangci-lint run --fix

# Manual verification
go run . doctor                          # should show check results
go run . doctor --output json            # should show JSON report
go run . version --output json           # should show JSON version