# Configuration and migrations

Chi stores module configuration outside Pi's settings:

- global: ~/.pi/agent/chi/config.json;
- project: .pi/chi/config.json in the current project, using Pi's
  CONFIG_DIR_NAME value.

The project file is read and written only when Pi reports that the current
project is trusted. Untrusted sessions ignore project data and reject project
writes.

## Storage format

Each file is a JSON object keyed by module ID:

~~~json
{
	"chi-example": {
		"schemaVersion": 2,
		"data": {
			"mode": "fast"
		}
	}
}
~~~

Missing entries and entries with empty data use the current Zod defaults and do
not cause a write merely because a module was discovered. Non-empty entries
carry their schema version so they can be migrated later.

## Loading

Chi reads global and trusted project entries independently, migrates older
non-empty entries, overlays project data on global data, and parses the merged
raw object once with the current schema. Defaults are therefore applied only
after the scope overlay.

Migrations are synchronous and receive their scope:

~~~ts
migrate(data, fromSchemaVersion, scope) {
	return {
		mode: data.mode === "quick" ? "fast" : "safe",
	};
}
~~~

Future versions, malformed entries, migration failures, non-object migration
results, and invalid merged data produce a module diagnostic. When migration
write-back is needed, each scope receives only its own migrated raw data; the
effective merged object is never written into both files.

## Updates

setConfigValue accepts an unknown value so the current Zod schema validates it.
The selected scope is written immediately, unrelated module entries are
preserved, and removing the last key removes that module's envelope. The
registry then updates its in-memory snapshot, computes effective field
changes, and awaits the owning module's optional callback:

~~~ts
onConfigChange(chi, current, changes) {
	state.config = current;
	// changes: [{ scope, key, previous, current }]
}
~~~

The callback receives the complete parsed effective configuration and is
responsible for synchronizing running state. It does not reinitialize the
module. Modules without the callback are silently updated and can read the
latest value with `chi.getConfig(id)`. Callback failures reject the update and
are reported to `/chi`; the persisted configuration remains the source of
truth. No Pi configuration event is emitted.

The `/chi` command exposes required top-level enum and defaulted string fields
in one tab per registered module. Module IDs lose only their leading `chi-`
prefix in tab labels. The custom screen is laid out as:

~~~text
Chi Configuration (project scope, toggle with p)

Settings (←/→):  example  [dependent]

→ enabled                 on

p toggle scope · x reset · Enter/Space change · Esc close
~~~

Enum rows cycle and persist on Enter or Space. For a string row, press Enter to
start editing, type the value, and press Enter again to persist it. Space is
part of the value while editing; Escape cancels. Confirming an empty value
removes the scoped key, applying the schema default or the effective global
value.

The editor starts in project mode when the project is trusted and at least one
loaded module has non-empty project data; otherwise it starts in global mode.
This is an initial display choice and is not persisted as a separate UI
preference. Use left/right arrows or `h`/`l` to change tabs and `p` to toggle
project mode. Project mode is rejected with an error notification when the
project is untrusted. A ready module with an empty schema shows `No parameters`;
a failed or blocked module shows the generic `Error loading settings` message.
Only the active scope is shown, and field labels never contain a `(project)`
suffix.

Global fallback choices display as `use default (<value>)`; project fallback
choices display as `use global (<effective global value>)`. Selecting a
fallback or pressing `x` on the selected row deletes the scoped key. These are
UI labels for semantic choices and never appear in JSON. Setting changes
persist immediately and notify the changed module's optional `onConfigChange`
callback. The list owns its selection and choice cycling, so it does not print
a second native settings help line.

## Duplicate modules

If two modules register the same module ID in one session, Chi marks both
conflicting registrations as failed and does not initialize either one. The
discovery pass continues so unrelated modules can still load. Chi Base prints
one error per duplicated ID with a hint that a global and local installation
may be clashing; remove one installation and restart Pi.
