Files
ThothII/docs/superpowers/plans/2026-07-16-user-preference-bootstrap.md
T

4.4 KiB

User Preference Bootstrap Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Seed a new principal's private settings from complete legacy settings so a first session can configure and start Pi.

Architecture: buildApp continues to resolve settings through the principal-bound harness runner. When preferencesGet() returns an empty object, it derives the existing effective legacy settings from SETTINGS_FILE, persists them once via preferencesSet(), and returns that same object. A non-empty private object remains authoritative.

Tech Stack: TypeScript, Fastify, Vitest.

Global Constraints

  • Never overwrite a non-empty private preference object.
  • Persist only provider, model, thinking, and workspace values; no credentials enter preferences.
  • Keep storage failures fail-closed through the existing 503 route contract.
  • Follow TDD: observe the regression test fail before adding implementation.

Task 1: Bootstrap legacy settings for an empty private profile

Files:

  • Modify: backend/test/routes-settings.test.ts
  • Modify: backend/src/app.ts

Interfaces:

  • Consumes: ThtRunner.preferencesGet(): Promise<Record<string, unknown>>, ThtRunner.preferencesSet(settings): Promise<void>, loadSettings(config), and effectiveSettings(config, settings).

  • Produces: getSettings(principal): Promise<Settings> that returns a complete persisted profile for first-time principals.

  • Step 1: Write the failing regression tests

test("GET /settings seeds an empty private profile from complete legacy settings once", async () => {
  // Seed SETTINGS_FILE with local-qwen/qwen3.6-35b-a3b/low.
  // Make preferencesGet return {} and record preferencesSet calls.
  // Assert the first GET returns and persists all four settings, and a second GET does not write again.
});

test("GET /settings keeps a non-empty private profile authoritative", async () => {
  // Seed different legacy settings, return an existing private profile,
  // and assert no preferencesSet call occurs.
});
  • Step 2: Run the focused test file and verify RED

Run: cd backend && npx vitest run test/routes-settings.test.ts

Expected: the first test fails because the current resolver returns only defaults and never calls preferencesSet.

  • Step 3: Implement the minimal resolver change
const stored = await runner.preferencesGet();
if (Object.keys(stored).length === 0) {
  const seeded = effectiveSettings(config, loadSettings(config));
  await runner.preferencesSet(seeded);
  return seeded;
}
return effectiveSettings(config, stored);
  • Step 4: Run focused tests and TypeScript verification

Run: cd backend && npx vitest run test/routes-settings.test.ts && npx tsc --noEmit -p .

Expected: exit 0.

  • Step 5: Run backend regression suite

Run: cd backend && npx vitest run && npm run build

Expected: exit 0.

Task 2: Preserve DWH artifact ownership across the optional session-storage field

Files:

  • Modify: harness/tests/test_dwh_preprocess_job.py
  • Modify: harness/tht/jobs/dwh_pipeline.py

Interfaces:

  • Consumes: config_dwh_binding(cfg) and Pydantic's Config.model_dump(mode="json").

  • Produces: a DWH binding whose config_fingerprint excludes only session_storage.

  • Step 1: Write the failing regression test

def test_session_storage_does_not_change_the_dwh_artifact_binding(tmp_path):
    assert config_dwh_binding(config()) == config_dwh_binding(config(session_storage={...}))
  • Step 2: Run the focused test and verify RED

Run: cd harness && .venv/bin/pytest tests/test_dwh_preprocess_job.py::test_session_storage_does_not_change_the_dwh_artifact_binding -q

Expected: FAIL because the full configuration JSON currently includes session_storage.

  • Step 3: Implement the minimal DWH-only fingerprint
payload = cfg.model_dump(mode="json")
payload.pop("session_storage", None)
config_fingerprint = fingerprint(json.dumps(payload, separators=(",", ":"), ensure_ascii=False))
  • Step 4: Run focused and complete harness verification

Run: cd harness && .venv/bin/pytest tests/test_dwh_preprocess_job.py -q && .venv/bin/pytest -q && .venv/bin/ruff check tht/jobs/dwh_pipeline.py tests/test_dwh_preprocess_job.py

Expected: exit 0.