Skip to content
31 changes: 11 additions & 20 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ version: "3"

tasks:
build:
desc: Build the Bauer binary
desc: Build the Bauer and Bauer API binaries
cmds:
- go build -o bauer cmd/bauer/main.go

build-api:
desc: Build the API server binary
cmds:
- go build -o bauer-api cmd/app/main.go

test:
Expand All @@ -21,22 +17,10 @@ tasks:
cmds:
- gofmt -w .

run:
desc: Run the bau tool locally with specified document ID
vars:
CREDS: "bau-test-creds.json"
DOC_ID: "1WJ-N_Xkkx4r_6knxW7h200oIDyi4mVMzgh1xYt5xaU0"
OUTPUT_DIR: "bauer-output"
MODEL: "gpt-5-mini-high"
cmds:
- go run cmd/bauer/main.go --doc-id "{{.DOC_ID}}" --credentials "{{.CREDS}}" --output-dir "{{.OUTPUT_DIR}}" --model "{{.MODEL}}"
# requires:
# vars: [DOC_ID]

test-repo:
desc: Check that test repo is created on /tmp/test-bauer-repo and has correct default branch
run-server:
desc: Run the API server locally
cmds:
- cd /tmp/test-bauer-repo/ && git branch --show-current
- ./bauer-api --config config.json

clean:
desc: Clean up generated files
Expand All @@ -45,3 +29,10 @@ tasks:
- rm -f bauer
- rm -f bauer.log
- rm -rf /tmp/gh* /tmp/tmp* /tmp/test-bauer-repo* || true

run:
desc: Clean up generated files, build and run Bauer server
cmds:
- task: clean
- task: build
- task: run-server
25 changes: 2 additions & 23 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
"bauer/cmd/app/types"
v1 "bauer/cmd/app/v1"
"bauer/internal/orchestrator"
"bauer/internal/workflow"
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
)

func run() error {
Expand All @@ -27,28 +27,6 @@ func run() error {
return err
}

if cfg.TargetRepo != "" {
// Convert credentials path to absolute before changing directory
absCredsPath, err := filepath.Abs(cfg.CredentialsPath)
if err != nil {
return fmt.Errorf("failed to resolve credentials path: %w", err)
}
cfg.CredentialsPath = absCredsPath

// Convert output directory to absolute before changing directory
absOutputDir, err := filepath.Abs(cfg.BaseOutputDir)
if err != nil {
return fmt.Errorf("failed to resolve output directory path: %w", err)
}
cfg.BaseOutputDir = absOutputDir

if err := os.Chdir(cfg.TargetRepo); err != nil {
return fmt.Errorf("failed to change to target repository %q: %w", cfg.TargetRepo, err)
}
cwd, _ := os.Getwd()
slog.Info("Working directory", "path", cwd)
}

rc := types.RouteConfig{
APIConfig: *cfg,
Orchestrator: orchestrator,
Expand All @@ -57,6 +35,7 @@ func run() error {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/job", v1.JobPost(rc))
mux.HandleFunc("/api/v1/health", v1.GetHealth)
mux.HandleFunc("/api/v1/workflow", workflow.ExecuteWorkflowHandler(orchestrator))
slog.Info("starting server", "address", ":8090")
err = http.ListenAndServe(":8090", middleware.RequestTrace(mux))

Expand Down
246 changes: 42 additions & 204 deletions cmd/bauer/main.go
Original file line number Diff line number Diff line change
@@ -1,234 +1,72 @@
package main

import (
"bauer/internal/config"
"bauer/internal/github"
"bauer/internal/orchestrator"
"bauer/internal/workflow"
"context"
"flag"
"fmt"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
)

func runGithub() {
// 1. Parse a GitHub repo (works with all 3 formats)
// Using default repo for now
defaultRepo := "canonical/canonical.com"
repo, err := github.ParseGitHubRepo(defaultRepo)
if err != nil {
log.Fatal(err)
}
fmt.Printf("✅ Parsed: %s/%s\n", repo.Owner, repo.Name)

// 2. Check if gh CLI is installed
if !github.IsGhCLIInstalled() {
log.Fatal("❌ gh CLI not installed.")
}
fmt.Println("✅ gh CLI detected")

// 3. Validate GitHub authentication
if err := github.ValidateGitHubAuth(); err != nil {
log.Fatal("❌ Not authenticated. Run: gh auth login")
}
fmt.Println("✅ GitHub authenticated")

// 4. Clone/update repo (creates temp directory)
localPath := "/tmp/test-bauer-repo"
if err := github.CloneOrUpdateRepo(repo, localPath); err != nil {
log.Fatal(err)
}
fmt.Printf("✅ Repo ready at: %s\n", localPath)

// 5. Create feature branch
// Using default branch name for now
branchName := "bauer/test-phase1"
if err := github.CreateFeatureBranch(localPath, branchName); err != nil {
log.Fatal(err)
func main() {
// Parse CLI flags
githubRepo := flag.String("github-repo", "", "GitHub repository (owner/repo or HTTPS URL)")
docID := flag.String("doc-id", "", "Google Doc ID")
credentialsPath := flag.String("credentials", "bau-test-creds.json", "Path to service account credentials JSON")
localRepoPath := flag.String("local-repo-path", "/tmp/ubuntu.com", "Local path for cloned repository")
dryRun := flag.Bool("dry-run", false, "Perform a dry run without creating PR")
outputDir := flag.String("output-dir", "bauer-output", "Output directory for Bauer results")
branchPrefix := flag.String("branch-prefix", "bauer", "Branch naming prefix")

flag.Parse()

// Validate required flags
if *githubRepo == "" {
fmt.Fprintf(os.Stderr, "ERROR: --github-repo is required\n")
os.Exit(1)
}
fmt.Printf("✅ Branch created: %s\n", branchName)

// 6. Check current branch
current, err := github.GetCurrentBranch(localPath)
if err != nil {
log.Fatal(err)
if *docID == "" {
fmt.Fprintf(os.Stderr, "ERROR: --doc-id is required\n")
os.Exit(1)
}
Comment on lines +27 to 34
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI now requires --github-repo and --doc-id flags but doesn't provide backward compatibility with the previous interface that could run Bauer processing standalone. Users who were using the CLI without GitHub integration will experience a breaking change. Consider adding a flag or subcommand to support both standalone Bauer processing and the full workflow, or document this breaking change in release notes.

Copilot uses AI. Check for mistakes.
fmt.Printf("✅ Currently on: %s\n", current)
}

func runBauer() error {

fmt.Println(strings.Repeat("=", 80))
fmt.Println("Bauer - A tool to automate BAU tasks")
fmt.Println(strings.Repeat("=", 80))
fmt.Println()

// 1. Load Configuration
cfg, err := config.Load()
// Create workflow input from CLI flags/config
ghToken, err := github.GetGitHubToken()
if err != nil {
return fmt.Errorf("error loading configuration\n%w", err)
}

// 1b. Change to target repository if specified
if cfg.TargetRepo != "" {
// Convert credentials path to absolute before changing directory
absCredsPath, err := filepath.Abs(cfg.CredentialsPath)
if err != nil {
return fmt.Errorf("failed to resolve credentials path: %w", err)
}
cfg.CredentialsPath = absCredsPath

// Convert output directory to absolute before changing directory
absOutputDir, err := filepath.Abs(cfg.OutputDir)
if err != nil {
return fmt.Errorf("failed to resolve output directory path: %w", err)
}
cfg.OutputDir = absOutputDir

if err := os.Chdir(cfg.TargetRepo); err != nil {
return fmt.Errorf("failed to change to target repository %q: %w", cfg.TargetRepo, err)
}
cwd, _ := os.Getwd()
fmt.Printf("Working directory: %s\n\n", cwd)
fmt.Fprintf(os.Stderr, "WARNING: Could not get GitHub token: %v\n", err)
ghToken = ""
Comment on lines +44 to +45
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GitHub token retrieval error is only logged as a warning and the token is set to an empty string. However, the workflow will likely fail later when trying to authenticate with GitHub. Consider making this a fatal error or at least inform the user that they need to provide a token through environment variables or gh auth login.

Suggested change
fmt.Fprintf(os.Stderr, "WARNING: Could not get GitHub token: %v\n", err)
ghToken = ""
fmt.Fprintf(os.Stderr, "ERROR: Could not get GitHub token: %v\n", err)
fmt.Fprintln(os.Stderr, "Please provide a GitHub token via the appropriate environment variable or by running `gh auth login`.")
os.Exit(1)

Copilot uses AI. Check for mistakes.
}

// 2. Setup Logging
// For now, mirroring POC behavior: logging to log.json in the current directory
// TODO disable with a flag or env var
logFile, err := os.OpenFile("bauer-log.json", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open log file: %w", err)
workflowInput := workflow.WorkflowInput{
GitHubRepo: *githubRepo,
GitHubToken: ghToken,
BranchPrefix: *branchPrefix,
DocID: *docID,
Credentials: *credentialsPath,
LocalRepoPath: *localRepoPath,
DryRun: *dryRun,
OutputDir: *outputDir,
}
Comment on lines +48 to 57
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI doesn't expose flags for ChunkSize, PageRefresh, or Model configuration options that are available in the WorkflowInput. Users must rely on defaults (ChunkSize=0, PageRefresh=false, Model="") which will be set by config.Validate(). Consider adding CLI flags for these options to provide users with the same configurability that was available in the previous CLI version.

Copilot uses AI. Check for mistakes.
defer func() {
if err := logFile.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to close log file: %v\n", err)
}
}()

logger := slog.New(slog.NewJSONHandler(logFile, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
slog.SetDefault(logger)

slog.Info("Starting BAU CLI",
slog.String("doc_id", cfg.DocID),
slog.Bool("dry_run", cfg.DryRun),
)

ctx := context.Background()

// 3. Create and execute orchestrator
orch := orchestrator.NewOrchestrator()
result, err := orch.Execute(ctx, cfg)
if err != nil {
slog.Error("Orchestration failed", slog.String("error", err.Error()))
// Check if the error is credentials-related and provide more context
errMsg := err.Error()
if strings.Contains(errMsg, "credentials") || strings.Contains(errMsg, "private_key") || strings.Contains(errMsg, "client_email") {
fmt.Fprintf(os.Stderr, "\n⚠️ CREDENTIALS ERROR:\n")
fmt.Fprintf(os.Stderr, " %v\n\n", err)
fmt.Fprintf(os.Stderr, "Please verify:\n")
fmt.Fprintf(os.Stderr, " 1. Credentials file exists at: %s\n", cfg.CredentialsPath)
fmt.Fprintf(os.Stderr, " 2. Credentials file is valid JSON\n")
fmt.Fprintf(os.Stderr, " 3. Credentials file contains required fields:\n")
fmt.Fprintf(os.Stderr, " - type\n")
fmt.Fprintf(os.Stderr, " - project_id\n")
fmt.Fprintf(os.Stderr, " - private_key\n")
fmt.Fprintf(os.Stderr, " - client_email\n")
fmt.Fprintf(os.Stderr, " - token_uri\n\n")
}
return err
}

// 4. Print results
outputFile := "bauer-doc-suggestions.json"
fmt.Println("[1/4] Extracting from Google Doc...")
fmt.Printf(" ✓ Extraction completed in %s\n", result.ExtractionDuration.Round(time.Millisecond))
fmt.Println()

fmt.Println("[2/4] Generating technical plan...")
fmt.Printf(" ✓ Saved: %s\n", outputFile)
fmt.Printf(" ✓ Generated %d chunk file(s) in '%s/'\n", len(result.Chunks), cfg.OutputDir)
fmt.Printf(" ✓ Planning completed in %s\n", result.PlanDuration.Round(time.Millisecond))
for _, chunk := range result.Chunks {
slog.Info("Generated chunk",
slog.Int("chunk_number", chunk.ChunkNumber),
slog.String("filename", chunk.Filename),
slog.Int("location_count", chunk.LocationCount),
)
}
fmt.Println()

if result.DryRun {
fmt.Println("[3/4] Copilot execution (skipped - dry run)")
fmt.Println()
fmt.Println(strings.Repeat("=", 80))
fmt.Println("DRY RUN COMPLETE")
fmt.Println(strings.Repeat("=", 80))
fmt.Printf(" Summary:\n")
fmt.Printf(" • Extracted: %d suggestions\n", len(result.ExtractionResult.ActionableSuggestions))
fmt.Printf(" • Grouped into: %d location(s)\n", len(result.ExtractionResult.GroupedSuggestions))
fmt.Printf(" • Generated: %d chunk file(s) in '%s/'\n", len(result.Chunks), cfg.OutputDir)
fmt.Printf("\n Timing:\n")
fmt.Printf(" • Extraction: %s\n", result.ExtractionDuration.Round(time.Millisecond))
fmt.Printf(" • Planning: %s\n", result.PlanDuration.Round(time.Millisecond))
fmt.Printf(" • Total: %s\n", result.TotalDuration.Round(time.Millisecond))
fmt.Printf("\n Next steps:\n")
fmt.Printf(" 1. Review generated chunks in '%s/'\n", cfg.OutputDir)
fmt.Printf(" 2. Run without --dry-run to execute changes via Copilot\n")
fmt.Printf(" 3. Or manually pass chunk files to: gh copilot\n")
fmt.Println(strings.Repeat("=", 80))
return nil
}

// 5. Copilot execution results
fmt.Println("[3/4] Executing changes via Copilot...")
fmt.Println(strings.Repeat("=", 80))
fmt.Printf(" ✓ All %d chunk(s) processed successfully\n", len(result.CopilotOutputs))
fmt.Printf(" ✓ Total Copilot execution time: %s\n", result.CopilotDuration.Round(time.Millisecond))
fmt.Println()

// 6. Summary results if multiple chunks
if len(result.Chunks) > 1 {
fmt.Println("[4/5] Generating summary...")
fmt.Printf(" ✓ Summary completed in %s\n", result.SummaryDuration.Round(time.Millisecond))
fmt.Println()
}

// 7. Final summary
stepLabel := "[4/4]"
if len(result.Chunks) > 1 {
stepLabel = "[5/5]"
}

fmt.Printf("%s Complete!\n", stepLabel)
fmt.Println(strings.Repeat("=", 80))
fmt.Println("SUCCESS: Feedback applied!")
fmt.Println(strings.Repeat("=", 80))
fmt.Printf(" Summary:\n")
fmt.Printf(" • Extracted: %d suggestions\n", len(result.ExtractionResult.ActionableSuggestions))
fmt.Printf(" • Processed: %d chunk(s)\n", len(result.Chunks))
fmt.Printf("\n Timing:\n")
fmt.Printf(" • Extraction: %s\n", result.ExtractionDuration.Round(time.Millisecond))
fmt.Printf(" • Planning: %s\n", result.PlanDuration.Round(time.Millisecond))
fmt.Printf(" • Copilot execution: %s\n", result.CopilotDuration.Round(time.Millisecond))
fmt.Printf(" • Total: %s\n", result.TotalDuration.Round(time.Millisecond))
fmt.Printf("\n Next steps:\n")
fmt.Printf(" • Review the changes made by Copilot\n")
fmt.Printf(" • Create a PR with: gh pr create\n")
fmt.Println(strings.Repeat("=", 80))

return nil
}

func main() {
runGithub()

if err := runBauer(); err != nil {
// Execute the complete workflow
result, err := workflow.ExecuteWorkflow(context.Background(), workflowInput, orch)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
os.Exit(1)
}

// Print results
fmt.Printf("Status: %s\n", result.Status)
fmt.Printf("Branch: %s\n", result.RepositoryInfo.BranchName)
fmt.Printf("PR: %s\n", result.FinalizationInfo.PullRequest.URL)
}
11 changes: 11 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"doc_id": "1WJ-N_Xkkx4r_6knxW7h200oIDyi4mVMzgh1xYt5xaU0",
"credentials": "bau-test-creds.json",
"chunk_size": 1,
"page_refresh": false,
"output_dir": "bauer-output",
"model": "gpt-5-mini-high",
"summary_model": "gpt-5-mini-high",
"target_repo": "../canonical.com",
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config.json file contains a relative path "../canonical.com" for target_repo. This path is specific to a particular developer's directory structure and should not be committed to version control. Consider using an environment variable, a local config file that's gitignored, or documenting that this should be customized per developer.

Suggested change
"target_repo": "../canonical.com",
"target_repo": "./canonical.com",

Copilot uses AI. Check for mistakes.
"dry_run": false
}
Loading