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
1 change: 0 additions & 1 deletion cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,3 @@ func main() {
os.Exit(1)
}
}

3 changes: 1 addition & 2 deletions cmd/app/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,11 @@ func executeJob(requestID string, cfg config.Config, rc types.RouteConfig) {
)
}


func GetHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := types.Success().Render(w, r)
if err != nil {
slog.Error("error writing response", "error", err.Error())
}
}
}
9 changes: 6 additions & 3 deletions cmd/bauer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ func main() {
openIssue := fs.Bool("open-issue", false, "Generate a plan and open a GitHub issue without applying changes (mutually exclusive with --open-pr)")
branchPrefix := fs.String("branch-prefix", "", "Prefix for created branches (default: bauer)")
githubRepo := fs.String("github-repo", "", "GitHub repository in owner/repo format (required for --open-pr and --open-issue)")
figmaURL := fs.String("figma-url", "", "Figma file or design URL for design reference\n\t(requires BAUER_FIGMA_TOKEN or FIGMA_TOKEN)")

fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage:\n\n")
fmt.Fprintf(os.Stderr, "\t%s --doc-id <doc-id> [--credentials <path>] [flags]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Usage: %s [flags]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Flags:\n\n")
fs.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nEnvironment variables:\n\n")
Expand All @@ -47,7 +47,9 @@ func main() {
fmt.Fprintf(os.Stderr, "\tBAUER_ARTIFACTS_DIR Override for --artifacts-dir\n")
fmt.Fprintf(os.Stderr, "\tBAUER_BRANCH_PREFIX Override for --branch-prefix\n")
fmt.Fprintf(os.Stderr, "\tBAUER_GITHUB_REPO Override for --github-repo\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "\tBAUER_FIGMA_URL Override for --figma-url\n")
fmt.Fprintf(os.Stderr, "\tBAUER_FIGMA_TOKEN Figma API token (required when --figma-url is supplied)\n")
fmt.Fprintf(os.Stderr, "\tFIGMA_TOKEN Fallback for BAUER_FIGMA_TOKEN\n")
}
Comment on lines 33 to 53

if err := fs.Parse(os.Args[1:]); err != nil {
Expand All @@ -71,6 +73,7 @@ func main() {
ArtifactsDir: *artifactsDir,
BranchPrefix: *branchPrefix,
GitHubRepo: *githubRepo,
FigmaURL: *figmaURL,
}
fs.Visit(func(f *flag.Flag) {
switch f.Name {
Expand Down
8 changes: 6 additions & 2 deletions docs/implementation-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,13 @@ _Parent: `feat/figma-phase-c-mapping`_

**Tasks:** T2F.8, T2F.9

**Summary:** _(to be filled by agent)_
**Summary:** Threaded Figma through the CLI and orchestrator (T2F.8) and added an optional MCP guidance block to prompts (T2F.9). Added `--figma-url` flag to `cmd/bauer/main.go`, wired into `CLIFlags.FigmaURL`. `orchestrator.Execute` now forks on `cfg.FigmaURL != ""`: the figma-aware path calls the new `generateChunksWithFigma()` method which fetches design data via `sources.FetchFigma`, runs `mapping.Resolver.Build`, persists figma artifacts (extraction, comments, mappings), and generates prompts via `engine.RenderChunksFromResolved`. For T2F.9: added `FigmaURL string` field (with `json:"-"`) to `figmaChunkContext` in `engine.go`; `RenderChunk` now sets `ctx.FigmaURL = data.FigmaURL` before template execution; added an optional MCP guidance block to `internal/prompt/templates/figma-context.md` that renders only when `{{if .FigmaURL}}`; added `Engine.RenderChunksFromResolved()` which generates figma-aware prompt files using `GenerateChunksFromResolved` + `RenderChunk`. `BAUER_FIGMA_TOKEN` env var usage mentioned in `--help` output.

**Files changed:** _(to be filled by agent)_
**Files changed:**
- `cmd/bauer/main.go` — added `--figma-url` flag; `CLIFlags.FigmaURL` wired through `FlagsSource`; `BAUER_FIGMA_TOKEN` env var note in help text
- `internal/orchestrator/orchestrator.go` — `Execute` forks on `cfg.FigmaURL != ""`; new `generateChunksWithFigma()` method: calls `figma.ParseLink`, `figma.NewClient`, `sources.FetchFigma`, `mapping.Resolver.Build`, `arts.WriteFigmaExtraction`/`WriteFigmaComments`/`WriteMappings`, `engine.RenderChunksFromResolved`; log line uses `design.Anchors` (not `.Nodes`)
- `internal/prompt/engine.go` — `figmaChunkContext` gains `FigmaURL string` (json:"-"); `RenderChunk` sets `ctx.FigmaURL = data.FigmaURL`; new `RenderChunksFromResolved()` method that calls `GenerateChunksFromResolved` + `RenderChunk` and writes prompt files to disk
- `internal/prompt/templates/figma-context.md` — optional `{{if .FigmaURL}}` MCP guidance block added at end of template

---

Expand Down
74 changes: 53 additions & 21 deletions internal/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,54 @@ func (r *Resolver) Resolve() (*Config, error) {
}

func mergeConfig(base, override *Config) {
if override.DocID != "" { base.DocID = override.DocID }
if override.CredentialsPath != "" { base.CredentialsPath = override.CredentialsPath }
if override.Model != "" { base.Model = override.Model }
if override.SummaryModel != "" { base.SummaryModel = override.SummaryModel }
if override.ArtifactsDir != "" { base.ArtifactsDir = override.ArtifactsDir }
if override.BranchPrefix != "" { base.BranchPrefix = override.BranchPrefix }
if override.ChunkSize != 0 { base.ChunkSize = override.ChunkSize }
if override.GitHubRepo != "" { base.GitHubRepo = override.GitHubRepo }
if override.FigmaURL != "" { base.FigmaURL = override.FigmaURL }
if override.FigmaToken != "" { base.FigmaToken = override.FigmaToken }
if override.OutputDir != "" { base.OutputDir = override.OutputDir }
if override.TargetRepo != "" { base.TargetRepo = override.TargetRepo }
if override.PageRefresh != nil { base.PageRefresh = override.PageRefresh }
if override.DryRun != nil { base.DryRun = override.DryRun }
if override.OpenPR != nil { base.OpenPR = override.OpenPR }
if override.OpenIssue != nil { base.OpenIssue = override.OpenIssue }
if override.DocID != "" {
base.DocID = override.DocID
}
if override.CredentialsPath != "" {
base.CredentialsPath = override.CredentialsPath
}
if override.Model != "" {
base.Model = override.Model
}
if override.SummaryModel != "" {
base.SummaryModel = override.SummaryModel
}
if override.ArtifactsDir != "" {
base.ArtifactsDir = override.ArtifactsDir
}
if override.BranchPrefix != "" {
base.BranchPrefix = override.BranchPrefix
}
if override.ChunkSize != 0 {
base.ChunkSize = override.ChunkSize
}
if override.GitHubRepo != "" {
base.GitHubRepo = override.GitHubRepo
}
if override.FigmaURL != "" {
base.FigmaURL = override.FigmaURL
}
if override.FigmaToken != "" {
base.FigmaToken = override.FigmaToken
}
if override.OutputDir != "" {
base.OutputDir = override.OutputDir
}
if override.TargetRepo != "" {
base.TargetRepo = override.TargetRepo
}
if override.PageRefresh != nil {
base.PageRefresh = override.PageRefresh
}
if override.DryRun != nil {
base.DryRun = override.DryRun
}
if override.OpenPR != nil {
base.OpenPR = override.OpenPR
}
if override.OpenIssue != nil {
base.OpenIssue = override.OpenIssue
}
}

// EnvVarSource reads BAUER_* env vars.
Expand All @@ -69,14 +101,14 @@ func (e *EnvVarSource) Load() (*Config, error) {
} else if v := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"); v != "" {
cfg.CredentialsPath = v
}
cfg.DocID = os.Getenv("BAUER_DOC_ID")
cfg.Model = os.Getenv("BAUER_MODEL")
cfg.DocID = os.Getenv("BAUER_DOC_ID")
cfg.Model = os.Getenv("BAUER_MODEL")
cfg.SummaryModel = os.Getenv("BAUER_SUMMARY_MODEL")
cfg.ArtifactsDir = os.Getenv("BAUER_ARTIFACTS_DIR")
cfg.BranchPrefix = os.Getenv("BAUER_BRANCH_PREFIX")
cfg.GitHubRepo = os.Getenv("BAUER_GITHUB_REPO")
cfg.FigmaURL = os.Getenv("BAUER_FIGMA_URL")
cfg.FigmaToken = os.Getenv("BAUER_FIGMA_TOKEN")
cfg.GitHubRepo = os.Getenv("BAUER_GITHUB_REPO")
cfg.FigmaURL = os.Getenv("BAUER_FIGMA_URL")
cfg.FigmaToken = os.Getenv("BAUER_FIGMA_TOKEN")
if cfg.FigmaToken == "" {
cfg.FigmaToken = os.Getenv("FIGMA_TOKEN")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/github/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func CreatePR(owner, repo string, opts CreatePROptions) (string, error) {
}

cmd := exec.Command("gh", args...)

// Log token availability for debugging
logger := slog.Default()
ghToken := os.Getenv("GH_TOKEN")
Expand All @@ -77,7 +77,7 @@ func CreatePR(owner, repo string, opts CreatePROptions) (string, error) {
} else {
logger.Debug("GH_TOKEN is set for PR creation", "token_prefix", ghToken[:10])
}

output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to create PR: %w, output: %s", err, output)
Expand Down
100 changes: 93 additions & 7 deletions internal/orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"bauer/internal/agent"
"bauer/internal/artifacts"
"bauer/internal/config"
"bauer/internal/figma"
"bauer/internal/prompt"
"bauer/internal/source"
"bauer/internal/source/mapping"
"context"
"fmt"
"log/slog"
Expand Down Expand Up @@ -137,11 +139,18 @@ func (o *DefaultOrchestrator) Execute(ctx context.Context, cfg *config.Config) (
slog.Int("total_locations", totalLocations),
slog.Int("chunk_size", cfg.ChunkSize),
)
chunks, err := engine.GenerateAllChunks(
bundle.Document,
cfg.ChunkSize,
cfg.OutputDir,
)

// When a Figma URL is configured, fetch design data and use figma-aware prompt generation.
var chunks []prompt.ChunkResult
if cfg.FigmaURL != "" {
chunks, err = o.generateChunksWithFigma(ctx, cfg, bundle, engine, runID)
} else {
chunks, err = engine.GenerateAllChunks(
bundle.Document,
cfg.ChunkSize,
cfg.OutputDir,
)
}
if err != nil {
slog.Error("Failed to generate prompts", slog.String("error", err.Error()))
completeRun("failed", 0)
Expand Down Expand Up @@ -272,9 +281,86 @@ func (o *DefaultOrchestrator) Execute(ctx context.Context, cfg *config.Config) (
}, nil
}

// executeAgentChunks executes each chunk via the agent and returns outputs.
func executeAgentChunks(
// generateChunksWithFigma fetches Figma design data and produces figma-aware prompt files.
// It is called by Execute when cfg.FigmaURL is non-empty.
func (o *DefaultOrchestrator) generateChunksWithFigma(
ctx context.Context,
cfg *config.Config,
bundle *source.SourceBundle,
engine *prompt.Engine,
runID string,
) ([]prompt.ChunkResult, error) {
figmaRef, err := figma.ParseLink(cfg.FigmaURL)
if err != nil {
return nil, fmt.Errorf("invalid figma URL %q: %w", cfg.FigmaURL, err)
}

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.

This is a known limitation. When a Figma URL has no node-id, FetchFigma still proceeds (fetches file-level metadata and comments) but produces zero anchors. The orchestrator continues without Figma anchor context (a warning is logged). Full whole-file node enumeration is planned but intentionally deferred; the current behavior is graceful degradation, not a failure path.


figmaClient := figma.NewClient(cfg.FigmaToken)

// Determine screenshot directory (inside the artifact run when available).
screenshotDir := ""
if runID != "" {
screenshotDir, err = o.arts.EnsureScreenshotsDir(runID)
if err != nil {
slog.Warn("Failed to create screenshots dir, proceeding without screenshots",
slog.String("error", err.Error()))
screenshotDir = ""
}
}
Comment on lines +305 to +309

if screenshotDir == "" {
slog.Warn("Screenshot directory unavailable, skipping screenshot download")
}

slog.Info("Fetching Figma design data", slog.String("figma_url", cfg.FigmaURL))
design, err := o.sources.FetchFigma(ctx, figmaClient, figmaRef, screenshotDir)
if err != nil {
Comment on lines +300 to +317
return nil, fmt.Errorf("fetching figma design: %w", err)
}
slog.Info("Figma design fetched",
slog.Int("anchors", len(design.Anchors)),
slog.Int("comments", len(design.Comments)),
)

// Persist figma artifacts.
if runID != "" {
if werr := o.arts.WriteFigmaExtraction(runID, design); werr != nil {
slog.Warn("Failed to write figma extraction artifact", slog.String("error", werr.Error()))
}
if werr := o.arts.WriteFigmaComments(runID, design.Comments); werr != nil {
slog.Warn("Failed to write figma comments artifact", slog.String("error", werr.Error()))
}
}

// Build resolved chunks (design-aware mapping).
resolver := &mapping.Resolver{}
resolvedChunks := resolver.Build(bundle.Document.GroupedSuggestions, design)
slog.Info("Mapping resolved", slog.Int("resolved_chunks", len(resolvedChunks)))

if runID != "" {
if werr := o.arts.WriteMappings(runID, resolvedChunks); werr != nil {
slog.Warn("Failed to write mappings artifact", slog.String("error", werr.Error()))
}
}

// Generate figma-aware prompt files.
suggestedURL := ""
if bundle.Document.Metadata != nil {
suggestedURL = bundle.Document.Metadata.SuggestedUrl
}

return engine.RenderChunksFromResolved(
bundle.Document.DocumentTitle,
suggestedURL,
cfg.FigmaURL,
resolvedChunks,
cfg.ChunkSize,
cfg.OutputDir,
)
}

// executeAgentChunks executes each chunk via the agent and returns outputs.
func executeAgentChunks(ctx context.Context,
chunks []prompt.ChunkResult,
cfg *config.Config,
a agent.Agent,
Expand Down
44 changes: 44 additions & 0 deletions internal/prompt/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func (e *Engine) RenderChunk(data PromptData) (string, error) {
if err := json.Unmarshal([]byte(data.FigmaContextJSON), &ctx); err != nil {
return "", fmt.Errorf("parsing figma context JSON: %w", err)
}
ctx.FigmaURL = data.FigmaURL
tmpl, err := template.New("figma-context").Parse(figmaContextTemplate)
if err != nil {
return "", fmt.Errorf("parsing figma context template: %w", err)
Expand Down Expand Up @@ -230,10 +231,13 @@ func (e *Engine) GenerateAllChunks(
}

// figmaChunkContext is the data structure serialized into FigmaContextJSON.
// FigmaURL is NOT serialized; it is set at render time from PromptData.FigmaURL
// to power the optional MCP guidance block in the figma-context template.
type figmaChunkContext struct {
Anchors []mapping.DesignAnchorRef `json:"anchors,omitempty"`
Screenshots []string `json:"screenshots,omitempty"`
Comments []mapping.DesignCommentRef `json:"comments,omitempty"`
FigmaURL string `json:"-"`
}

// GenerateChunksFromResolved creates one PromptData per batch of resolved chunks.
Expand Down Expand Up @@ -338,3 +342,43 @@ func indexOf(s, substr string) int {
}
return -1
}

// RenderChunksFromResolved generates figma-aware prompt files from pre-resolved chunks.
// It is used when --figma-url is supplied so that Figma design context is embedded in
// each prompt. outputDir is created if it does not exist.
// The returned ChunkResults contain Filenames suitable for agent execution.
func (e *Engine) RenderChunksFromResolved(
docTitle, suggestedURL, figmaURL string,
chunks []mapping.ResolvedChunk,
chunkSize int,
outputDir string,
) ([]ChunkResult, error) {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return nil, fmt.Errorf("creating output directory %q: %w", outputDir, err)
}

promptDatas, err := e.GenerateChunksFromResolved(docTitle, suggestedURL, figmaURL, chunks, chunkSize)
if err != nil {
return nil, err
}

results := make([]ChunkResult, len(promptDatas))
for i, pd := range promptDatas {
content, err := e.RenderChunk(pd)
if err != nil {
return nil, fmt.Errorf("rendering chunk %d: %w", i+1, err)
}
fname := fmt.Sprintf("chunk-%d-of-%d.md", pd.ChunkNumber, pd.TotalChunks)
fpath := filepath.Join(outputDir, fname)
if err := os.WriteFile(fpath, []byte(content), 0644); err != nil {
return nil, fmt.Errorf("writing chunk %d to file: %w", i+1, err)
}
results[i] = ChunkResult{
ChunkNumber: pd.ChunkNumber,
Content: content,
Filename: fpath,
LocationCount: pd.LocationCount,
}
}
return results, nil
}
17 changes: 17 additions & 0 deletions internal/prompt/templates/figma-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,20 @@ The Google Doc is the canonical intent source. Designer comments are requirement
- Verify your implementation matches the visual design for the suggestion locations in this chunk.
- Do not invent new UI components if the design shows an existing one.
- If the design shows a spacing or typography token, check whether an equivalent exists in the codebase.
{{if .FigmaURL}}
### If you have access to Figma MCP tools (optional)

If your runtime supports the Figma MCP server (e.g. VS Code Copilot Chat, Cursor, or Claude Code),
you may fetch live data directly from the design file to supplement the stored context above:

`{{.FigmaURL}}`

If you choose to use MCP tools:
- Treat Bauer's stored artifacts (screenshots, design node references, and designer comments above)
as the **ground truth** for this run.
- If the live MCP view conflicts with stored artifacts, **surface the conflict explicitly** rather
than silently preferring one over the other.
- Do not rely on MCP tools alone — the stored artifacts are the authoritative reference.

If you do not have access to Figma MCP tools, ignore this section entirely.
{{end}}
2 changes: 1 addition & 1 deletion internal/source/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (m *Manager) FetchFigma(ctx context.Context, client *figma.Client, ref *fig

// Request screenshots for the specified node(s)
screenshotPaths := map[string]string{}
if len(nodeIDs) > 0 {
if len(nodeIDs) > 0 && screenshotDir != "" {
imageURLs, err := client.GetImages(ctx, ref.FileKey, nodeIDs)
if err != nil {
// Non-fatal: log and continue without screenshots
Expand Down
Loading