Migration troubleshooting
Common friction points when running qfg migrate. If you hit something not
listed here, file an issue on the cli repo with the output of
qfg migrate status and your MIGRATION_REPORT.md.
"I ran it twice and got a merge conflict"
Symptom: the second qfg migrate --from launch ... --push run fails with a
git push rejection or a merge-conflict error from the clone-and-stack step.
Why this happens. qfg migrate --push deliberately does not use
git push --mirror. It clones your cloud workspace, applies the migrator's
delta as new commits on top of existing history, and fast-forward pushes. If
you (or someone on your team) edited the same file in the Quonfig UI between
migrator runs, the cloud history diverged — the fast-forward push cannot
proceed without overwriting the UI edit.
This is working as intended. A silent clobber would destroy your UI changes.
How to resolve.
- Inspect the conflict. The migrator prints the conflicting file paths.
- Decide which version wins:
- UI edit wins — skip re-migrating that flag. Run with
--sinceset to just after the UI edit so the migrator does not re-process the change:qfg migrate --from launch --api-key $LAUNCH_KEY --workspace acme-prod \
--push --since 2026-04-18T12:00:00Z - Launch edit wins — manually revert the UI edit, then re-run the
migrator without
--since.
- UI edit wins — skip re-migrating that flag. Run with
- If you are mid-cutover and the UI is not yet a source of truth,
--resetrebuilds the workspace from Launch. This discards UI edits; use with care.
Prevention. During active migration, either freeze UI edits on the flags you are still syncing from Launch, or migrate Launch-managed flags into a subdirectory the UI does not touch.
"My flag did not appear"
Symptom: a flag exists in Launch but is missing from the migrated output directory or cloud workspace.
First, check MIGRATION_REPORT.md. Any flag that was seen but not
translated will be in the "lossy" or "unsupported" lists with a reason (e.g.,
"uses prerequisite-flag operator — not yet supported in Quonfig schema").
If the flag is listed there, that is the answer.
If it is not in the report, walk through:
- Delta cursor. If you are re-running, the migrator only fetches changes
after
.qf/import-state.json'slastProcessedAt. A flag created before that timestamp that has not been modified since will not re-appear. Run with--resetto force a full reimport:qfg migrate --from launch --api-key $LAUNCH_KEY --dir ./quonfig-config --reset - API-key scope. Confirm the Launch API key has read access to the
project and environment the flag lives in.
qfg migrate doctorverifies this. - Identifier collision. On case-insensitive filesystems (macOS default,
Windows), two Launch flags that differ only in case (
MyFlagvsmyflag) cannot coexist. The migrator fails loudly with both names printed — check stderr andMIGRATION_REPORT.mdfor a collision entry. - Environment filter. Flags scoped to an environment that is not in your workspace's environment list get skipped. Your environment mapping is in the report.
"My context shape is different"
Symptom: the same flag evaluates differently in Quonfig than in Launch for the same user. The flag exists and the rules look right, but the result is wrong.
Why this happens. Quonfig and Launch pass context to evaluation rules using
different shapes. A Launch rule that keys on user.email does not match
against a Quonfig context with email at the top level. The migrator rewrites
rules into Quonfig's shape, but your SDK call sites still pass the old
shape until you update them.
How to resolve.
- Run
qfg migrate my-codeto scan your codebase and rewrite call sites. The Claude skill reads.qf/identifier-map.json(written by the migrator) and knows which context keys to translate. - For anything the skill cannot auto-migrate — custom context adapters, unusual call patterns — it leaves a comment summary you can work through by hand.
- Verify with a small test matrix: pick 3-5 flags across distinct rule shapes (segment match, boolean, percentage rollout), evaluate them against a fixed user in both systems, and diff the outputs.
Gotcha: private attributes. Launch's privateAttributes concept (attributes
evaluated client-side but never sent to the server) has no Quonfig equivalent.
If your Launch setup uses them, they show up in the "unsupported" section of
MIGRATION_REPORT.md. You will need a different approach — usually evaluating
those rules server-side in your app before calling the SDK.
"The migrator says my working tree is dirty"
qfg migrate refuses to run against a --dir with uncommitted changes, to
protect the cursor file from getting clobbered.
- Commit or stash the pending changes, then re-run.
- Or pass a fresh
--dirand merge the output into your workspace by hand.
"I got a warning about dropped override sections"
Symptom: the migrator prints something like
Warning: Dropped N override section(s) referencing M env ID(s) that no
longer exist in the source: ...
Why this happens. The source system had rule overrides pointing at environments that have since been archived or deleted there. The migrator will not forge a mapping to an environment that no longer exists, so it drops those override sections and moves on.
How to resolve. Review the per-env breakdown the warning prints.
- If any of the listed envs is still in use in the source system, restore
it there and re-run
qfg migrate. - If the envs are genuinely dead (long-archived, renamed years ago), the warning is advisory — the migrated workspace is correct. The only data you lost was the override section that pointed nowhere.
"I got a warning about skipped invalid configs"
Symptom:
Warning: Skipped N invalid config(s): <key> (variant value type
<actual> does not match declared valueType <declared>), ...
…or, for a Zod schema that the migrator could not convert:
Warning: Skipped N invalid config(s): <key> (Schema conversion failed
for "<key>": Unsupported Zod expression: ...)
…or, for a schema config whose payload is not a Zod schema at all
(e.g. schemaType: "OPENAPI"):
Warning: Skipped N invalid config(s): <key> (Unsupported schema payload
for "<key>")
Why this happens. A source config is structurally invalid in a way that the migrator refuses to translate:
- Variant/valueType mismatch. A variant whose
value.typedoes not match the config'svalueType(e.g.valueType: "double"butvalue.type: "string"). The source allowed this from schema drift; Quonfig does not, because adoubleconfig that returns a string would break SDK callers. - Schema conversion failed. The Zod source for a schema-typed config
uses syntax the migrator cannot translate to JSON Schema (e.g. a
hand-written value that isn't a real Zod expression, or an exotic
refine()predicate — see "Schema-typed configs are auto-converted" below for the supported surface). - Unsupported schema payload. A schema config whose
schemaTypeis something other thanZOD(e.g.OPENAPI). qfg currently only ingests Zod-typed schemas.
Previously any one of these aborted the entire run on the first
offender. The migrator now skips just the bad config, lists it in
MIGRATION_REPORT.md under Skipped invalid configs, and continues
so the rest of your workspace still migrates.
How to resolve. Fix the offending config in the source system — correct the type, fix the Zod source, or convert the schema to Zod — and re-run. The rest of the workspace is unaffected; only the listed configs are missing.
"I got a warning about resolved cross-type duplicates"
Symptom:
Warning: Resolved N cross-type key collision(s): <key> (kept config,
dropped feature_flag), ...
Why this happens. A key existed in the source as both a config
and a feature flag at the same time. The source allowed the
collision; Quonfig requires globally unique keys across all types
(config, feature-flag, log-level). The migrator's tiebreaker is
config-wins, because configs are strictly more expressive than boolean
flags — a flag's behavior can always be expressed as a config, but not
vice versa.
How to resolve. The dropped sides are logged in MIGRATION_REPORT.md
so you can review what was lost. If the feature-flag side was actually
the live one:
- In the source system, delete whichever side should not have existed.
- Re-run
qfg migrate.
If the warning was benign (e.g. a renamed key where both sides happened to linger), no action is needed.
Schema-typed configs are auto-converted to JSON Schema
Configs whose valueType is schema (Reforge's Zod-schema storage) are
converted to JSON Schema Draft 2020-12
automatically during qfg migrate. You do not need to do anything
special — schema configs migrate like any other config.
Object schemas are emitted strict. The Quonfig authoring validator
requires additionalProperties: false on every object that declares
properties, so the converter always emits strict object schemas —
even when the original Zod source was open (the default z.object({…})
shape). If you intentionally want an open schema, edit the emitted JSON
Schema by hand after the migration; otherwise no action is needed.
The supported Zod surface area covers the common cases:
- Primitives:
string(with.email(),.uuid(),.datetime(),.min(),.max(),.regex()),number/int(with.min()/.max()),boolean,null,undefined .optional(),.nullable(),.default(),.describe()object,object().partial(),object().strict()/.strip()/.passthrough()array(with.min()/.max()/.length()),tupleenum,nativeEnum,literalunion,discriminatedUnion,intersectionrecord,map,set
If your schemas reach for the exotic end of Zod — z.function(),
z.promise(), z.lazy() with recursive references, custom
refine() / superRefine() predicates that cannot be expressed as
JSON Schema constraints — the migrator fails loudly and prints the
offending Zod source. File a bead so we can either add the surface or
document a workaround.
"I need to verify the migration actually worked"
- Run
qfg migrate statusto see counts and the current cursor. - Read
MIGRATION_REPORT.mdtop to bottom — the "must fix before cutover" checklist is not optional. - Pick 5-10 flags that cover your rule shapes and evaluate them against the same user in both systems. Any difference is a cutover blocker.
Shadow-evaluation tooling (qfg diff --against launch) is on the roadmap but
not in v1 — manual spot checks are the supported path today.
Still stuck?
qfg migrate doctor— runs preflight checks (API key,qfg login, workspace exists, SDK datadir support, clean git state) and emits a human-readable report. Add--jsonfor machine-readable output.- File an issue on the
clirepo with doctor output +MIGRATION_REPORT.md.