Config upgrade

View as markdown

You may be sitting on a Shelbi config authored by any past release, and no Shelbi config file carries a schema version. So instead of migrating by version number, Shelbi runs a content-based validate-and-upgrade pass that inspects each live config file for deprecated or legacy forms and either fixes them or flags them for a human decision.

When it runs

The pass runs automatically:

  • on hub start (across every registered project), and
  • on shelbi reload (for the reloaded project plus the shared global surfaces).

It is read-only about your intent and best-effort about failures: a detection or write error on one project is logged and swallowed so it can never stop the hub from serving or a reload from completing.

Detect, classify, apply

Each run is detect, classify, apply, re-detect, emit:

  1. Detect. Sniffers run over every Shelbi-owned config surface (project registration YAML, statuses.yaml, workflows, the global config.yaml, per-agent settings.json, the Zen and instructions Markdown, agent manifests (agent.yaml), the workspace-settings template and per-workspace settings.local.json, the shared agent preamble, and legacy-named sibling files), looking for known legacy forms. Detection never uses a version number, only the file's content, so it works for a config written by any past release.
  2. Classify. Every finding is sorted into one of two buckets:
    • auto_heal: a deterministic, non-lossy, unambiguous rewrite exists. Examples: a renamed key (workers: becomes workspaces:), a tag shorthand (tag: x becomes tags: [x]), a moved keybinding, a legacy status-id spelling, or a lifecycle-owned default a project is missing because it predates the file (today the per-project pr-template.md, changelog-template.md, and agents/_shared/preamble.md).
    • needs_judgment: ambiguous, potentially lossy, or the correct target is not mechanically determinable. Examples: an unknown top-level key that could be a typo or real data, a display_name: that disagrees with both name: and the project id, a zen.danger_paths that sets both extend: and override:, or a prose refresh in instructions a user may have customized. When a sniffer is unsure, a finding lands here rather than risk a lossy auto-heal.
  3. Apply. The pass applies every auto_heal write-back itself, then re-detects the residual so the emitted report reflects the post-heal state. A form healed this run is not re-surfaced. needs_judgment findings are never applied automatically.
  4. Emit. The residual is disclosed (see below).

What you see

On events.log. Each applied auto-heal is disclosed on its own line:

project=<id> config-upgrade code=<CODE> surface=<surface> file=<path> change=<old->new>

When findings remain after healing, each scope (a project id, or global for the shared global surfaces) also gets a summary line:

project=<id> config-upgrade auto_heal=<M> needs_judgment=<N>

Both counts describe the residual: M counts auto-heals that could not be applied this run, and N counts findings awaiting a decision. Only needs_judgment=<N> with N > 0 needs your attention. A fully clean scope gets no summary line.

The findings file. The residual is also recorded as a single hub-global JSON file (a snapshot of the boot pass; the orchestrator reads the live --needs-judgment channel instead):

~/.shelbi/config-upgrade-findings.json

It sits next to events.log. The pass removes the file when a config set is fully clean, so a stale file never lingers after the last deprecation is resolved. Each finding in it carries a stable id, its code (the deprecation kind, e.g. ZEN_DANGER_PATHS_CONFLICT), the scope, file, and location, a one-line message describing the legacy form, a proposed_fix, and for needs_judgment findings a rationale explaining what cannot be inferred mechanically.

Needs-judgment findings and the orchestrator

The needs_judgment findings are the channel the orchestrator surfaces to you. When it sees a needs_judgment > 0 line, it reads the live channel (shelbi config upgrade --needs-judgment) and folds the findings into its next reply: the surface, the legacy form, the proposed fix, and the rationale, then asks how you want to proceed. It never applies a needs-judgment finding you have not approved. Where an approved finding has a mechanical resolution (today only the danger_paths extend-plus-override conflict, ZEN_DANGER_PATHS_CONFLICT), it applies exactly that finding by its id with shelbi config upgrade --apply-finding <id>, reusing the same atomic write-back the auto-heal pass uses. For any other finding that command refuses, and the orchestrator edits the file directly and tells you what changed.

Inspecting it yourself

The same detector is exposed through shelbi config upgrade: run it read-only to preview what the boot pass would act on, --needs-judgment to read just the human- decision channel, --apply to perform the auto-heals on demand, or --apply-finding <id> to resolve a single approved finding that has a mechanical resolution.

Both the bare report and --needs-judgment detect live against the files on disk, so they always agree for the same on-disk state. Neither replays the config-upgrade-findings.json snapshot the boot pass wrote: a fix you apply by hand (or with --apply-finding) drops off the --needs-judgment list on the next run with no hub restart, and reintroducing a legacy form brings its finding back.

See also

  • shelbi config: the upgrade subcommand and its flags.
  • The events log: where the config-upgrade disclosure line lands.
  • Project config: the surfaces the pass sniffs, including the pr-template.md it materializes for older projects.