Skip to content

feat(config): read baton's config file - #29

Merged
Bakhomious merged 3 commits into
mainfrom
step/config
Nov 3, 2026
Merged

Bakhomious merged 3 commits into
mainfrom
step/config

Conversation

@Bakhomious

@Bakhomious Bakhomious commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Brief (lead)

Goal. baton's settings come from a config file of the user's, outside the repo (decision 16): each role's model, reasoning effort and AI credits cap, the orchestrator's extra tools, the round cap and the branch prefix, for every repo and per repo. This step is the package that reads and checks the file. The next one, config-cli, wires it into baton pane and baton feature start.

Scope.

  • internal/config, with Load(configDir, remote):

    • reads config.json in baton's folder under the user's config folder; a missing file gives baton's own settings;
    • the settings for every repo at the file's top, and a section per repo in repos, keyed by the remote's URL as its host and path (github.com/codedogapp/baton for its HTTPS, SSH and scp-like URLs); a local path keeps its path;
    • the repo's section lies over the top, and the top over baton's own, one setting at a time. A setting that a layer leaves out, or gives its zero value, keeps the one below. A list (extraTools) replaces the one below whole, and [] gives none;
    • checks the whole file, the other repos' sections too, so that a mistake shows at once, with the file's path, and not in a copilot that it stops.
  • The plan:

    • decision 49 (lead's call, flagged);
    • fact 31;
    • the Config paragraph, with an example;
    • decision 41's and risk 5's references;
    • rows 51 and 52, and the new config-cli row.
  • AGENTS.md: the lead merges Renovate's green pull requests, once their release notes show no breaking change and their pins match their tags, and asks about a red one or a major update (the user's OK of 2026-11-03, asked through nudge when Renovate opened its first two, chore(deps): update jdx/mise-action action to v5.2.6 #27 and chore(deps): update sonarsource/sonarqube-scan-action action to v8.4.1 #28, merged under it).

Out of scope.

  • Using the settings, repo.Remote and the README's config section: config-cli.
  • The gate command, plan approval, the pull request host, the base branch, and Jira's site and email: these come with their steps.

Acceptance criteria.

  • Without a file, or with empty sections, Load gives baton's own settings.
  • The file's top lies over baton's own, and the repo's section over the top, setting by setting. Sections of other repos change nothing.
  • The repo's section is found by any form of its remote's URL, and not by a URL that only looks like it.
  • Every refusal says where in the file it is and what baton wants; the first mistake is reported in a fixed order.
  • mise run check is green, and every probe is caught.

Engineer

  • internal/config/config.go:
    • Settings and Role, with their JSON names;
    • defaults(): Claude Opus 5.5 for every role, nudge's two tools for the orchestrator, 3 rounds, feature/;
    • Load: reads strictly with encoding/json/v2 (RejectUnknownMembers; v2 refuses duplicate names and matches names case-sensitively), checks, then lays the layers;
    • Settings.with and Role.with: cmp.Or per setting; a nil ExtraTools keeps the list below.
  • internal/config/check.go:
    • check: the top first, then the repos' sections in the order of their keys; each key must be in key's form;
    • Settings.check and Role.check, roles in name order:
      • known roles only;
      • no model or tool that starts with a dash, which copilot would read as a flag;
      • CLI 1.0.105's efforts;
      • a cap of 0 or at least 30;
      • a round cap not below 0;
      • extra tools for the orchestrator only (decision 10);
    • key: a URL's host and path, without scheme, user and port; git's scp-like [user@]host:path when no slash comes before the colon, with the user cut at the last @, as ssh does; then one trailing / and .git trimmed.
  • Tests:
    • config_test.go: the defaults, both layers, the remotes' forms (17 URLs and paths);
    • refusals_test.go: 16 exact refusals, 10 JSON errors (by substring), and a read error.

QA

  • mise run check green: lint 0 issues, internal/config 100% covered, 99.2% in total, govulncheck none.
  • Probes, in a copy of the tree: 53, all caught.
    • Load: a missing file as an error; the read error dropped; unknown members taken; no check, or no repos' check; the path left out of the error; no repo layer, or no top layer; the layers swapped; the repo found by its raw remote.
    • Defaults: no orchestrator tools; a role left out.
    • Layering: roles not layered; each cmp.Or turned around; extraTools replaced only when non-empty, or always.
    • Checks:
      • the top not checked;
      • repos and roles in reverse order;
      • an empty or non-canonical key taken;
      • a repo's section not checked;
      • names left out of errors;
      • the round cap's bound moved either way;
      • unknown roles, one-dash models and any effort taken;
      • an empty effort refused, and the list cut short;
      • the cap's minimum dropped, 0 refused, 30 refused;
      • tools for any role, [] taken, empty and dash names taken, only the first tool checked.
    • key: no :// test; the port kept; a slash before the colon ignored; the scp user kept, or cut at the first @; leading or trailing slashes, or .git, kept; the trims swapped.
  • Found on the way:
    • copilot's least cap. The spike found that copilot refuses a cap below 30 (fact 10), and the first draft took any cap of 0 or more. A probe with a bad --reasoning-effort, so copilot stopped before any session, showed:

      • --max-ai-credits parses as a number, fractions too;
      • 10 and 0 pass the parse, so copilot checks the 30 later, after its pane had picked up the work;
      • -1 reads as a flag.

      baton now refuses a cap that is neither 0 nor 30 or more (fact 31). A test takes exactly 30.

    • json/v2's errors change their wording. One run in ten failed the [] row: json/v2 picks "cannot" or "unable to" at random, so that nobody compares its errors as text. The row now matches "unmarshal JSON array"; 300 runs in a row pass.

    • An scp-like user with an @ (me@example.com@github.com:…) is cut at the last @, as ssh does; a test pins it.

  • The plan's example loads as the plan says: QA on Sonnet with a cap of 60 for every repo, 2 rounds and feat/ for baton's repo only.
  • No smoke run. Nothing in the binary reads the config yet; config-cli's smoke run will.
  • Leftovers: none (tmux servers, processes, temporary and go-build folders).

Review

  • Zero means unset. An explicit roundCap: 0 keeps the layer below, and the branch prefix can't be set empty. This is flagged in decision 49 for the end review; a pointer type per setting would tell "absent" from "zero" at the cost of every caller's nil checks.
  • The key keeps the URL's case. A remote with GitHub.com doesn't match a github.com section. Rare, and documented.
  • The efforts are pinned to CLI 1.0.105. A new level needs a new baton; risk 7 covers the CLI's pace.
  • Type names in JSON errors. json/v2 names the Go type (Go config.layers), next to the JSON pointer that says where the problem is. Acceptable for a config error.
  • Size. About 150 lines of code without comments, plus tests.

internal/config reads config.json in baton's folder under the user's
config folder, so nothing goes into a work repo: settings for every
repo at its top, and a section per repo in "repos", keyed by the
remote's URL as its host and path, so that a repo's HTTPS and SSH
clones share a section. The repo's section lies over the top, and the
top over baton's own settings, one setting at a time; a setting left
out, or given its zero value, keeps the one below (decision 49).

The settings so far: each role's model, reasoning effort and AI
credits cap, the orchestrator's extra tools (nudge's by default), the
round cap and the branch prefix. baton checks the whole file when it
reads it, the other repos' sections too, and refuses a member that it
doesn't know or that comes twice, a role that it doesn't know, a model
or a tool that starts with a dash, a reasoning effort or a cap that
copilot doesn't take (fact 31; fact 10's least cap, 30), a negative
round cap, extra tools for any role but the orchestrator, and a repo's
key in another form, with the form wanted.

The next step, config-cli, gives the settings to baton pane and baton
feature start; the other settings come with their steps.
@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

@Bakhomious
Bakhomious merged commit a463651 into main Nov 3, 2026
3 checks passed
@Bakhomious
Bakhomious deleted the step/config branch November 3, 2026 18:06
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.

1 participant