Skip to content
Open
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
23 changes: 20 additions & 3 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"bauer/cmd/app/core/middleware"
"bauer/cmd/app/types"
v1 "bauer/cmd/app/v1"
"bauer/internal/copilotcli"
"bauer/internal/orchestrator"
"bauer/internal/source"
"bauer/internal/workflow"
"fmt"
"log/slog"
Expand All @@ -20,22 +22,36 @@ func run() error {
slog.Info("startup", "status", "initializing API")
defer slog.Info("shutdown complete")

orchestrator := orchestrator.NewOrchestrator()
cfg, err := types.LoadConfig()
if err != nil {
slog.Error("failed to load config", "error", err.Error())
return err
}

cwd, err := os.Getwd()
if err != nil {
slog.Error("failed to get working directory", "error", err.Error())
return err
}

copilotAgent, err := copilotcli.NewClient(cwd)
if err != nil {
slog.Error("failed to create Copilot client", "error", err.Error())
return err
}
Comment on lines +37 to +41

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed this is a concern for concurrent requests. It is addressed in later branches (phase-3/phase-4) where the API handler creates a fresh agent per workflow execution. The shared agent pattern here is interim.


sources := source.NewManager(cfg.CredentialsPath)
orch := orchestrator.New(copilotAgent, sources)

Comment on lines +43 to +45

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same — later branches (phase-3, T3.2) remove per-request credentials entirely from the API request body. The Manager is initialized at startup with the service account path, and that becomes the only source. This interim state goes away.

rc := types.RouteConfig{
APIConfig: *cfg,
Orchestrator: orchestrator,
Orchestrator: orch,
}

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))
mux.HandleFunc("/api/v1/workflow", workflow.ExecuteWorkflowHandler(orch))
slog.Info("starting server", "address", ":8090")
err = http.ListenAndServe(":8090", middleware.RequestTrace(mux))

Expand All @@ -53,3 +69,4 @@ func main() {
os.Exit(1)
}
}

80 changes: 79 additions & 1 deletion cmd/bauer/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package main

import (
"bauer/internal/config"
"bauer/internal/copilotcli"
"bauer/internal/github"
"bauer/internal/orchestrator"
"bauer/internal/source"
"bauer/internal/workflow"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
)

Expand Down Expand Up @@ -56,7 +60,27 @@ func main() {
OutputDir: *outputDir,
}

orch := orchestrator.NewOrchestrator()
// Resolve credentials path to absolute so it remains valid after directory changes.
absCredentials, err := filepath.Abs(*credentialsPath)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to resolve credentials path: %v\n", err)
os.Exit(1)
}

cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to get working directory: %v\n", err)
os.Exit(1)
}

copilotAgent, err := copilotcli.NewClient(cwd)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to create Copilot client: %v\n", err)
os.Exit(1)
}

sources := source.NewManager(absCredentials)
orch := orchestrator.New(copilotAgent, sources)

// Execute the complete workflow
result, err := workflow.ExecuteWorkflow(context.Background(), workflowInput, orch)
Expand All @@ -70,3 +94,57 @@ func main() {
fmt.Printf("Branch: %s\n", result.RepositoryInfo.BranchName)
fmt.Printf("PR: %s\n", result.FinalizationInfo.PullRequest.URL)
}

// resolveCLIConfig builds a Config from CLI flags, falling back to environment variables
// when flag values are empty. It does NOT call Validate — callers must do that separately.
func resolveCLIConfig(flags config.CLIFlags) (*config.Config, error) {
docID := flags.DocID
if docID == "" {
Comment on lines +98 to +102
docID = os.Getenv("BAUER_DOC_ID")
}

credentialsPath := flags.CredentialsPath
if credentialsPath == "" {
credentialsPath = os.Getenv("BAUER_CREDENTIALS")
}

outputDir := flags.OutputDir
if outputDir == "" {
outputDir = os.Getenv("BAUER_OUTPUT_DIR")
}

model := flags.Model
if model == "" {
model = os.Getenv("BAUER_MODEL")
}

summaryModel := flags.SummaryModel
if summaryModel == "" {
summaryModel = os.Getenv("BAUER_SUMMARY_MODEL")
}

targetRepo := flags.TargetRepo
if targetRepo == "" {
targetRepo = os.Getenv("BAUER_TARGET_REPO")
}

return &config.Config{
DocID: docID,
CredentialsPath: credentialsPath,
DryRun: flags.DryRun,
ChunkSize: flags.ChunkSize,
OutputDir: outputDir,
Model: model,
SummaryModel: summaryModel,
TargetRepo: targetRepo,
}, nil
}

// openPRExecutionConfig returns a copy of cfg with DryRun forced to false,
// so that the Copilot agent runs even when the overall --dry-run flag was set.
func openPRExecutionConfig(cfg *config.Config) *config.Config {
clone := *cfg
f := false
clone.DryRun = &f
return &clone
}
67 changes: 59 additions & 8 deletions docs/implementation-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Each sub-agent appends its entry to the **Branch Log** section below. You (the r
| Order | Branch | Parent | Phase / Tasks | Status |
|-------|--------|--------|---------------|--------|
| 0 | `feature/bauer-v2` | `main` | Base branch — no code changes | ✅ created |
| 1 | `feat/phase-0a-agent-source` | `feature/bauer-v2` | 001 Phase 0: T0.1, T0.2, T0.2a, T0.2b | ⏳ pending |
| 1 | `feat/phase-0a-agent-source` | `feature/bauer-v2` | 001 Phase 0: T0.1, T0.2, T0.2a, T0.2b | ✅ done |
| 2 | `feat/phase-0b-artifacts-config` | `feat/phase-0a-agent-source` | 001 Phase 0: T0.2c, T0.3, T0.4, T0.5 | ⏳ pending |
| 3 | `feat/phase-1-cli-restore` | `feat/phase-0b-artifacts-config` | 001 Phase 1: T1.1, T1.2, T1.3 | ⏳ pending |
| 4 | `feat/phase-2-cli-features` | `feat/phase-1-cli-restore` | 001 Phase 2: T2.1, T2.2, T2.3 | ⏳ pending |
Expand All @@ -45,13 +45,64 @@ _Parent: `feature/bauer-v2`_

**Tasks:** T0.1, T0.2, T0.2a, T0.2b

**Summary:** _(to be filled by agent)_

**Files changed:** _(to be filled by agent)_

**Docs / references:** _(to be filled by agent)_

**Diagram:** _(optional mermaid, to be filled by agent)_
**Summary:** Introduced the `agent.Agent` interface to decouple the orchestrator from `copilotcli.Client`. Created the `source` layer (`source.Manager`) so the orchestrator no longer imports `gdocs` directly. `copilotcli.Client` now satisfies `agent.Agent` via a compile-time check. All call sites in `cmd/bauer` and `cmd/app` updated to use `orchestrator.New(agent, sources)`. Also fixed pre-existing test failures: `Config.DryRun` promoted to `*bool` with `BoolPtr`/`BoolVal` helpers; `config.CLIFlags` added; `cmd/bauer` now has `resolveCLIConfig` and `openPRExecutionConfig`; `config_test.go` updated with valid credentials JSON fixture.

**Files changed:**
- `internal/agent/agent.go` — new: `Agent` interface with `Start`, `ExecuteChunk`, `GenerateSummary`, `Stop`
- `internal/agent/mock.go` — new: `MockAgent` no-op implementation for tests
- `internal/agent/agent_test.go` — new: tests for `MockAgent` and compile-time interface check
- `internal/source/source.go` — new: `Adapter` interface and `Request` type
- `internal/source/types.go` — new: `SourceBundle` wrapping `*gdocs.ProcessingResult` and design placeholder
- `internal/source/manager.go` — new: `Manager` with `NewManager` and `Fetch` (calls gdocs)
- `internal/source/manager_test.go` — new: tests for `NewManager` and empty-request `Fetch`
- `internal/copilotcli/client.go` — updated: `Start` accepts `context.Context`; `GenerateSummary` signature changed to `(ctx, []string, model) (string, error)`; `buildSummaryPrompt` simplified to `[]string`; compile-time check `var _ agent.Agent = (*Client)(nil)` added
- `internal/orchestrator/orchestrator.go` — major refactor: no longer imports `copilotcli` or `gdocs`; `DefaultOrchestrator` holds `agent.Agent` and `*source.Manager`; `New(agent, sources)` constructor; `OrchestrationResult.ExtractionResult` renamed to `ExtractionBundle *source.SourceBundle`; local `ChunkOutput` type; `executeAgentChunks` uses `agent.Agent`
- `internal/config/config.go` — `DryRun` changed from `bool` to `*bool`; `BoolPtr` and `BoolVal` helpers added
- `internal/config/cli.go` — `CLIFlags` struct added; `DryRun` assignment fixed for `*bool`
- `internal/config/config_test.go` — credentials fixture updated to valid service-account JSON
- `internal/workflow/workflow.go` — `DryRun: config.BoolPtr(input.DryRun)` and `ExtractionBundle` reference
- `cmd/bauer/main.go` — uses `orchestrator.New(copilotAgent, sources)`; adds `resolveCLIConfig` and `openPRExecutionConfig`
- `cmd/app/main.go` — uses `orchestrator.New(copilotAgent, sources)`

**Diagram:**

```mermaid
graph LR
subgraph cmd
A[cmd/bauer/main.go]
B[cmd/app/main.go]
end
subgraph orchestrator
C[DefaultOrchestrator]
end
subgraph agent
D[Agent interface]
E[MockAgent]
end
subgraph copilotcli
F[Client]
end
subgraph source
G[Manager]
H[SourceBundle]
end
subgraph gdocs
I[ProcessingResult]
end

A --> C
B --> C
A --> F
B --> F
A --> G
B --> G
C --> D
C --> G
F -->|implements| D
E -->|implements| D
G --> I
H --> I
```

---

Expand Down
24 changes: 24 additions & 0 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// internal/agent/agent.go
package agent

import "context"

// Agent is the interface any AI execution backend must implement.
// copilotcli.Client implements this; future backends (REST-based agents,
// test mocks, etc.) can implement it without touching the orchestrator.
type Agent interface {
// Start boots the agent (e.g. starts the Copilot SDK server process).
// Must be called before any other method. Callers should defer Stop().
Start(ctx context.Context) error

// ExecuteChunk sends a single chunk prompt file to the agent and returns
// the full text output. chunkNum is for logging/display only.
ExecuteChunk(ctx context.Context, chunkPath string, chunkNum int, model string) (string, error)

// GenerateSummary produces a summary of all chunk outputs.
// Only called when there are multiple chunks.
GenerateSummary(ctx context.Context, outputs []string, model string) (string, error)

// Stop shuts the agent down cleanly. Safe to call after a failed Start.
Stop() error
}
50 changes: 50 additions & 0 deletions internal/agent/agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package agent_test

import (
"context"
"testing"

"bauer/internal/agent"
)

// Compile-time check that MockAgent implements Agent.
var _ agent.Agent = agent.MockAgent{}

func TestMockAgent_Start(t *testing.T) {
a := agent.MockAgent{}
if err := a.Start(context.Background()); err != nil {
t.Fatalf("Start() = %v, want nil", err)
}
}

func TestMockAgent_Stop(t *testing.T) {
a := agent.MockAgent{}
if err := a.Stop(); err != nil {
t.Fatalf("Stop() = %v, want nil", err)
}
}

func TestMockAgent_ExecuteChunk(t *testing.T) {
a := agent.MockAgent{}
out, err := a.ExecuteChunk(context.Background(), "chunk-1.md", 1, "gpt-4")
if err != nil {
t.Fatalf("ExecuteChunk() error = %v", err)
}
if out == "" {
t.Fatal("ExecuteChunk() returned empty output")
}
if want := "mock output for chunk chunk-1.md"; out != want {
t.Fatalf("ExecuteChunk() = %q, want %q", out, want)
}
}

func TestMockAgent_GenerateSummary(t *testing.T) {
a := agent.MockAgent{}
summary, err := a.GenerateSummary(context.Background(), []string{"output1", "output2"}, "gpt-4")
if err != nil {
t.Fatalf("GenerateSummary() error = %v", err)
}
if want := "mock summary"; summary != want {
t.Fatalf("GenerateSummary() = %q, want %q", summary, want)
}
}
16 changes: 16 additions & 0 deletions internal/agent/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// internal/agent/mock.go
package agent

import "context"

// MockAgent is a no-op Agent implementation for use in tests.
type MockAgent struct{}

func (m MockAgent) Start(_ context.Context) error { return nil }
func (m MockAgent) ExecuteChunk(_ context.Context, chunkPath string, _ int, _ string) (string, error) {
return "mock output for chunk " + chunkPath, nil
}
func (m MockAgent) GenerateSummary(_ context.Context, _ []string, _ string) (string, error) {
return "mock summary", nil
}
func (m MockAgent) Stop() error { return nil }
16 changes: 15 additions & 1 deletion internal/config/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ import (
"os"
)

// CLIFlags holds the raw command-line flag values before environment-variable resolution.
type CLIFlags struct {
DocID string
CredentialsPath string
ConfigFile string
DryRun *bool
ChunkSize int
PageRefresh *bool
OutputDir string
Model string
SummaryModel string
TargetRepo string
}

// Load parses command-line flags and returns a validated Config.
func Load() (*Config, error) {
// Define flags
Expand Down Expand Up @@ -76,7 +90,7 @@ func Load() (*Config, error) {
cfg := &Config{
DocID: *docID,
CredentialsPath: *credentialsPath,
DryRun: *dryRun,
DryRun: dryRun,
ChunkSize: *chunkSize,
PageRefresh: *pageRefresh,
OutputDir: *outputDir,
Expand Down
14 changes: 13 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ type Config struct {
CredentialsPath string `json:"credentials"`

// DryRun indicates if the tool should skip side-effect operations (Copilot CLI, PR creation).
DryRun bool `json:"dry_run"`
// Uses *bool so that an explicit false from CLI flags can override a true from a config file.
DryRun *bool `json:"dry_run,omitempty"`

Comment thread
canonical-muhammadbassiony marked this conversation as resolved.
// ChunkSize is the total number of chunks to create from all locations.
// Default is 1 if not specified, or 5 if PageRefresh is true.
Expand Down Expand Up @@ -100,3 +101,14 @@ func ValidateCredentialsPath(path string) error {
}
return nil
}

// BoolPtr returns a pointer to the given bool value.
func BoolPtr(v bool) *bool { return &v }

// BoolVal dereferences a *bool pointer. Returns def if v is nil.
func BoolVal(v *bool, def bool) bool {
if v == nil {
return def
}
return *v
}
Loading