Skip to content

feat: artifact history and config manager (Phase 0b — T0.2c, T0.3, T0.4, T0.5)#37

Open
canonical-muhammadbassiony wants to merge 5 commits into
feat/phase-0a-agent-sourcefrom
feat/phase-0b-artifacts-config
Open

feat: artifact history and config manager (Phase 0b — T0.2c, T0.3, T0.4, T0.5)#37
canonical-muhammadbassiony wants to merge 5 commits into
feat/phase-0a-agent-sourcefrom
feat/phase-0b-artifacts-config

Conversation

@canonical-muhammadbassiony

Copy link
Copy Markdown
Collaborator

Summary

Adds append-only artifact storage and a unified layered config resolver. Removes JSON config entirely.

Tasks Implemented

  • T0.2c: Append-only artifact history — timestamped run directories with runs.jsonl index under --artifacts-dir (default ./bauer-artifacts)
  • T0.3: internal/config/manager.go with Resolver merging DefaultsSourceEnvVarSourceFlagsSource
  • T0.4: BAUER_CREDENTIALS_PATH + GOOGLE_APPLICATION_CREDENTIALS fallback; BAUER_GITHUB_TOKEN checked first
  • T0.5: Deleted config.json + internal/config/json.go; created .env.example; updated .gitignore

Files Changed

  • internal/artifacts/manager.goManager, StartRun, CompleteRun, WriteGDocsExtraction, WritePrompt, WriteOutput, WriteSummary
  • internal/artifacts/manager_test.go — tests for run ID format, directory structure, JSONL append
  • internal/config/manager.goSource interface, Resolver, EnvVarSource, DefaultsSource, FlagsSource
  • internal/config/manager_test.go — env override, bool pointer, credentials fallback tests
  • internal/config/config.goPageRefresh*bool; new fields (ArtifactsDir, FigmaURL, FigmaToken, etc.)
  • internal/config/json.godeleted
  • .env.example — new: canonical reference for all BAUER_* env vars
  • .gitignore — added config.json, *.pem

Config Resolution

FlagsSource (highest) → EnvVarSource → DefaultsSource (lowest)

Part of the Bauer v2 stacked PR series (Branch 2 of 12).

Bauer Agent added 3 commits May 20, 2026 12:30
- 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/artifacts run manager and integrated artifact writing into orchestration (extraction, prompts, outputs, summary, run status/indexing).
  • Added internal/config/manager.go resolver with DefaultsSource, EnvVarSource, and FlagsSource; updated Config (notably PageRefresh*bool) and updated call sites.
  • Removed JSON config support (config.json, internal/config/json.go, --config flag); 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.

Comment thread internal/config/manager.go Outdated
Comment on lines +41 to +56
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 }
Comment thread internal/config/manager.go Outdated
Comment on lines +84 to +91
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"
Comment on lines +116 to +139
// 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

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 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.

Comment thread internal/config/manager_test.go Outdated
Comment on lines +40 to +43
// Clear any env vars that might interfere
os.Unsetenv("BAUER_DOC_ID")
os.Unsetenv("BAUER_MODEL")

Comment thread internal/config/manager_test.go Outdated
}

func TestResolver_CredentialsFallback(t *testing.T) {
os.Unsetenv("BAUER_CREDENTIALS_PATH")
Comment thread internal/artifacts/manager.go Outdated
Comment on lines +57 to +58
rand.Read(b)
return fmt.Sprintf("%s-%s", ts, hex.EncodeToString(b))
Comment thread internal/artifacts/manager.go Outdated
Comment on lines +138 to +139
_, err = fmt.Fprintf(f, "%s\n", line)
return err
Comment thread Taskfile.yml
desc: Run the API server locally
cmds:
- ./bauer-api --config config.json
- ./bauer-api --credentials ${BAUER_CREDENTIALS_PATH}

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.

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.

Comment thread Taskfile.yml
Comment on lines +28 to +36
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."
Comment on lines +44 to +50
// NewManager creates a Manager for the given artifacts directory.
func NewManager(base string) *Manager {
if base == "" {
base = "./bauer-artifacts"
}
return &Manager{base: base}
}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)")

Comment on lines +150 to +159
// 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()))
}
}
}
Comment on lines +124 to +149
// 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)
}
Comment thread prompt.md
Comment on lines +1 to +14
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.

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.

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.

Comment thread internal/orchestrator/orchestrator.go Outdated
Comment on lines +83 to +90
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()))
}
}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 8 comments.

Comment on lines +80 to +89
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"),
Comment on lines +96 to +114
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")
}
Comment on lines +170 to +181
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
Comment thread internal/config/cli.go
Comment on lines 31 to 96
@@ -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,
}
Comment thread internal/github/pr.go
Comment on lines 69 to 79
@@ -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])
}
Comment thread .env.example
Comment on lines +33 to +43
# --- 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
Comment thread .env.example
Comment on lines +30 to +32
# --- API Server ---
BAUER_API_PORT=8090

Comment thread prompt.md
Comment on lines +3 to +7
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants