Skip to content

design(cli): per-tenant config files under ~/.pramana/tenants/*.json #245

Description

@sarath-soman

Motivation

Today `~/.pramana/config.json` is a single monolithic file holding a flat `{ name → source_dir }` map. This works, but the natural evolution — per-tenant knobs like FTS5 stemmer, embedder choice, source-dir excludes, watch/reload — will nest badly in one file. A file-per-tenant layout scales cleanly.

CLAUDE.md already hints at one of these knobs: "add fts5 stemmer config per tenant" (referenced as an example conventional-commit scope in the repo constitution).

This issue is design-first. Schema and migration are sketched below; implementation is gated on picking a concrete first knob (see "Ordering").

Target layout

```
~/.pramana/
├── daemon.json # global: port, log level, shared defaults for missing tenant fields
└── tenants/
├── expr-rust.json
├── kastrup.json
└── ggo-learned.json
```

`daemon.json` (new)

```json
{
"version": 1,
"port": 5111,
"log_level": "info",
"defaults": {
"embedder": "all-MiniLM-L6-v2",
"stemmer": "english"
}
}
```

Any field in `defaults` applies to tenants that don't override it.

`tenants/.json`

```json
{
"version": 1,
"name": "expr-rust",
"source_dir": "/abs/path",

// optional — omitted fields inherit from daemon.json.defaults
"embedder": "bge-small",
"stemmer": "english",
"exclude": ["drafts/**", "*.tmp"],
"watch": false,

// reserved escape hatch — if set, this tenant runs on a separate daemon
// on this port instead of the shared one. Default (absent) = shared daemon.
// NOT implemented in the first pass; reserved in the schema so future-us
// doesn't migrate again.
"daemon_port": null
}
```

Filename = `.json`. Validated against the existing `^[a-z][a-z0-9-]*$` rule (`config.rs:43-68`). Reserved names unchanged. The `name` field inside the file must match the filename stem (reject mismatch at load time).

Migration

```
~/.pramana/config.json → ~/.pramana/daemon.json + ~/.pramana/tenants/*.json
```

  • On first run of the new version, if `config.json` exists and `tenants/` doesn't:
    1. Read `config.json`
    2. Create `daemon.json` with defaults (port=5111, no embedder/stemmer defaults — leave inheritance to runtime)
    3. For each entry in `tenants`, write `tenants/.json` with `{ version: 1, name, source_dir }`
    4. Rename old file to `config.json.migrated` (do not delete — user should see it happened)
    5. Print one-line migration summary to stderr
  • If `tenants/` already exists, skip migration entirely.
  • No automatic back-migration. Forward-only.

CLI surface changes

The existing `pramana config` subcommands (`add`, `remove`, `list`, `path`) keep their shape. Behaviour becomes:

  • `config add [--embedder X] [--stemmer Y]` — writes `tenants/.json`; errors if the file exists
  • `config remove ` — deletes `tenants/.json` (not `daemon.json`)
  • `config list` — enumerates `tenants/*.json`; flags tenants whose `source_dir` doesn't exist (aligns with the silent-drop warnings issue)
  • `config path [--tenant ]` — prints `tenants/.json` path if flag given, else prints `~/.pramana/` root
  • New: `config edit ` — opens `tenants/.json` in `$EDITOR` (convenience for the knob flags not yet surfaced via `config add`)
  • New: `config set ` — update a single field in a tenant file (alternative to editor)

Daemon-side changes

  • Startup: enumerate `tenants/*.json`, merge each with `daemon.json.defaults`, mount.
  • Hot-reload: `/v1/reload/:name` re-reads `tenants/.json` + daemon defaults.
  • `daemon_port` field: on first pass, ignored with a warning if set ("per-tenant daemon ports not yet implemented; this tenant will use the shared daemon"). Reserved for a future issue that builds the per-tenant daemon supervisor.
  • Broken tenant files: log-and-skip per the silent-drop-warnings policy (separate issue). Don't crash the whole daemon on one malformed file.

Non-goals

  • Don't build per-tenant daemons now. The `daemon_port` field is a schema reservation; wiring it takes a supervisor, port conflict detection, per-tenant log routing, etc. File a follow-up when someone actually needs it.
  • Don't source-local the tenant config. Pattern "drop a `.pramana.json` in the source_dir, auto-discover" is tempting but adds ambiguity (which wins: local or central?) and complicates import/export. Keep it central for now.
  • Don't touch the engine. `TenantConfig` struct gains optional fields; `TenantManager::mount` keeps its shape.

Ordering

This issue is design-first. Do not implement speculatively. Pick it up when:

  1. A concrete need for the first per-tenant knob appears (most likely: FTS5 stemmer, per CLAUDE.md's example). That need drives the issue from design → implementation.
  2. Issues chore: retire pramana-tui; move to archive/ and cut from CLI + workspace #242 (retire TUI) and chore: retire pramana-mcp; move to archive/ and cut from CLI + workspace #244 (retire MCP) have landed, so the migration touches fewer call sites.

Until then, this issue stays open as the reference doc for the target shape. Label with `enhancement` + whatever "design" label fits.

Acceptance (when picked up)

  • `cargo test --workspace` covers migration from monolithic `config.json` to per-tenant layout
  • Existing `pramana config list/add/remove` continue to work; no breaking CLI change
  • Mounted tenants in a running daemon don't change their serve-side behaviour (same primitives, same output shape)
  • At least one concrete per-tenant knob is wired (the one that motivates the pickup)
  • `daemon_port` field present in schema but refused at runtime with a clear message
  • `pramana config list` marks broken tenants (coordinate with the silent-drop warnings issue — implement once, don't duplicate the detection logic)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions