Config upgrade
View as markdownYou 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:
- Detect. Sniffers run over every Shelbi-owned config surface (project
registration YAML,
statuses.yaml, workflows, the globalconfig.yaml, per-agentsettings.json, the Zen and instructions Markdown, agent manifests (agent.yaml), the workspace-settings template and per-workspacesettings.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. - Classify. Every finding is sorted into one of two buckets:
auto_heal: a deterministic, non-lossy, unambiguous rewrite exists. Examples: a renamed key (workers:becomesworkspaces:), a tag shorthand (tag: xbecomestags: [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-projectpr-template.md,changelog-template.md, andagents/_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, adisplay_name:that disagrees with bothname:and the project id, azen.danger_pathsthat sets bothextend:andoverride:, 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.
- Apply. The pass applies every
auto_healwrite-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_judgmentfindings are never applied automatically. - 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.jsonIt 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
upgradesubcommand and its flags. - The events log: where the
config-upgradedisclosure line lands. - Project config: the surfaces the pass sniffs,
including the
pr-template.mdit materializes for older projects.