70cd530578
Closes the two follow-up caveats from the 2026-06-04
config-migration follow-up plan:
Caveat 1 — Duration pointer conversion
SLM.StartupTimeout and SLM.ClassifyTimeout are now *Duration
(pointer) instead of bare Duration. nil = "use documented
default" (5s and 0s respectively); *Duration(0) = explicit
zero. ResolvedSLMSection added to the mirror so consumers
read resolved time.Duration values instead of the raw
pointer. cmd/gnoma/main.go, profile_cmd, and the SLM
startup wiring all move through the mirror. The remaining
cosmetic encoder issue (startup_timeout = 0 / classify_timeout
= 0 written even with omitempty) is fixed because the
BurntSushi encoder now sees a nil pointer when the user
didn't set the field.
ResolvedSLMSection's RegisterAsArm mirrors the existing
nil→true default-substitution semantics from the field's
doc comment; the if-nil check in main.go is collapsed to
a direct read of resolved.SLM.RegisterAsArm.
Caveat 2 — `gnoma upgrade-config` (single-file mode)
New command that cleans a config file in place: drops
pointer-converted fields whose resolved value matches the
resolved default, leaves explicit-zero pointer fields
alone (the "explicit zero preserved" contract from Phase 1),
and writes the cleaned form atomically with a
.bak-YYYYMMDD-HHMMSS backup of the original. Idempotent —
a second run on the cleaned file reports "already clean,
nothing to do" without creating a second backup.
Cleaning rules per field type (encoded in internal/config/
upgrade.go::clean):
- pointer-converted fields: null iff resolved value
equals resolved default
- non-pointer string / map / slice / numeric / bool
fields: encoder's omitempty already handles them on
rewrite; the cleaner doesn't touch them
Diff output uses a simple line-by-line algorithm (added/
removed/neutral) via splitLines + a forward scan. Adequate
for the small config files gnoma produces. A proper Myers
diff could be vendored later — pmezard/go-difflib is
already a transitive dep in go.sum.
internal/config/load.go::ProjectConfigPath is now exported
so the CLI can default the upgrade target to the project
config when no path is given.
--dry-run runs the upgrade then restores the file from the
backup so the operation is truly side-effect-free.
Scope notes
Single-file mode only. --all-projects is deferred until the
project registry (Phase 2 of the 2026-05-24 plan) lands —
the follow-up doc calls this out as the natural next slice
and it can be added as a follow-up PR without touching
upgrade-config's core semantics.
No-op test cases (TestUpgrade_NoChangesOnAlreadyCleanFile,
TestUpgrade_KeepsExplicitUserValues, TestUpgrade_Keeps-
ExplicitZeroPointerFields) assert the "resolved view is
identical before and after" contract.
Test coverage
internal/config/upgrade_test.go: 10 tests (drops, keeps,
backup, idempotency, diff, edge cases)
internal/config/resolve_test.go: +3 tests for ResolvedSLM
internal/config/write_test.go: +1 test for the Duration
emission fix
cmd/gnoma/upgrade_config_cmd_test.go: 3 tests for the CLI
Refs: docs/superpowers/plans/2026-06-04-config-migration-followups.md