`gsd-tools config-set <key> null` — the "Clear" action documented in
settings-integrations.md / settings-advanced.md — previously fell through the
value parser as the literal STRING "null" and persisted it. Consequences:
"cleared" keys stayed set (config-get returned truthy "null"), and for secret
keys (brave_search/firecrawl/exa_search) a masked success line hid a truthy
4-char value on disk that integrations could pass along as a real credential.
There was also no unset/delete verb at all.
Fix: parse a bare `null` to JS null and short-circuit to a real UNSET that
DELETES the key from config.json — the semantic the docs already describe
("Remove the stored key" / "remove the key by setting it to null"). Deleting
(not persisting JSON null) is the correct clear: a persisted null is still a
present value consumers must special-case.
- src/config.cts:
- parse block: `else if (val === 'null') parsedValue = null;`
- cmdConfigSet: when parsedValue === null, short-circuit BEFORE the typed
per-key validator gauntlet (so clearing an enum/boolean/number key removes
it rather than being rejected) and before the project_code special-case;
mask the previous value for secret keys in the output.
- new `unsetConfigValue()` + `_unsetNestedValue()` mirroring setConfigValue/
_setNestedValue: same prototype-pollution guard, but never creates missing
intermediates and never prunes empty parents; returns { previousValue,
existed }. Unsetting a never-set key is an idempotent no-op success.
- tests/config.test.cjs: new suite covering non-secret routing key, secret key,
typed-enum-key bypass (context), idempotent unset, literal-"null"-on-disk
guard, the unset-path prototype-pollution guard (alert #26 parity), and a
4-segment deep-nested unset.
- tests/review-model-config.test.cjs: update the stale round-trip test that
codified the bug (asserted config-set null → config-get returns "null") to the
fixed contract — the model key is removed; the review workflow's
`[ -n "$VAR" ] && [ "$VAR" != "null" ]` guard handles the empty read as
"no override → reviewer default", same as the old "null" sentinel.
The 4 documented "Clear" flows (settings-integrations.md, settings-advanced.md)
were verified — their prose already describes removal, so the fix makes them
accurate rather than aspirational; no doc wording change required.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds two tests to review-model-config.test.cjs:
- isValidConfigKey accepts review.models.claude (schema validation)
- round-trip: config-set then config-get for review.models.claude
The dynamic key pattern (^review\.models\.[a-zA-Z0-9_-]+$), the workflow
model-read logic in review.md, and the CONFIGURATION.md docs were already
in place. Only the claude-specific test coverage was missing.
Closes#2688
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(review): add per-CLI model selection via config
- Add review.models.<cli> dynamic config keys to VALID_CONFIG_KEYS
- Update review.md to read model preferences via config-get at runtime
- Null/missing values fall back to CLI defaults (backward compatible)
- Add key suggestion for common typo (review.model)
- Update planning-config reference doc
Closes#1849
* fix(review): handle absent and null model config gracefully
Address PR #1859 review feedback from @trek-e:
1. Add `|| true` to all four config-get subshell invocations in
review.md so that an absent review.models.<cli> key does not
produce a non-zero exit from the subshell. cmdConfigGet calls
error() (process.exit(1)) when the key path is missing; the
2>/dev/null suppresses the message but the exit code was still
discarded silently. The || true makes the fall-through explicit
and survives future set -e adoption.
2. Add `&& [ "$VAR" != "null" ]` to all four if guards. cmdConfigSet
does not parse the literal 'null' as JSON null — it stores the
string 'null' — and cmdConfigGet --raw returns the literal text
'null' for that value. Without the extra guard the workflow would
pass `-m "null"` to the CLI, which crashes. The issue spec
documents null as the "fall back to CLI default" sentinel, so this
restores the contract.
3. Add tests/review-model-config.test.cjs covering all five cases
trek-e listed:
- isValidConfigKey accepts review.models.gemini (via config-set)
- isValidConfigKey accepts review.models.codex (via config-set)
- review.model is rejected and suggests review.models.<cli-name>
- config-set then config-get round-trip with a model ID
- config-set then config-get round-trip with null (returns "null")
Tests follow the node:test + node:assert/strict pattern from
tests/agent-skills.test.cjs and use runGsdTools from helpers.cjs.
Closes#1849