From 0883fb91ec9f4803c9f82e442737e354adc8b825 Mon Sep 17 00:00:00 2001 From: "jason.woltje" Date: Thu, 25 Jun 2026 17:35:19 +0000 Subject: [PATCH] fix(wizard): resolve skills sync script path (#690) --- docs/scratchpads/B2-skills-sync-path.md | 34 +++++++++++++++++++ packages/mosaic/framework/defaults/README.md | 14 ++++---- .../mosaic/src/stages/finalize-skills.spec.ts | 33 ++++++++++++++---- packages/mosaic/src/stages/finalize.ts | 19 +++++++++-- 4 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 docs/scratchpads/B2-skills-sync-path.md diff --git a/docs/scratchpads/B2-skills-sync-path.md b/docs/scratchpads/B2-skills-sync-path.md new file mode 100644 index 0000000..763de39 --- /dev/null +++ b/docs/scratchpads/B2-skills-sync-path.md @@ -0,0 +1,34 @@ +# B2 — Fresh-install skills sync path + +## Problem + +Greenfield wizard on `next` reported: + +```text +Skills sync script not found at ~/.config/mosaic/bin/mosaic-sync-skills +Skills: install failed +``` + +## Diagnosis + +The framework install migration removed the legacy `~/.config/mosaic/bin/` directory and now installs framework helper scripts under: + +```text +~/.config/mosaic/tools/_scripts/ +``` + +`packages/mosaic/src/stages/finalize.ts` still resolved wizard helper scripts from `mosaicHome/bin`, so wizard-selected skills failed even though `mosaic-sync-skills` was present in the current framework layout. + +## Fix + +- Resolve framework helper scripts through `tools/_scripts/` first. +- Keep a legacy `bin/` fallback for pre-migration installs. +- Point missing-script warnings at the current `tools/_scripts` layout. +- Update the finalize skills test fixture to model the fresh framework layout. +- Update framework README examples from legacy `bin/` helper paths to `tools/_scripts/`. + +## Verification + +- Unit: `pnpm --filter @mosaicstack/mosaic test -- finalize-skills` +- Gates: `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, `pnpm build` +- Fresh path: ran `packages/mosaic/framework/install.sh` with a temp `MOSAIC_HOME` and `MOSAIC_SYNC_ONLY=1`; verified `tools/_scripts/mosaic-sync-skills` exists, legacy `bin/mosaic-sync-skills` does not, and the script installs a selected fake `lint` skill into Mosaic + Pi runtime skill directories. diff --git a/packages/mosaic/framework/defaults/README.md b/packages/mosaic/framework/defaults/README.md index 82f3085..1a598dc 100644 --- a/packages/mosaic/framework/defaults/README.md +++ b/packages/mosaic/framework/defaults/README.md @@ -118,8 +118,8 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim ├── TOOLS.md ← Machine-level tool reference (generated by mosaic init) ├── STANDARDS.md ← Machine-wide standards ├── guides/ ← Operational guides (E2E delivery, PRD, docs, etc.) -├── bin/ ← CLI tools (mosaic launcher, mosaic-init, mosaic-doctor, etc.) ├── tools/ ← Tool suites: git, orchestrator, prdy, quality, etc. +│ └── _scripts/ ← Framework helper scripts (sync skills, doctor, runtime links) ├── runtime/ ← Runtime adapters + runtime-specific references │ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks │ ├── codex/ ← instructions.md, RUNTIME.md @@ -194,15 +194,15 @@ bash tools/install.sh --ref v1.0 # Install from a specific git ref (--ref win The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories. ```bash -mosaic sync # Full sync (clone + link) -~/.config/mosaic/bin/mosaic-sync-skills --link-only # Re-link only +mosaic sync # Full sync (clone + link) +~/.config/mosaic/tools/_scripts/mosaic-sync-skills --link-only # Re-link only ``` ## Health Audit ```bash -mosaic doctor # Standard audit -~/.config/mosaic/bin/mosaic-doctor --fail-on-warn # Strict mode +mosaic doctor # Standard audit +~/.config/mosaic/tools/_scripts/mosaic-doctor --fail-on-warn # Strict mode ``` ## MCP Registration @@ -213,8 +213,8 @@ sequential-thinking MCP is required for Mosaic Stack. The installer registers it To verify or re-register manually: ```bash -~/.config/mosaic/bin/mosaic-ensure-sequential-thinking -~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check +~/.config/mosaic/tools/_scripts/mosaic-ensure-sequential-thinking +~/.config/mosaic/tools/_scripts/mosaic-ensure-sequential-thinking --check ``` ### Claude Code MCP Registration diff --git a/packages/mosaic/src/stages/finalize-skills.spec.ts b/packages/mosaic/src/stages/finalize-skills.spec.ts index 61e427c..e8dd48f 100644 --- a/packages/mosaic/src/stages/finalize-skills.spec.ts +++ b/packages/mosaic/src/stages/finalize-skills.spec.ts @@ -85,16 +85,16 @@ function makeConfigService(): ConfigService { describe('finalizeStage — skill installer', () => { let tmp: string; - let binDir: string; + let scriptsDir: string; let syncScript: string; beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'mosaic-finalize-')); - binDir = join(tmp, 'bin'); - mkdirSync(binDir, { recursive: true }); - syncScript = join(binDir, 'mosaic-sync-skills'); + scriptsDir = join(tmp, 'tools', '_scripts'); + mkdirSync(scriptsDir, { recursive: true }); + syncScript = join(scriptsDir, 'mosaic-sync-skills'); - // Default: script exists and succeeds + // Default: current framework layout has tools/_scripts and succeeds. writeFileSync(syncScript, '#!/usr/bin/env bash\necho ok\n', { mode: 0o755 }); spawnSyncMock.mockReturnValue({ status: 0, stdout: 'ok', stderr: '' }); }); @@ -122,10 +122,29 @@ describe('finalizeStage — skill installer', () => { const call = findSkillsSyncCall(); expect(call).toBeDefined(); + expect(call![1]).toEqual([join(tmp, 'tools', '_scripts', 'mosaic-sync-skills')]); const opts = call![2] as { env?: Record }; expect(opts.env?.['MOSAIC_INSTALL_SKILLS']).toBe('brainstorming:lint:systematic-debugging'); }); + it('falls back to legacy bin path for pre-migration installs', async () => { + rmSync(syncScript); + const legacyBinDir = join(tmp, 'bin'); + mkdirSync(legacyBinDir, { recursive: true }); + const legacySyncScript = join(legacyBinDir, 'mosaic-sync-skills'); + writeFileSync(legacySyncScript, '#!/usr/bin/env bash\necho ok\n', { mode: 0o755 }); + + const state = makeState(tmp, ['brainstorming']); + const p = buildPrompter(); + const config = makeConfigService(); + + await finalizeStage(p, state, config); + + const call = findSkillsSyncCall(); + expect(call).toBeDefined(); + expect(call![1]).toEqual([legacySyncScript]); + }); + it('skips the sync script entirely when no skills are selected', async () => { const state = makeState(tmp, []); const p = buildPrompter(); @@ -165,7 +184,9 @@ describe('finalizeStage — skill installer', () => { // spawnSync should NOT have been called for the skills script expect(findSkillsSyncCall()).toBeUndefined(); - expect(p.warn).toHaveBeenCalledWith(expect.stringContaining('not found')); + expect(p.warn).toHaveBeenCalledWith( + expect.stringContaining('tools/_scripts/mosaic-sync-skills'), + ); }); it('includes skills count in the summary when install succeeds', async () => { diff --git a/packages/mosaic/src/stages/finalize.ts b/packages/mosaic/src/stages/finalize.ts index 4835f6e..cde124b 100644 --- a/packages/mosaic/src/stages/finalize.ts +++ b/packages/mosaic/src/stages/finalize.ts @@ -7,8 +7,21 @@ import type { ConfigService } from '../config/config-service.js'; import type { WizardState } from '../types.js'; import { getShellProfilePath } from '../platform/detect.js'; +function frameworkScriptPath(mosaicHome: string, name: string): string { + const currentPath = join(mosaicHome, 'tools', '_scripts', name); + if (existsSync(currentPath)) return currentPath; + + // Backward-compatible fallback for pre-migration installs that still have bin/. + const legacyPath = join(mosaicHome, 'bin', name); + if (existsSync(legacyPath)) return legacyPath; + + // Return the current expected path so user-facing errors point at the layout + // installed by packages/mosaic/framework/install.sh. + return currentPath; +} + function linkRuntimeAssets(mosaicHome: string, skipClaudeHooks: boolean): void { - const script = join(mosaicHome, 'bin', 'mosaic-link-runtime-assets'); + const script = frameworkScriptPath(mosaicHome, 'mosaic-link-runtime-assets'); if (existsSync(script)) { try { spawnSync('bash', [script], { @@ -48,7 +61,7 @@ function syncSkills(mosaicHome: string, selectedSkills: string[]): SyncSkillsRes return { success: true, installedCount: 0 }; } - const script = join(mosaicHome, 'bin', 'mosaic-sync-skills'); + const script = frameworkScriptPath(mosaicHome, 'mosaic-sync-skills'); if (!existsSync(script)) { return { success: false, @@ -96,7 +109,7 @@ interface DoctorResult { } function runDoctor(mosaicHome: string): DoctorResult { - const script = join(mosaicHome, 'bin', 'mosaic-doctor'); + const script = frameworkScriptPath(mosaicHome, 'mosaic-doctor'); if (!existsSync(script)) { return { warnings: 0, output: 'mosaic-doctor not found' }; }