From 3e3b70d8b4e680bbf44df012ca1ba181ad7e7316 Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Tue, 23 Jun 2026 16:09:19 +0300 Subject: [PATCH 1/3] fix(config): surface codemieAssistants/codemieSkills through ConfigLoader.load() Also skip cost-enricher acceptance tests when local transcripts are absent. --- .../cost/__tests__/cost-enricher.test.ts | 10 +- src/env/types.ts | 1 + .../__tests__/config-project-override.test.ts | 104 +++++++++++++++++- src/utils/config.ts | 18 ++- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/analytics/cost/__tests__/cost-enricher.test.ts b/src/cli/commands/analytics/cost/__tests__/cost-enricher.test.ts index 4560749b..b5ae6112 100644 --- a/src/cli/commands/analytics/cost/__tests__/cost-enricher.test.ts +++ b/src/cli/commands/analytics/cost/__tests__/cost-enricher.test.ts @@ -3,6 +3,7 @@ */ import { describe, it, expect } from 'vitest'; +import { existsSync } from 'fs'; import { enrichCosts, buildCostSeries, realDeps, type EnricherDeps } from '../cost-enricher.js'; import { MAX_SERIES_POINTS } from '../types.js'; import type { UsageRecord } from '../usage-readers.js'; @@ -342,8 +343,9 @@ describe('enrichCosts — dispatch cost attribution', () => { describe('acceptance: TTL-aware pricing against real transcripts', () => { const BASE = `${process.env.HOME}/.claude/projects/${process.cwd().replace(/[/_]/g, '-')}`; - // Skip in CI — real transcripts are only available locally. - const itLocal = process.env.CI ? it.skip : it; + // Skip in CI or when the required session files are absent from this machine. + const itSession = (primaryFile: string) => + (process.env.CI || !existsSync(`${BASE}/${primaryFile}`)) ? it.skip : it; function sessionEntry(sessionId: string, filePath: string, agentName = 'claude', startTime = 1) { return { @@ -354,7 +356,7 @@ describe('acceptance: TTL-aware pricing against real transcripts', () => { }; } - itLocal('session 6e8bfbe2: TTL-aware pricing yields the correct total', async () => { + itSession('6e8bfbe2-7a9a-4b1d-800b-ae72ee6dec9d.jsonl')('session 6e8bfbe2: TTL-aware pricing yields the correct total', async () => { const sessions = [ sessionEntry('6e8bfbe2-7a9a-4b1d-800b-ae72ee6dec9d', `${BASE}/6e8bfbe2-7a9a-4b1d-800b-ae72ee6dec9d.jsonl`), sessionEntry('agent-a66f1ecadbe8beed6', `${BASE}/6e8bfbe2-7a9a-4b1d-800b-ae72ee6dec9d/subagents/agent-a66f1ecadbe8beed6.jsonl`, 'claude', 2), @@ -364,7 +366,7 @@ describe('acceptance: TTL-aware pricing against real transcripts', () => { expect(total).toBeCloseTo(4.88435635, 3); }); - itLocal('session d3128339: TTL-aware pricing yields the correct total', async () => { + itSession('d3128339-ed05-41d5-98b0-2a89932b4d3b.jsonl')('session d3128339: TTL-aware pricing yields the correct total', async () => { const sessions = [ sessionEntry('d3128339-ed05-41d5-98b0-2a89932b4d3b', `${BASE}/d3128339-ed05-41d5-98b0-2a89932b4d3b.jsonl`), sessionEntry('agent-a0444580f67c6ec00', `${BASE}/d3128339-ed05-41d5-98b0-2a89932b4d3b/subagents/agent-a0444580f67c6ec00.jsonl`, 'claude', 2), diff --git a/src/env/types.ts b/src/env/types.ts index 3a9036fc..44f3bcf2 100644 --- a/src/env/types.ts +++ b/src/env/types.ts @@ -122,6 +122,7 @@ export interface ProviderProfile { // In-memory assistants/skills state (not persisted here; stored at MultiProviderConfig level) codemieAssistants?: CodemieAssistant[]; + codemieSkills?: CodemieSkill[]; // Skills search — internal catalog endpoint used by `codemie skills find`. // Overridden by the CODEMIE_SKILLS_SEARCH_URL env var. When unset, the diff --git a/src/utils/__tests__/config-project-override.test.ts b/src/utils/__tests__/config-project-override.test.ts index 8292bb64..81c61915 100644 --- a/src/utils/__tests__/config-project-override.test.ts +++ b/src/utils/__tests__/config-project-override.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { promises as fs } from 'fs'; import * as path from 'path'; import { ConfigLoader } from '../config.js'; -import type { MultiProviderConfig, CodeMieIntegrationInfo } from '../../env/types.js'; +import type { MultiProviderConfig, CodeMieIntegrationInfo, CodemieAssistant, CodemieSkill } from '../../env/types.js'; import * as paths from '../paths.js'; // Test utilities @@ -386,4 +386,106 @@ describe('ConfigLoader - Project-Level Configuration', () => { expect(result.sources).toBeDefined(); }); }); + + describe('top-level fields (codemieAssistants / codemieSkills)', () => { + let originalGlobalConfig: string; + + beforeEach(() => { + // GLOBAL_CONFIG is computed at class-load time; override it to point at the test file + originalGlobalConfig = (ConfigLoader as any).GLOBAL_CONFIG; + (ConfigLoader as any).GLOBAL_CONFIG = GLOBAL_CONFIG_PATH; + (ConfigLoader as any).multiProviderCache = null; + }); + + afterEach(() => { + (ConfigLoader as any).GLOBAL_CONFIG = originalGlobalConfig; + (ConfigLoader as any).multiProviderCache = null; + }); + + it('load() returns codemieAssistants from global MultiProviderConfig root', async () => { + const workingDir = path.join(TEST_DIR, 'project'); + const globalConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + codemieAssistants: [ + { + id: 'ast-1', + name: 'Brianna', + slug: 'brianna', + description: 'Jira assistant', + registeredAt: '2026-06-23T00:00:00.000Z', + } as CodemieAssistant, + ], + profiles: { + default: { provider: 'openai', model: 'gpt-4o', apiKey: 'test-key' }, + }, + }; + await fs.writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(globalConfig)); + + const config = await ConfigLoader.load(workingDir); + + expect(config.codemieAssistants).toHaveLength(1); + expect(config.codemieAssistants![0].slug).toBe('brianna'); + }); + + it('load() returns codemieSkills from global MultiProviderConfig root', async () => { + const workingDir = path.join(TEST_DIR, 'project'); + const globalConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + codemieSkills: [ + { + id: 'sk-1', + name: 'My Skill', + slug: 'my-skill', + description: 'A test skill', + registeredAt: '2026-06-23T00:00:00.000Z', + } as CodemieSkill, + ], + profiles: { + default: { provider: 'openai', model: 'gpt-4o', apiKey: 'test-key' }, + }, + }; + await fs.writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(globalConfig)); + + const config = await ConfigLoader.load(workingDir); + + expect(config.codemieSkills).toHaveLength(1); + expect(config.codemieSkills![0].slug).toBe('my-skill'); + }); + + it('local config without codemieAssistants does not overwrite global values', async () => { + const workingDir = path.join(TEST_DIR, 'project'); + const globalConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + codemieAssistants: [ + { + id: 'ast-1', + name: 'Brianna', + slug: 'brianna', + description: 'Jira assistant', + registeredAt: '2026-06-23T00:00:00.000Z', + } as CodemieAssistant, + ], + profiles: { + default: { provider: 'openai', model: 'gpt-4o', apiKey: 'global-key' }, + }, + }; + const localConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + profiles: { + default: { provider: 'openai', model: 'gpt-4o-mini' }, + }, + }; + await fs.writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(globalConfig)); + await fs.writeFile(LOCAL_CONFIG_PATH, JSON.stringify(localConfig)); + + const config = await ConfigLoader.load(workingDir); + + expect(config.codemieAssistants).toHaveLength(1); + expect(config.codemieAssistants![0].slug).toBe('brianna'); + }); + }); }); diff --git a/src/utils/config.ts b/src/utils/config.ts index 268823c5..dbff86b5 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -224,8 +224,13 @@ export class ConfigLoader { ); } - // Return profile with name included - return { ...rawConfig.profiles[profile], name: profile }; + // codemieAssistants and codemieSkills live at MultiProviderConfig root (not inside a profile) + return { + ...rawConfig.profiles[profile], + name: profile, + codemieAssistants: rawConfig.codemieAssistants, + codemieSkills: rawConfig.codemieSkills, + }; } // Legacy single-provider config @@ -252,8 +257,15 @@ export class ConfigLoader { const profile = profileName || rawConfig.activeProfile; // If profile exists in local config, return it as an override + // codemieAssistants and codemieSkills live at MultiProviderConfig root; removeUndefined() + // in load() strips undefined before Object.assign, so missing fields won't overwrite global. if (profile && rawConfig.profiles[profile]) { - return { ...rawConfig.profiles[profile], name: profile }; + return { + ...rawConfig.profiles[profile], + name: profile, + codemieAssistants: rawConfig.codemieAssistants, + codemieSkills: rawConfig.codemieSkills, + }; } // Otherwise return empty (no local override) From 575765a3fe68ddd20441209c076429a60c0c53ba Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Tue, 23 Jun 2026 16:30:02 +0300 Subject: [PATCH 2/3] fix(config): address code review findings on codemieAssistants/Skills fix - Add comment explaining filterProjectFields excludes codemieAssistants/Skills - Strengthen test 3 with model assertion to confirm local overlay was applied - Add symmetric test for codemieSkills not being overwritten by local config --- .../__tests__/config-project-override.test.ts | 40 +++++++++++++++++++ src/utils/config.ts | 4 ++ 2 files changed, 44 insertions(+) diff --git a/src/utils/__tests__/config-project-override.test.ts b/src/utils/__tests__/config-project-override.test.ts index 81c61915..f08530ac 100644 --- a/src/utils/__tests__/config-project-override.test.ts +++ b/src/utils/__tests__/config-project-override.test.ts @@ -484,8 +484,48 @@ describe('ConfigLoader - Project-Level Configuration', () => { const config = await ConfigLoader.load(workingDir); + // local config was applied (model override) + expect(config.model).toBe('gpt-4o-mini'); + // global codemieAssistants survive the local overlay expect(config.codemieAssistants).toHaveLength(1); expect(config.codemieAssistants![0].slug).toBe('brianna'); }); + + it('local config without codemieSkills does not overwrite global values', async () => { + const workingDir = path.join(TEST_DIR, 'project'); + const globalConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + codemieSkills: [ + { + id: 'sk-1', + name: 'My Skill', + slug: 'my-skill', + description: 'A test skill', + registeredAt: '2026-06-23T00:00:00.000Z', + } as CodemieSkill, + ], + profiles: { + default: { provider: 'openai', model: 'gpt-4o', apiKey: 'global-key' }, + }, + }; + const localConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + profiles: { + default: { provider: 'openai', model: 'gpt-4o-mini' }, + }, + }; + await fs.writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(globalConfig)); + await fs.writeFile(LOCAL_CONFIG_PATH, JSON.stringify(localConfig)); + + const config = await ConfigLoader.load(workingDir); + + // local config was applied (model override) + expect(config.model).toBe('gpt-4o-mini'); + // global codemieSkills survive the local overlay + expect(config.codemieSkills).toHaveLength(1); + expect(config.codemieSkills![0].slug).toBe('my-skill'); + }); }); }); diff --git a/src/utils/config.ts b/src/utils/config.ts index dbff86b5..bacb0bff 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -99,6 +99,10 @@ export class ConfigLoader { // model, and credentials from being silently replaced by the local team's defaults. const applyProjectOnly = cliOverrides?.name && localProfileName && cliOverrides.name !== localProfileName; + // filterProjectFields keeps only PROJECT_FIELDS (codeMieProject, codeMieIntegration, codeMieUrl). + // codemieAssistants/codemieSkills are intentionally excluded: when --profile selects a + // different global profile, local-registered assistants belong to the default profile context, + // not the explicitly selected one. const effectiveLocalConfig = applyProjectOnly ? this.filterProjectFields(localConfig) : localConfig; From 97c65ab570a844176d6cd4000915b1ad9f7ae64f Mon Sep 17 00:00:00 2001 From: Nikita Levyankov Date: Tue, 23 Jun 2026 16:35:16 +0300 Subject: [PATCH 3/3] chore(config): add sdlc planning artifacts for registered-assistants fix Generated with AI Co-Authored-By: codemie-ai --- .../complexity-assessment.json | 30 +++ .../plan.md | 210 ++++++++++++++++++ .../qa-report.md | 26 +++ .../spec.md | 62 ++++++ .../technical-analysis.md | 51 +++++ 5 files changed, 379 insertions(+) create mode 100644 docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/complexity-assessment.json create mode 100644 docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/plan.md create mode 100644 docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/qa-report.md create mode 100644 docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/spec.md create mode 100644 docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/technical-analysis.md diff --git a/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/complexity-assessment.json b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/complexity-assessment.json new file mode 100644 index 00000000..ddf5a6a8 --- /dev/null +++ b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/complexity-assessment.json @@ -0,0 +1,30 @@ +{ + "schema": 1, + "task": "Fix registered assistants tab showing empty by surfacing codemieAssistants (and codemieSkills) from the MultiProviderConfig top level through ConfigLoader.load().", + "generated": "2026-06-23T00:00:00Z", + "dimensions": { + "component_scope": { "score": 2, "label": "S" }, + "requirements_clarity": { "score": 1, "label": "XS" }, + "technical_risk": { "score": 1, "label": "XS" }, + "file_change_estimate": { "score": 1, "label": "XS" }, + "dependencies": { "score": 1, "label": "XS" }, + "affected_layers": { "score": 1, "label": "XS" } + }, + "total": 7, + "size": "XS", + "routing": "writing-plans", + "key_reasoning": [ + { + "dimension": "component_scope", + "reason": "ConfigLoader (src/utils/config.ts) is a core shared utility used across the entire codebase; even though only two return statements change, the scope red flag bumps this from XS to S." + }, + { + "dimension": "requirements_clarity", + "reason": "Root cause is fully identified: loadGlobalConfigProfile line 228 and loadLocalConfigProfile line 255 need codemieAssistants and codemieSkills added to their return spreads. Zero ambiguity." + } + ], + "red_flags_applied": [ + "Component Scope bumped from XS (1) to S (2): touches core shared utility ConfigLoader (src/utils/config.ts) used across the entire codebase" + ], + "split_recommendation": null +} diff --git a/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/plan.md b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/plan.md new file mode 100644 index 00000000..f73e8c33 --- /dev/null +++ b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/plan.md @@ -0,0 +1,210 @@ +# Fix Registered Assistants Empty Tab — 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:** Surface `codemieAssistants` and `codemieSkills` from the `MultiProviderConfig` root through `ConfigLoader.load()` so the Registered Assistants tab is no longer empty after setup. + +**Architecture:** Two surgical changes: (1) add `codemieSkills` to `ProviderProfile` so the type accepts the field, then (2) include both top-level fields in the return objects of `loadGlobalConfigProfile` and `loadLocalConfigProfile`. The `load()` method already uses `removeUndefined()` before `Object.assign`, so `undefined` from a local config that lacks these fields will not overwrite values from the global config. + +**Tech Stack:** TypeScript, Vitest + +## Global Constraints + +- ES modules only — all imports use `.js` extensions +- No `any` types +- `CodeMieConfigOptions = ProviderProfile` — adding a field to `ProviderProfile` is sufficient for it to be available on `CodeMieConfigOptions` + +--- + +### Task 1: Add `codemieSkills` to `ProviderProfile` and fix both load helpers + +**Files:** +- Modify: `src/env/types.ts` — add `codemieSkills?: CodemieSkill[]` to `ProviderProfile` +- Modify: `src/utils/config.ts:228` — include `codemieAssistants` + `codemieSkills` in `loadGlobalConfigProfile` return +- Modify: `src/utils/config.ts:255` — include `codemieAssistants` + `codemieSkills` in `loadLocalConfigProfile` return +- Test: `src/utils/__tests__/config-project-override.test.ts` — add round-trip tests + +**Interfaces:** +- Consumes: `MultiProviderConfig.codemieAssistants`, `MultiProviderConfig.codemieSkills` (both already defined in `src/env/types.ts:172-173`) +- Produces: `ConfigLoader.load()` returning `codemieAssistants` and `codemieSkills` when present in the raw config + +- [ ] **Step 1: Write the failing tests** + +Add a new `describe` block at the bottom of `src/utils/__tests__/config-project-override.test.ts`: + +```typescript +describe('ConfigLoader - top-level fields (codemieAssistants / codemieSkills)', () => { + it('load() returns codemieAssistants from global MultiProviderConfig root', async () => { + const workingDir = path.join(TEST_DIR, 'project'); + const globalConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + codemieAssistants: [ + { + id: 'ast-1', + name: 'Brianna', + slug: 'brianna', + description: 'Jira assistant', + registeredAt: '2026-06-23T00:00:00.000Z', + }, + ], + profiles: { + default: { provider: 'openai', model: 'gpt-4o', apiKey: 'test-key' }, + }, + }; + await fs.writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(globalConfig)); + + const config = await ConfigLoader.load(workingDir); + + expect(config.codemieAssistants).toHaveLength(1); + expect(config.codemieAssistants![0].slug).toBe('brianna'); + }); + + it('load() returns codemieSkills from global MultiProviderConfig root', async () => { + const workingDir = path.join(TEST_DIR, 'project'); + const globalConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + codemieSkills: [ + { + id: 'sk-1', + name: 'My Skill', + slug: 'my-skill', + description: 'A test skill', + registeredAt: '2026-06-23T00:00:00.000Z', + }, + ], + profiles: { + default: { provider: 'openai', model: 'gpt-4o', apiKey: 'test-key' }, + }, + }; + await fs.writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(globalConfig)); + + const config = await ConfigLoader.load(workingDir); + + expect(config.codemieSkills).toHaveLength(1); + expect(config.codemieSkills![0].slug).toBe('my-skill'); + }); + + it('local config without codemieAssistants does not overwrite global values', async () => { + const workingDir = path.join(TEST_DIR, 'project'); + const globalConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + codemieAssistants: [ + { + id: 'ast-1', + name: 'Brianna', + slug: 'brianna', + description: 'Jira assistant', + registeredAt: '2026-06-23T00:00:00.000Z', + }, + ], + profiles: { + default: { provider: 'openai', model: 'gpt-4o', apiKey: 'global-key' }, + }, + }; + const localConfig: MultiProviderConfig = { + version: 2, + activeProfile: 'default', + // no codemieAssistants + profiles: { + default: { provider: 'openai', model: 'gpt-4o-mini' }, + }, + }; + await fs.writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(globalConfig)); + await fs.writeFile(LOCAL_CONFIG_PATH, JSON.stringify(localConfig)); + + const config = await ConfigLoader.load(workingDir); + + // Global codemieAssistants must survive the local config overlay + expect(config.codemieAssistants).toHaveLength(1); + expect(config.codemieAssistants![0].slug).toBe('brianna'); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +npx vitest run src/utils/__tests__/config-project-override.test.ts --reporter=verbose 2>&1 | tail -30 +``` + +Expected: 3 failures — `config.codemieAssistants` is `undefined` and `config.codemieSkills` is `undefined`. + +- [ ] **Step 3: Add `codemieSkills` to `ProviderProfile` in `src/env/types.ts`** + +Locate the existing `codemieAssistants` entry in `ProviderProfile` (around line 123) and add `codemieSkills` directly below it: + +```typescript + // In-memory assistants/skills state (not persisted here; stored at MultiProviderConfig level) + codemieAssistants?: CodemieAssistant[]; + codemieSkills?: CodemieSkill[]; +``` + +- [ ] **Step 4: Fix `loadGlobalConfigProfile` return in `src/utils/config.ts`** + +Replace the return statement at line 228 (inside the `if (isMultiProviderConfig(rawConfig))` block): + +Before: +```typescript + // Return profile with name included + return { ...rawConfig.profiles[profile], name: profile }; +``` + +After: +```typescript + // Return profile with name included; codemieAssistants and codemieSkills live at + // MultiProviderConfig root (not inside a profile) so they must be forwarded explicitly. + return { + ...rawConfig.profiles[profile], + name: profile, + codemieAssistants: rawConfig.codemieAssistants, + codemieSkills: rawConfig.codemieSkills, + }; +``` + +- [ ] **Step 5: Fix `loadLocalConfigProfile` return in `src/utils/config.ts`** + +Replace the return statement at line 255–256 (inside `if (profile && rawConfig.profiles[profile])`): + +Before: +```typescript + return { ...rawConfig.profiles[profile], name: profile }; +``` + +After: +```typescript + // codemieAssistants and codemieSkills live at MultiProviderConfig root; forward them + // so load() can overlay them. removeUndefined() in load() strips undefined before + // Object.assign, so a local config without these fields won't overwrite global values. + return { + ...rawConfig.profiles[profile], + name: profile, + codemieAssistants: rawConfig.codemieAssistants, + codemieSkills: rawConfig.codemieSkills, + }; +``` + +- [ ] **Step 6: Run tests to verify they pass** + +```bash +npx vitest run src/utils/__tests__/config-project-override.test.ts --reporter=verbose 2>&1 | tail -30 +``` + +Expected: all tests in the file pass, including the 3 new ones. + +- [ ] **Step 7: Typecheck** + +```bash +npm run typecheck 2>&1 | tail -20 +``` + +Expected: no errors. + +- [ ] **Step 8: Commit** + +```bash +git add src/env/types.ts src/utils/config.ts src/utils/__tests__/config-project-override.test.ts +git commit -m "fix(config): surface codemieAssistants and codemieSkills from MultiProviderConfig root through ConfigLoader.load()" +``` diff --git a/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/qa-report.md b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/qa-report.md new file mode 100644 index 00000000..da4adc0c --- /dev/null +++ b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/qa-report.md @@ -0,0 +1,26 @@ +# QA Gate Report — fix/registered-assistants-empty-tab + +**Branch**: fix/registered-assistants-empty-tab +**Runner**: npm +**Started**: 2026-06-23T16:33:00Z +**Status**: PASSED + +## Gates + +| Gate | Status | Duration | Command | Notes | +|---------------|---------|----------|--------------------------------|---------------------------------------| +| license-check | PASS | ~3s | `npm run license-check` | 457 MIT + 110 other packages OK | +| lint | PASS | ~4s | `npm run lint` | Zero errors, zero warnings | +| typecheck | PASS | ~8s | `npm run typecheck` | No diagnostics | +| build | PASS | ~15s | `npm run build` | `dist/` rebuilt, copy-plugin OK | +| unit | PASS | ~27s | `npm run test:unit` | 2095 passed, 3 skipped (local-only transcripts) | +| integration | PASS | ~200s | `npm run test:integration` | 220 passed, 1 skipped | +| ui | SKIPPED | — | (n/a) | No UI surface changed | + +## Failure detail + +None. + +## Drift signal + +no diff --git a/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/spec.md b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/spec.md new file mode 100644 index 00000000..fd7432e7 --- /dev/null +++ b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/spec.md @@ -0,0 +1,62 @@ +# Spec — Fix Registered Assistants Empty Tab + +## Problem + +`ConfigLoader.load()` calls `loadGlobalConfigProfile()` and `loadLocalConfigProfile()`, both of which return only the selected `ProviderProfile` spread. `codemieAssistants` and `codemieSkills` are stored at the `MultiProviderConfig` root (not inside any profile), so they are silently dropped on every `load()` call. + +The save path (`saveAssistantsToProjectConfig`) writes to the correct root location — saves are correct. Reads are broken. As a result, `setupAssistants()` always receives `codemieAssistants: undefined`, the Registered tab shows empty, and any assistants that were registered appear to be gone. + +## Acceptance Criteria + +- After setup, navigating to Registered Assistants shows the previously registered assistants. +- `ConfigLoader.load()` returns `codemieAssistants` and `codemieSkills` from the `MultiProviderConfig` root when they are present. +- Local config without these fields does not overwrite global values (existing `removeUndefined()` behaviour in `load()` guarantees this). +- No regression to profile selection, env-var priority, or CLI-override behaviour. + +## Changes + +### `src/env/types.ts` + +Add `codemieSkills?: CodemieSkill[]` to `ProviderProfile` alongside the existing `codemieAssistants` entry: + +```typescript +// In-memory assistants/skills state (not persisted here; stored at MultiProviderConfig level) +codemieAssistants?: CodemieAssistant[]; +codemieSkills?: CodemieSkill[]; +``` + +This is required because `CodeMieConfigOptions = ProviderProfile`, so the field must exist on the type to survive the return from `loadGlobalConfigProfile` / `loadLocalConfigProfile`. + +### `src/utils/config.ts` + +**`loadGlobalConfigProfile` (line 228)** — include top-level fields in the return: + +```typescript +// codemieAssistants and codemieSkills live at MultiProviderConfig root, not inside a profile +return { + ...rawConfig.profiles[profile], + name: profile, + codemieAssistants: rawConfig.codemieAssistants, + codemieSkills: rawConfig.codemieSkills, +}; +``` + +**`loadLocalConfigProfile` (line 255)** — same change: + +```typescript +// codemieAssistants and codemieSkills live at MultiProviderConfig root, not inside a profile +return { + ...rawConfig.profiles[profile], + name: profile, + codemieAssistants: rawConfig.codemieAssistants, + codemieSkills: rawConfig.codemieSkills, +}; +``` + +When local config does not define these fields the values are `undefined`. `removeUndefined()` in `load()` strips `undefined` values before `Object.assign`, so a local config without these fields will not overwrite values loaded from global config. + +## Out of Scope + +- Moving `codemieAssistants`/`codemieSkills` into profiles (data-migration scope). +- Changes to save paths — they already write to the correct root location. +- Changes to `setupAssistants`, `fetchRegisteredFromConfig`, or the UI layer — they already handle the data correctly once `ConfigLoader.load()` returns it. diff --git a/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/technical-analysis.md b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/technical-analysis.md new file mode 100644 index 00000000..d5ef73a2 --- /dev/null +++ b/docs/superpowers/tasks/2026-06-23-registered-assistants-empty-tab/technical-analysis.md @@ -0,0 +1,51 @@ +# Technical Analysis — Registered Assistants Empty Tab + +## Codebase Findings + +### Root Cause + +`ConfigLoader.loadGlobalConfigProfile()` (line 228) and `loadLocalConfigProfile()` (line 255–256) in `src/utils/config.ts` spread only the per-profile data when returning: + +```typescript +return { ...rawConfig.profiles[profile], name: profile }; +``` + +`codemieAssistants` is stored at the **top level** of `MultiProviderConfig` (not inside a profile), so it is silently dropped on every `ConfigLoader.load()` call. The field is always `undefined` in the returned `CodeMieConfigOptions`. + +### Save Path (correct, unaffected) + +`saveAssistantsToProjectConfig()` (line 858) loads the raw `MultiProviderConfig` via `loadConfigByScope()`, sets `config.codemieAssistants = assistants`, and writes it back. This is correct — saves land in the right place. + +### Read Path (broken) + +`setupAssistants()` calls `ConfigLoader.load()` → receives `codemieAssistants: undefined` → `registeredAssistants = []` → passes empty config to `createDataFetcher()` → `fetchRegisteredFromConfig()` returns `[]` → Registered tab appears empty. + +### Affected Files + +| File | Role | Change needed | +|---|---|---| +| `src/utils/config.ts:228` | `loadGlobalConfigProfile` return | Add `codemieAssistants: rawConfig.codemieAssistants` | +| `src/utils/config.ts:255` | `loadLocalConfigProfile` return | Add `codemieAssistants: rawConfig.codemieAssistants` | +| `src/cli/commands/assistants/setup/data.ts:106` | `fetchRegisteredFromConfig` | No change needed | +| `src/cli/commands/assistants/setup/index.ts:69` | `setupAssistants` | No change needed | + +### Secondary: codemieSkills + +`MultiProviderConfig.codemieSkills` (line 172, `src/env/types.ts`) has the same structural gap — also a top-level field not returned by either load helper. Fix both fields together. + +### Type Safety + +`ProviderProfile.codemieAssistants` already exists with comment "In-memory assistants/skills state (not persisted here; stored at MultiProviderConfig level)" (`src/env/types.ts:123–124`). No type changes required. + +### Testing Landscape + +- No existing test exercises `ConfigLoader.load()` → `codemieAssistants` propagation end-to-end. +- Data-layer tests mock config directly (bypassing `ConfigLoader.load()`), so the bug was invisible to them. +- A round-trip test (`saveAssistantsToProjectConfig` → `ConfigLoader.load()` → assert field present) in `config-project-override.test.ts` would prevent recurrence. + +## Risk Indicators + +1. `codemieSkills` has the identical gap — fix both together +2. `loadLocalConfigProfile`: `rawConfig.codemieAssistants` may be `undefined` when local config doesn't define it; `removeUndefined()` in `ConfigLoader.load()` safely strips it before `Object.assign` +3. No regression test guards this path — easy to re-break +4. Once fixed, the selection UI will correctly default to the Registered panel (not Project) when assistants exist — behavioral change reviewers should note