Skip to content

Commit de0bdec

Browse files
authored
Merge pull request #41 from dapi/codex/gh-38
Replace per-setting configuration files with config.yaml
2 parents 85ac935 + fbf8a01 commit de0bdec

12 files changed

Lines changed: 395 additions & 578 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ This project follows Semantic Versioning.
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- **Breaking:** replaced legacy per-setting `.code-converge/` files with strict project and user `config.yaml` documents. The next release must be a SemVer major release.
12+
913
## [0.7.0] - 2026-07-30
1014

1115
### Added

‎README.md‎

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,13 @@ Agent output is sanitized before rendering: terminal controls are removed, inval
296296

297297
## Configuration
298298

299-
Every option can be supplied in four places: a command-line flag, an environment variable, project configuration, or user configuration.
299+
Every option can be supplied in four places: a command-line flag, an environment variable, project YAML, or user YAML.
300300

301301
Resolution order is highest to lowest priority:
302302

303303
1. Command-line flags
304-
2. Project configuration in `<git-root>/.code-converge/`
305-
3. User configuration in `~/.code-converge/`
304+
2. Project configuration in `<git-root>/.code-converge/config.yaml`
305+
3. User configuration in `~/.code-converge/config.yaml`
306306
4. Environment variables
307307
5. Built-in defaults
308308

@@ -322,9 +322,11 @@ The `fast` and `best` modes select these operative stage profiles. `fast` is the
322322
| Fix findings | `gpt-5.6-luna`, `medium` | `gpt-5.6-terra`, `high` | Findings involve architecture, security, migrations, concurrency, or several connected modules. |
323323
| Fix CI | `gpt-5.6-luna`, `medium` | `gpt-5.6-terra`, `high` | The cause is not localized by logs, spans multiple components, or persists after a repair. |
324324

325+
`config.yaml` is the only file-based configuration source. It is a strict, flat YAML mapping: unknown, duplicate, nested, malformed, and invalid values are rejected. Legacy per-setting files are ignored without migration or fallback. Prompt values remain file references; relative paths resolve from the directory containing the YAML file.
326+
325327
### Options and defaults
326328

327-
| Option | Flag | Environment variable | Project / user file | Default |
329+
| Option | Flag | Environment variable | YAML key | Default |
328330
| --- | --- | --- | --- | --- |
329331
| Workflow log format | `--log-format` | `CODE_CONVERGE_LOG_FORMAT` | `log-format` | `human` |
330332
| Human liveness heartbeat | `--heartbeat` | `CODE_CONVERGE_HEARTBEAT` | `heartbeat` | `0` (disabled) |
@@ -337,43 +339,38 @@ The `fast` and `best` modes select these operative stage profiles. `fast` is the
337339
| Review reasoning effort | `--review-reasoning-effort` | `CODE_CONVERGE_REVIEW_REASONING_EFFORT` | `review-reasoning-effort` | selected profile |
338340
| Fix-findings model | `--fix-model` | `CODE_CONVERGE_FIX_MODEL` | `fix-model` | selected profile |
339341
| Fix-findings reasoning effort | `--fix-reasoning-effort` | `CODE_CONVERGE_FIX_REASONING_EFFORT` | `fix-reasoning-effort` | selected profile |
340-
| Fix-findings prompt | `--fix-prompt-file` | `CODE_CONVERGE_FIX_PROMPT_FILE` | `fix-findings.md` | `fix findings` |
342+
| Fix-findings prompt | `--fix-prompt-file` | `CODE_CONVERGE_FIX_PROMPT_FILE` | `fix-prompt-file` | `fix findings` |
341343
| CI-fix model | `--ci-fix-model` | `CODE_CONVERGE_CI_FIX_MODEL` | `ci-fix-model` | selected profile |
342344
| CI-fix reasoning effort | `--ci-fix-reasoning-effort` | `CODE_CONVERGE_CI_FIX_REASONING_EFFORT` | `ci-fix-reasoning-effort` | selected profile |
343-
| CI-fix prompt | `--ci-fix-prompt-file` | `CODE_CONVERGE_CI_FIX_PROMPT_FILE` | `fix-ci.md` | `Исправь CI` |
345+
| CI-fix prompt | `--ci-fix-prompt-file` | `CODE_CONVERGE_CI_FIX_PROMPT_FILE` | `ci-fix-prompt-file` | `Исправь CI` |
344346
| Review base override | `--review-base` | `CODE_CONVERGE_REVIEW_BASE` | `review-base` | discover intended base |
345347
| Diagnostic session-log directory | `--session-log-dir` | `CODE_CONVERGE_SESSION_LOG_DIR` | `session-log-dir` | `~/.code-converge/session-logs` |
346348
| Diagnostic session-log retention | `--session-log-retention` | `CODE_CONVERGE_SESSION_LOG_RETENTION` | `session-log-retention` | `24h` |
347349
| Disable diagnostic logging for this run | `--no-session-log` | — | — | disabled only when flag supplied |
348350

349-
`--finalize-model`, `--finalize-reasoning-effort`, and `--finalize-prompt-file`, their `CODE_CONVERGE_FINALIZE_*` environment variables, and `finalize-*` / `finalize.md` configuration files were removed in this release. Remove them during migration: obsolete environment or configuration-file settings cause an actionable configuration error rather than being ignored, because Codex no longer performs publication or CI polling.
351+
`--finalize-model`, `--finalize-reasoning-effort`, and `--finalize-prompt-file`, their `CODE_CONVERGE_FINALIZE_*` environment variables, and their YAML keys were removed in this release because Codex no longer performs publication or CI polling. Obsolete environment settings and YAML keys cause actionable configuration errors.
350352

351-
For example, a team can commit these files:
353+
For example, a team can commit one project configuration document and keep prompt content alongside it:
352354

353355
```text
354356
.code-converge/
355-
├── log-format
356-
├── heartbeat
357-
├── color
358-
├── mode
359-
├── review-model
360-
├── review-reasoning-effort
361-
├── review-base
362-
├── fix-model
363-
├── fix-reasoning-effort
364-
├── ci-fix-model
365-
├── ci-fix-reasoning-effort
366-
├── max-cycles
367-
├── max-ci-recoveries
368-
369-
├── ci-timeout
370-
├── session-log-dir
371-
├── session-log-retention
372-
├── fix-findings.md
373-
└── fix-ci.md
357+
├── config.yaml
358+
└── prompts/
359+
├── fix-findings.md
360+
└── fix-ci.md
361+
```
362+
363+
```yaml
364+
# <git-root>/.code-converge/config.yaml
365+
mode: best
366+
max-cycles: 3
367+
ci-timeout: 45m
368+
review-base: main
369+
fix-prompt-file: prompts/fix-findings.md
370+
ci-fix-prompt-file: prompts/fix-ci.md
374371
```
375372
376-
The same layout in `~/.code-converge/` sets user-level defaults. Environment variables are particularly useful in CI or temporary shell sessions:
373+
The same schema in `~/.code-converge/config.yaml` sets user-level defaults. Environment variables are particularly useful in CI or temporary shell sessions:
377374

378375
```sh
379376
CODE_CONVERGE_MAX_CYCLES=3 \

‎go.mod‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ require (
99
)
1010

1111
require golang.org/x/sys v0.21.0
12+
13+
require gopkg.in/yaml.v3 v3.0.1

‎go.sum‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
66
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
77
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
88
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
9+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
10+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
12+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

‎internal/config/config.go‎

Lines changed: 108 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package config
22

33
import (
44
"fmt"
5+
"io"
56
"os"
67
"os/exec"
78
"path/filepath"
89
"strconv"
910
"strings"
1011
"time"
12+
13+
"gopkg.in/yaml.v3"
1114
)
1215

1316
const (
@@ -91,6 +94,29 @@ type spec struct {
9194
promptFile bool
9295
}
9396

97+
type fileConfig map[string]string
98+
99+
type yamlFileConfig struct {
100+
LogFormat *string `yaml:"log-format"`
101+
Heartbeat *string `yaml:"heartbeat"`
102+
Color *string `yaml:"color"`
103+
Mode *string `yaml:"mode"`
104+
MaxCycles *int `yaml:"max-cycles"`
105+
MaxCIRecoveries *int `yaml:"max-ci-recoveries"`
106+
CITimeout *string `yaml:"ci-timeout"`
107+
ReviewModel *string `yaml:"review-model"`
108+
ReviewEffort *string `yaml:"review-reasoning-effort"`
109+
FixModel *string `yaml:"fix-model"`
110+
FixEffort *string `yaml:"fix-reasoning-effort"`
111+
FixPromptPath *string `yaml:"fix-prompt-file"`
112+
CIFixModel *string `yaml:"ci-fix-model"`
113+
CIFixEffort *string `yaml:"ci-fix-reasoning-effort"`
114+
CIFixPromptPath *string `yaml:"ci-fix-prompt-file"`
115+
ReviewBase *string `yaml:"review-base"`
116+
SessionLogDir *string `yaml:"session-log-dir"`
117+
SessionLogRetention *string `yaml:"session-log-retention"`
118+
}
119+
94120
type stageProfile struct {
95121
reviewModel, reviewEffort string
96122
fixModel, fixEffort string
@@ -183,10 +209,10 @@ func Load(cwd, home string, overrides Overrides) (Config, error) {
183209
{name: "review-reasoning-effort", file: "review-reasoning-effort", env: "CODE_CONVERGE_REVIEW_REASONING_EFFORT", def: profile.reviewEffort, builtIn: fast.reviewEffort, defSource: profileSource, override: overrides.ReviewEffort},
184210
{name: "fix-model", file: "fix-model", env: "CODE_CONVERGE_FIX_MODEL", def: profile.fixModel, builtIn: fast.fixModel, defSource: profileSource, override: overrides.FixModel},
185211
{name: "fix-reasoning-effort", file: "fix-reasoning-effort", env: "CODE_CONVERGE_FIX_REASONING_EFFORT", def: profile.fixEffort, builtIn: fast.fixEffort, defSource: profileSource, override: overrides.FixEffort},
186-
{name: "fix-prompt", file: "fix-findings.md", env: "CODE_CONVERGE_FIX_PROMPT_FILE", def: "fix findings", builtIn: "fix findings", defSource: SourceDefault, override: overrides.FixPromptPath, promptFile: true},
212+
{name: "fix-prompt", file: "fix-prompt-file", env: "CODE_CONVERGE_FIX_PROMPT_FILE", def: "fix findings", builtIn: "fix findings", defSource: SourceDefault, override: overrides.FixPromptPath, promptFile: true},
187213
{name: "ci-fix-model", file: "ci-fix-model", env: "CODE_CONVERGE_CI_FIX_MODEL", def: profile.ciFixModel, builtIn: fast.ciFixModel, defSource: profileSource, override: overrides.CIFixModel},
188214
{name: "ci-fix-reasoning-effort", file: "ci-fix-reasoning-effort", env: "CODE_CONVERGE_CI_FIX_REASONING_EFFORT", def: profile.ciFixEffort, builtIn: fast.ciFixEffort, defSource: profileSource, override: overrides.CIFixEffort},
189-
{name: "ci-fix-prompt", file: "fix-ci.md", env: "CODE_CONVERGE_CI_FIX_PROMPT_FILE", def: "Исправь CI", builtIn: "Исправь CI", defSource: SourceDefault, override: overrides.CIFixPromptPath, promptFile: true},
215+
{name: "ci-fix-prompt", file: "ci-fix-prompt-file", env: "CODE_CONVERGE_CI_FIX_PROMPT_FILE", def: "Исправь CI", builtIn: "Исправь CI", defSource: SourceDefault, override: overrides.CIFixPromptPath, promptFile: true},
190216
{name: "review-base", file: "review-base", env: "CODE_CONVERGE_REVIEW_BASE", def: "", builtIn: "", defSource: SourceDefault, override: overrides.ReviewBase},
191217
{name: "session-log-dir", file: "session-log-dir", env: "CODE_CONVERGE_SESSION_LOG_DIR", def: filepath.Join(home, ".code-converge", "session-logs"), builtIn: filepath.Join(home, ".code-converge", "session-logs"), defSource: SourceDefault, override: overrides.SessionLogDir},
192218
{name: "session-log-retention", file: "session-log-retention", env: "CODE_CONVERGE_SESSION_LOG_RETENTION", def: "24h", builtIn: "24h", defSource: SourceDefault, override: overrides.SessionLogRetention},
@@ -266,19 +292,6 @@ func rejectObsoleteFinalizeSettings(userDir, projectDir string) error {
266292
return fmt.Errorf("%s was removed; remove this obsolete Finalize-stage setting", name)
267293
}
268294
}
269-
for _, directory := range []struct {
270-
path string
271-
source string
272-
}{{userDir, "user"}, {projectDir, "project"}} {
273-
for _, name := range []string{"finalize-model", "finalize-reasoning-effort", "finalize.md"} {
274-
path := filepath.Join(directory.path, name)
275-
if _, err := os.Stat(path); err == nil {
276-
return fmt.Errorf("%s Finalize-stage setting %q was removed; delete it", directory.source, path)
277-
} else if !os.IsNotExist(err) {
278-
return fmt.Errorf("inspect obsolete Finalize-stage setting %q: %w", path, err)
279-
}
280-
}
281-
}
282295
return nil
283296
}
284297

@@ -359,18 +372,22 @@ func resolve(item spec, cwd, userDir, projectDir string) (string, Setting, error
359372
source = SourceEnv
360373
}
361374
for _, candidate := range []struct{ dir, source string }{{userDir, SourceUser}, {projectDir, SourceProject}} {
362-
path := filepath.Join(candidate.dir, item.file)
363-
content, err := os.ReadFile(path)
375+
config, err := readFileConfig(candidate.dir)
364376
if err != nil {
365-
if os.IsNotExist(err) {
366-
continue
367-
}
368-
return "", Setting{}, fmt.Errorf("read %s: %w", path, err)
377+
return "", Setting{}, err
378+
}
379+
content, ok := config[item.file]
380+
if !ok {
381+
continue
369382
}
370383
if item.promptFile {
371-
value, display = string(content), path
384+
prompt, path, err := readConfiguredPrompt(candidate.dir, content)
385+
if err != nil {
386+
return "", Setting{}, fmt.Errorf("%s from %s config: %w", item.name, candidate.source, err)
387+
}
388+
value, display = prompt, path
372389
} else {
373-
value, display = strings.TrimSpace(string(content)), strings.TrimSpace(string(content))
390+
value, display = strings.TrimSpace(content), strings.TrimSpace(content)
374391
}
375392
source = candidate.source
376393
}
@@ -389,6 +406,74 @@ func resolve(item spec, cwd, userDir, projectDir string) (string, Setting, error
389406
return value, Setting{Name: item.name, Value: value, Source: source, Default: item.builtIn, DisplayValue: display, DisplayDefault: displayDefault(item)}, nil
390407
}
391408

409+
func readFileConfig(dir string) (fileConfig, error) {
410+
path := filepath.Join(dir, "config.yaml")
411+
content, err := os.ReadFile(path)
412+
if err != nil {
413+
if os.IsNotExist(err) {
414+
return fileConfig{}, nil
415+
}
416+
return nil, fmt.Errorf("read %s: %w", path, err)
417+
}
418+
var decoded yamlFileConfig
419+
decoder := yaml.NewDecoder(strings.NewReader(string(content)))
420+
decoder.KnownFields(true)
421+
if err := decoder.Decode(&decoded); err == io.EOF {
422+
return fileConfig{}, nil
423+
} else if err != nil {
424+
return nil, fmt.Errorf("invalid YAML configuration %s: %w", path, err)
425+
}
426+
var extra yaml.Node
427+
if err := decoder.Decode(&extra); err != io.EOF {
428+
if err == nil {
429+
return nil, fmt.Errorf("invalid YAML configuration %s: only one YAML document is permitted", path)
430+
}
431+
return nil, fmt.Errorf("invalid YAML configuration %s: %w", path, err)
432+
}
433+
values := fileConfig{}
434+
setString := func(key string, value *string) {
435+
if value != nil {
436+
values[key] = *value
437+
}
438+
}
439+
setInt := func(key string, value *int) {
440+
if value != nil {
441+
values[key] = strconv.Itoa(*value)
442+
}
443+
}
444+
setString("log-format", decoded.LogFormat)
445+
setString("heartbeat", decoded.Heartbeat)
446+
setString("color", decoded.Color)
447+
setString("mode", decoded.Mode)
448+
setInt("max-cycles", decoded.MaxCycles)
449+
setInt("max-ci-recoveries", decoded.MaxCIRecoveries)
450+
setString("ci-timeout", decoded.CITimeout)
451+
setString("review-model", decoded.ReviewModel)
452+
setString("review-reasoning-effort", decoded.ReviewEffort)
453+
setString("fix-model", decoded.FixModel)
454+
setString("fix-reasoning-effort", decoded.FixEffort)
455+
setString("fix-prompt-file", decoded.FixPromptPath)
456+
setString("ci-fix-model", decoded.CIFixModel)
457+
setString("ci-fix-reasoning-effort", decoded.CIFixEffort)
458+
setString("ci-fix-prompt-file", decoded.CIFixPromptPath)
459+
setString("review-base", decoded.ReviewBase)
460+
setString("session-log-dir", decoded.SessionLogDir)
461+
setString("session-log-retention", decoded.SessionLogRetention)
462+
return values, nil
463+
}
464+
465+
func readConfiguredPrompt(configDir, value string) (string, string, error) {
466+
path := value
467+
if !filepath.IsAbs(path) {
468+
path = filepath.Join(configDir, path)
469+
}
470+
content, err := os.ReadFile(path)
471+
if err != nil {
472+
return "", "", fmt.Errorf("read prompt file %s: %w", path, err)
473+
}
474+
return string(content), path, nil
475+
}
476+
392477
func displayDefault(item spec) string {
393478
return displayValue(item.builtIn, item.promptFile)
394479
}

0 commit comments

Comments
 (0)