feat: artifact history and config manager (Phase 0b — T0.2c, T0.3, T0.4, T0.5)#37
Conversation
- T0.1: internal/agent package with Agent interface and MockAgent - T0.2: copilotcli.Client implements agent.Agent (updated Start/GenerateSummary sigs) - T0.2a: internal/source package with SourceBundle and Manager - T0.2b: orchestrator refactored to use source.Manager and agent.Agent - Fix: Config.DryRun changed to *bool with BoolPtr/BoolVal helpers - Fix: config.CLIFlags added; resolveCLIConfig/openPRExecutionConfig added to cmd/bauer - Fix: config_test.go updated to use valid credentials JSON fixture
…remove JSON config - T0.2c: internal/artifacts package with append-only run directories - T0.3: internal/config/manager.go with Resolver, EnvVarSource, DefaultsSource - T0.4: BAUER_GITHUB_TOKEN priority in github/auth.go - T0.5: remove json.go + --config flag, add .env.example, update .gitignore
There was a problem hiding this comment.
Pull request overview
This PR introduces an append-only artifact history store for Bauer runs (timestamped run directories + runs.jsonl index) and adds a layered configuration resolver (defaults → env vars → flags), while removing the legacy JSON config file/loader and updating entrypoints/docs accordingly.
Changes:
- Added
internal/artifactsrun manager and integrated artifact writing into orchestration (extraction, prompts, outputs, summary, run status/indexing). - Added
internal/config/manager.goresolver withDefaultsSource,EnvVarSource, andFlagsSource; updatedConfig(notablyPageRefresh→*bool) and updated call sites. - Removed JSON config support (
config.json,internal/config/json.go,--configflag); added.env.example, updated Taskfile and.gitignore.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| Taskfile.yml | Updates local run instructions and adds a Figma token verification task. |
| internal/workflow/workflow.go | Converts workflow input booleans to *bool config fields (PageRefresh). |
| internal/orchestrator/orchestrator.go | Starts/completes artifact runs and writes extraction/prompts/outputs/summary into the artifact store. |
| internal/github/auth.go | Adds BAUER_GITHUB_TOKEN as highest-priority env var for GitHub auth. |
| internal/config/manager.go | New layered config resolver and env/default/flags sources. |
| internal/config/manager_test.go | New tests for resolver layering and env/credentials behavior. |
| internal/config/config.go | Promotes PageRefresh to *bool and adds new config fields (artifacts, figma, GitHub, PR/issue toggles). |
| internal/config/config_test.go | Updates tests to use BoolPtr(...) for PageRefresh. |
| internal/config/cli.go | Removes JSON config flag and adds --artifacts-dir; adapts PageRefresh to pointer. |
| internal/artifacts/manager.go | New artifact manager: run IDs, run dirs, metadata, runs.jsonl, artifact writers. |
| internal/artifacts/manager_test.go | Adds tests for run ID format/uniqueness, directory structure, and JSONL appends. |
| docs/implementation-log.md | Marks Phase 0b complete and documents the changes. |
| internal/config/json.go | Deletes JSON config loader. |
| config.json | Removes repository JSON config file. |
| cmd/bauer/main.go | Wires in the artifact manager when constructing the orchestrator. |
| cmd/app/v1/api.go | Converts API payload PageRefresh into *bool in config.Config. |
| cmd/app/types/config.go | Adds --artifacts-dir and credentials env fallback; removes JSON config flag usage. |
| cmd/app/main.go | Wires in the artifact manager using configured artifacts directory. |
| .gitignore | Adds ignores for config.json and *.pem. |
| .env.example | Adds canonical list of BAUER_* env vars and example setup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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 } |
| cfg.ChunkSize, _ = strconv.Atoi(v) | ||
| } | ||
| if v := os.Getenv("BAUER_PAGE_REFRESH"); v != "" { | ||
| b := v == "true" | ||
| cfg.PageRefresh = &b | ||
| } | ||
| if v := os.Getenv("BAUER_DRY_RUN"); v != "" { | ||
| b := v == "true" |
| // FlagsSource converts a CLIFlags struct into a Config. | ||
| // Only non-zero values are included so they properly override lower-priority sources. | ||
| type FlagsSource struct { | ||
| flags CLIFlags | ||
| } | ||
|
|
||
| // NewFlagsSource creates a FlagsSource from the given CLI flags. | ||
| func NewFlagsSource(flags CLIFlags) *FlagsSource { | ||
| return &FlagsSource{flags: flags} | ||
| } | ||
|
|
||
| func (f *FlagsSource) Load() (*Config, error) { | ||
| return &Config{ | ||
| DocID: f.flags.DocID, | ||
| CredentialsPath: f.flags.CredentialsPath, | ||
| DryRun: f.flags.DryRun, | ||
| ChunkSize: f.flags.ChunkSize, | ||
| PageRefresh: f.flags.PageRefresh, | ||
| OutputDir: f.flags.OutputDir, | ||
| Model: f.flags.Model, | ||
| SummaryModel: f.flags.SummaryModel, | ||
| TargetRepo: f.flags.TargetRepo, | ||
| ArtifactsDir: f.flags.ArtifactsDir, | ||
| }, nil |
There was a problem hiding this comment.
This is working as intended. CLIFlags.DryRun and CLIFlags.PageRefresh are *bool fields — they start as nil when the flag is not explicitly set by the user. FlagsSource.Load() forwards these nil pointers, and mergeConfig only overwrites the base when the override value is non-nil (if override.DryRun != nil). Forwarding nil is a no-op and lower-priority sources correctly apply.
| // Clear any env vars that might interfere | ||
| os.Unsetenv("BAUER_DOC_ID") | ||
| os.Unsetenv("BAUER_MODEL") | ||
|
|
| } | ||
|
|
||
| func TestResolver_CredentialsFallback(t *testing.T) { | ||
| os.Unsetenv("BAUER_CREDENTIALS_PATH") |
| rand.Read(b) | ||
| return fmt.Sprintf("%s-%s", ts, hex.EncodeToString(b)) |
| _, err = fmt.Fprintf(f, "%s\n", line) | ||
| return err |
| desc: Run the API server locally | ||
| cmds: | ||
| - ./bauer-api --config config.json | ||
| - ./bauer-api --credentials ${BAUER_CREDENTIALS_PATH} |
There was a problem hiding this comment.
The file header at line 3 explicitly states: Copy .env.example to .env.local and fill in your values before running. This is a local dev-only Taskfile — users are expected to have their environment configured before running tasks. An empty argument will cause the binary to fail with a clear error message at startup, which is the desired behavior.
| verify-figma: | ||
| desc: Verify Figma token is set and reachable | ||
| cmds: | ||
| - | | ||
| if [ -z "$BAUER_FIGMA_TOKEN" ] && [ -z "$FIGMA_TOKEN" ]; then | ||
| echo "ERROR: BAUER_FIGMA_TOKEN (or FIGMA_TOKEN) must be set" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "Figma token is set." |
| // NewManager creates a Manager for the given artifacts directory. | ||
| func NewManager(base string) *Manager { | ||
| if base == "" { | ||
| base = "./bauer-artifacts" | ||
| } | ||
| return &Manager{base: base} | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 22 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
internal/config/cli.go:36
- In config.Load(), dryRun and pageRefresh are created via flag.Bool(...), which always returns non-nil pointers. That means these fields will always look “set” even when the user did not explicitly provide the flags, preventing env/default layers (and future Resolver usage) from ever enabling them. Consider using an explicit tri-state flag implementation (tracks whether the flag was provided) so these pointers can remain nil when unset, and only become non-nil when the user passes the flag.
docID := flag.String("doc-id", "", "Google Doc ID to extract feedback from (required)")
credentialsPath := flag.String("credentials", "", "Path to service account JSON (required)")
dryRun := flag.Bool("dry-run", false, "Run extraction and planning only; skip Copilot and PR creation")
chunkSize := flag.Int("chunk-size", 0, "Total number of chunks to create (default: 1, or 5 if --page-refresh is set)")
pageRefresh := flag.Bool("page-refresh", false, "Use page refresh mode with page-refresh-instructions template (default chunk size: 5)")
outputDir := flag.String("output-dir", "bauer-output", "Directory for generated prompt files (default: bauer-output)")
| // Write prompts to artifact store | ||
| if runID != "" { | ||
| for _, chunk := range chunks { | ||
| // Read the generated prompt file and archive it | ||
| if data, readErr := os.ReadFile(chunk.Filename); readErr == nil { | ||
| if writeErr := o.arts.WritePrompt(runID, chunk.ChunkNumber, len(chunks), string(data)); writeErr != nil { | ||
| slog.Warn("Failed to write prompt artifact", slog.String("error", writeErr.Error())) | ||
| } | ||
| } | ||
| } |
| // Append to runs.jsonl | ||
| entry := RunIndexEntry{ | ||
| RunID: meta.RunID, | ||
| StartedAt: meta.StartedAt, | ||
| CompletedAt: meta.CompletedAt, | ||
| Status: status, | ||
| DocID: meta.DocID, | ||
| FigmaURL: meta.FigmaURL, | ||
| Mode: meta.Mode, | ||
| ChunkCount: chunkCount, | ||
| ArtifactDir: meta.ArtifactDir, | ||
| } | ||
| line, err := json.Marshal(entry) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal run index entry: %w", err) | ||
| } | ||
|
|
||
| indexPath := filepath.Join(m.base, "runs.jsonl") | ||
| f, err := os.OpenFile(indexPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) | ||
| if err != nil { | ||
| return fmt.Errorf("open runs.jsonl: %w", err) | ||
| } | ||
| defer f.Close() | ||
| if _, err = fmt.Fprintf(f, "%s\n", line); err != nil { | ||
| return fmt.Errorf("write run index: %w", err) | ||
| } |
| You are an agents coordinator, your main task is to take my instructions and make sure they are implemented properly. | ||
| Take the specs #file:001_v2_reconciliation.md and #file:002_figma_integration.md into account, along with all other files in #file:docs into account. I had implemented detailed specs to implement a few new features. | ||
| There is an #file:implementation-log.md that describes what happened in the previous runs and progress on implementation, however, Im not sure about what is the progress of this plan. I see all mentioned branches created when I run git branch, but I do not see them marked as done in the log. | ||
| Your main task is to ensure that this plan has been implemented correctly. This means: | ||
|
|
||
| - review already implemented code | ||
| - make sure all requirements were adressed correctly | ||
| - implement any improvements | ||
| - rebase next branch in the list | ||
| - repeat | ||
|
|
||
| as you can see from the log, this plan is a bunch of stacked PRs this is important to make reviewing the code easier on me. As I just mentioned you need to sping up subagents, make each one of them rebase their branch on top of the previous one, review the branch, implement any improvements, then log what it did and mark the branch as done & reviewed in the log. | ||
|
|
||
| Your main task is an agents coordinator, you should NOT attempt to read or write any code yourself, except what is in the docs folder files, if you need to check the state of something simply spin up a subagent and give it the appropriate instruction to complete its task. You should also NOT attempt to merge any branches on top of each other, this will be my job as I review the code. |
There was a problem hiding this comment.
prompt.md is an internal development guide/coordinator reference used during development sessions outside the codebase tooling (e.g., when driving Copilot manually). It is intentionally in the repo root as a working document for contributors — not part of the product runtime.
| completeRun := func(status string, chunkCount int) { | ||
| if runID == "" { | ||
| return | ||
| } | ||
| if err := o.arts.CompleteRun(runID, status, chunkCount); err != nil { | ||
| slog.Warn("Failed to complete artifact run", slog.String("error", err.Error())) | ||
| } | ||
| } |
| if meta.ArtifactDir == "" { | ||
| meta.ArtifactDir = filepath.Join(m.base, meta.RunID) | ||
| } | ||
|
|
||
| dirs := []string{ | ||
| filepath.Join(m.base, meta.RunID, "extraction"), | ||
| filepath.Join(m.base, meta.RunID, "prompts"), | ||
| filepath.Join(m.base, meta.RunID, "outputs"), | ||
| filepath.Join(m.base, meta.RunID, "logs"), | ||
| filepath.Join(m.base, meta.RunID, "screenshots"), |
| func (e *EnvVarSource) Load() (*Config, error) { | ||
| cfg := &Config{} | ||
| // Credentials — check BAUER_CREDENTIALS_PATH, then GOOGLE_APPLICATION_CREDENTIALS | ||
| if v := os.Getenv("BAUER_CREDENTIALS_PATH"); v != "" { | ||
| cfg.CredentialsPath = v | ||
| } 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.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") | ||
| if cfg.FigmaToken == "" { | ||
| cfg.FigmaToken = os.Getenv("FIGMA_TOKEN") | ||
| } |
| return &Config{ | ||
| DocID: f.flags.DocID, | ||
| CredentialsPath: f.flags.CredentialsPath, | ||
| DryRun: f.flags.DryRun, | ||
| ChunkSize: f.flags.ChunkSize, | ||
| PageRefresh: f.flags.PageRefresh, | ||
| OutputDir: f.flags.OutputDir, | ||
| Model: f.flags.Model, | ||
| SummaryModel: f.flags.SummaryModel, | ||
| TargetRepo: f.flags.TargetRepo, | ||
| ArtifactsDir: f.flags.ArtifactsDir, | ||
| }, nil |
| @@ -51,13 +51,13 @@ func Load() (*Config, error) { | |||
| typ string | |||
| desc string | |||
| }{ | |||
| {"--config", "<string>", "Path to JSON config file"}, | |||
| {"--doc-id", "<string>", "Google Doc ID to extract feedback from (required)"}, | |||
| {"--credentials", "<string>", "Path to service account JSON (required)"}, | |||
| {"--dry-run", "", "Run extraction and planning only; skip Copilot and PR creation"}, | |||
| {"--page-refresh", "", "Use page refresh mode with page-refresh-instructions template"}, | |||
| {"--chunk-size", "<int>", "Total number of chunks to create (default: 1, or 5 if --page-refresh is set)"}, | |||
| {"--output-dir", "<string>", "Directory for generated prompt files (default: bauer-output)"}, | |||
| {"--artifacts-dir", "<string>", "Directory for run artifacts (default: ./bauer-artifacts)"}, | |||
| {"--model", "<string>", "Copilot model to use for sessions (default: gpt-5-mini-high)"}, | |||
| {"--summary-model", "<string>", "Copilot model to use for summary session (default: gpt-5-mini-high)"}, | |||
| {"--target-repo", "<string>", "Path to target repository where tasks should be executed (default: current directory)"}, | |||
| @@ -76,11 +76,6 @@ func Load() (*Config, error) { | |||
|
|
|||
| flag.Parse() | |||
|
|
|||
| // If --config is provided, load from JSON file | |||
| if *configFile != "" { | |||
| return LoadFromJSONFile(*configFile) | |||
| } | |||
|
|
|||
| // If no required flags are provided, show usage and exit | |||
| if *docID == "" && *credentialsPath == "" { | |||
| flag.Usage() | |||
| @@ -92,11 +87,12 @@ func Load() (*Config, error) { | |||
| CredentialsPath: *credentialsPath, | |||
| DryRun: dryRun, | |||
| ChunkSize: *chunkSize, | |||
| PageRefresh: *pageRefresh, | |||
| PageRefresh: pageRefresh, | |||
| OutputDir: *outputDir, | |||
| Model: *model, | |||
| SummaryModel: *summaryModel, | |||
| TargetRepo: *targetRepo, | |||
| ArtifactsDir: *artifactsDir, | |||
| } | |||
| @@ -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]) | |||
| } | |||
| # --- Copilot / model --- | ||
| BAUER_MODEL=gpt-5-mini-high | ||
| BAUER_SUMMARY_MODEL=gpt-5-mini-high | ||
|
|
||
| # --- Bauer behaviour --- | ||
| BAUER_DOC_ID= | ||
| BAUER_CHUNK_SIZE=1 | ||
| BAUER_PAGE_REFRESH=false | ||
| BAUER_ARTIFACTS_DIR=./bauer-artifacts | ||
| BAUER_BRANCH_PREFIX=bauer | ||
| BAUER_DRY_RUN=false |
| # --- API Server --- | ||
| BAUER_API_PORT=8090 | ||
|
|
| There is an #file:implementation-log.md that describes what happened in the previous runs and progress on implementation, however, Im not sure about what is the progress of this plan. I see all mentioned branches created when I run git branch, but I do not see them marked as done in the log. | ||
| Your main task is to ensure that this plan has been implemented correctly. This means: | ||
|
|
||
| - review already implemented code | ||
| - make sure all requirements were adressed correctly |
5dd0c4e to
aa21438
Compare
Summary
Adds append-only artifact storage and a unified layered config resolver. Removes JSON config entirely.
Tasks Implemented
runs.jsonlindex under--artifacts-dir(default./bauer-artifacts)internal/config/manager.gowithResolvermergingDefaultsSource→EnvVarSource→FlagsSourceBAUER_CREDENTIALS_PATH+GOOGLE_APPLICATION_CREDENTIALSfallback;BAUER_GITHUB_TOKENchecked firstconfig.json+internal/config/json.go; created.env.example; updated.gitignoreFiles Changed
internal/artifacts/manager.go—Manager,StartRun,CompleteRun,WriteGDocsExtraction,WritePrompt,WriteOutput,WriteSummaryinternal/artifacts/manager_test.go— tests for run ID format, directory structure, JSONL appendinternal/config/manager.go—Sourceinterface,Resolver,EnvVarSource,DefaultsSource,FlagsSourceinternal/config/manager_test.go— env override, bool pointer, credentials fallback testsinternal/config/config.go—PageRefresh→*bool; new fields (ArtifactsDir,FigmaURL,FigmaToken, etc.)internal/config/json.go— deleted.env.example— new: canonical reference for allBAUER_*env vars.gitignore— addedconfig.json,*.pemConfig Resolution
Part of the Bauer v2 stacked PR series (Branch 2 of 12).