migrate-config
Migrate a cli-config.yaml to the current schema version. Deterministic, idempotent, and previewable.
the-loop migrate-config [--path ~/.the-loop/cli-config.yaml] [--dry-run]Why it exists
Some changes are breaking, and the owner's call was to make them breaking properly rather than carry a shadow override forever — "Let's make breaking changes. /upgrade should be able to handle it."
A breaking change is only as good as its migration, so four properties hold:
- Version the schema; do not sniff for keys. Detection is exact.
- Fail closed and loudly. A config still carrying a removed key makes the runtime refuse to start, naming the key, its replacement and the exact command. Silently ignoring a value you deliberately set would change your behaviour without telling you — much worse than an error.
- The migration is a deterministic key move, idempotent and previewable, that reports what it changed rather than rewriting the file quietly.
- It is tested both ways — an old config migrates to the expected new one, and the runtime refuses an un-migrated one.
What it migrates today
Current version: 0.4.0.
ghBinary → integrations.github.cli.binary (issue-109). The per-feature keys — declared separately under routing.control, routing.reactions and routing.announce — move to a single integrations.github.cli.binary. Three copies of one setting is exactly the duplication the integrations block exists to remove.
polling.stateFile removed (issue-128). The poller's ledger became one record per work item under state.root, so a file path has nothing left to point at.
webhooks.ghWebhook.routing → routing (issue-142). The block is promoted to the top level, unchanged key for key. It was never the receiver's: the poller reads every value in it verbatim for dispatch, and the-loop sessions reads it again. If your config happens to declare both the old and the new block, the top-level one wins key by key and the report names every value it dropped — never a merge, because unioning two authorizedUsers lists would quietly re-admit a login you had removed.
$ the-loop migrate-config --dry-run
migrated the CLI config:
· webhooks.ghWebhook.routing.control.ghBinary → integrations.github.cli.binary ('gh')
· webhooks.ghWebhook.routing.reactions.ghBinary → integrations.github.cli.binary ('gh')
· webhooks.ghWebhook.routing.announce.ghBinary → integrations.github.cli.binary ('gh')
· webhooks.ghWebhook.routing → routing (top level; it governs the poller too)
· version '0.1.0' → '0.4.0'
--- /home/you/.the-loop/cli-config.yaml (preview, not written) ---
version: 0.4.0
…Flags
| Flag | Default | Meaning |
|---|---|---|
--path | the resolved CLI config | Which file to migrate. |
--dry-run | off | Print the report and the migrated YAML without writing. |
--path defaults to whatever the normal resolution order finds.
Behaviour
- Reads with the raw loader, on purpose. The normal loader refuses an un-migrated config — and refusing to load the very file you are trying to migrate would be a locked door with the key inside.
- Keeps a backup. The pre-migration file is written to
<path>.bakbefore the new one replaces it. A breaking migration you cannot walk back from is a worse trade than one stray.bak. - Idempotent. Running it twice produces the same file and reports no second change:
config is already current; nothing to migrate. - Reports, then acts. The report is printed either way;
--dry-runstops there.
Exit codes
| Code | Meaning |
|---|---|
0 | Migrated, or already current |
2 | File not found, or it could not be parsed |
When the runtime refuses
this CLI config still declares `webhooks.ghWebhook.routing`. That block governs BOTH
ingresses — the poller reads it verbatim for dispatch — so it moved to a top-level
`routing` (issue-142). It is NOT being ignored: `routing.authorizedUsers` decides which
GitHub logins may drive your daemon, and quietly falling back to none is not a decision
this config gets to make for you. Run `/the-loop:upgrade-the-loop` to migrate./the-loop:upgrade-the-loop shells out to this command, so an upgrade never hand-edits a config the runtime already knows how to move. Running migrate-config directly does the same job.
An unset version is not refused
The gate refuses exactly two things: a removed key still present, and a config that declares a version older than the current one. A config with no version at all and no removed keys is accepted — there is nothing to move and nothing to lose, and a gate that stops a daemon over a missing bookkeeping key is a gate operators learn to route around.
See also
- Versioning and migration
- Integrations options — where the moved key now lives.