From 7ec93ba28f787986501ad5824a0e476a903ea7a1 Mon Sep 17 00:00:00 2001 From: Prakhar Pandey Date: Sat, 11 Jul 2026 14:04:31 +0530 Subject: [PATCH] fix: watch skill roots before they exist so first install loads without restart --- .../runtime/assembly/skills/disk-loader.ts | 12 ++++++++- .../src/runtime/assembly/skills/watcher.ts | 25 ++++++------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/x/packages/core/src/runtime/assembly/skills/disk-loader.ts b/apps/x/packages/core/src/runtime/assembly/skills/disk-loader.ts index 55f6ed16..3a5fc8a6 100644 --- a/apps/x/packages/core/src/runtime/assembly/skills/disk-loader.ts +++ b/apps/x/packages/core/src/runtime/assembly/skills/disk-loader.ts @@ -72,8 +72,18 @@ export function loadDiskSkills(): DiskSkill[] { } for (const entry of entries) { - if (!entry.isDirectory()) continue; const dir = path.join(root, entry.name); + // entry.isDirectory() is false for a symlink to a directory, so stat the + // full path (statSync follows symlinks) to accept symlinked skill folders. + try { + if (!fs.statSync(dir).isDirectory()) { + console.warn(`[disk-skills] Skipping '${entry.name}': not a directory at ${dir}`); + continue; + } + } catch (err) { + console.warn(`[disk-skills] Skipping '${entry.name}': cannot stat ${dir}:`, err); + continue; + } const skillFile = path.join(dir, "SKILL.md"); let raw: string; diff --git a/apps/x/packages/core/src/runtime/assembly/skills/watcher.ts b/apps/x/packages/core/src/runtime/assembly/skills/watcher.ts index e5925f52..6f21e21b 100644 --- a/apps/x/packages/core/src/runtime/assembly/skills/watcher.ts +++ b/apps/x/packages/core/src/runtime/assembly/skills/watcher.ts @@ -1,5 +1,4 @@ import chokidar, { type FSWatcher } from "chokidar"; -import fs from "node:fs"; import { SKILL_ROOTS } from "./disk-loader.js"; import { refreshDiskSkills } from "./index.js"; import { invalidateCopilotInstructionsCache } from "../copilot/instructions.js"; @@ -28,28 +27,18 @@ function scheduleReload(): void { /** * Start watching the disk skill roots (~/.rowboat/skills, ~/.agents/skills) for - * changes and live-reload the skill catalog. Idempotent. Missing directories - * are skipped so watching ~/.agents when it doesn't exist never errors. + * changes and live-reload the skill catalog. Idempotent. Roots that don't exist + * yet (e.g. ~/.agents/skills before the first install) are watched anyway — + * chokidar handles absent paths and emits events once they are created. */ export function startSkillsWatcher(): void { if (watcher) return; - const roots = SKILL_ROOTS.filter((root) => { - try { - return fs.statSync(root).isDirectory(); - } catch { - return false; // Directory absent (e.g. ~/.agents/skills) — nothing to watch. - } - }); - - if (roots.length === 0) { - console.log("[disk-skills] no skill directories present; watcher idle"); - return; - } - try { - watcher = chokidar.watch(roots, { + watcher = chokidar.watch(SKILL_ROOTS, { ignoreInitial: true, + // Only // one level deep — don't recurse into large skill repos. + depth: 1, awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50, @@ -67,7 +56,7 @@ export function startSkillsWatcher(): void { console.error("[disk-skills] watcher error:", error); }); - console.log(`[disk-skills] watching ${roots.length} skill director${roots.length === 1 ? "y" : "ies"} for changes`); + console.log(`[disk-skills] watching ${SKILL_ROOTS.length} skill director${SKILL_ROOTS.length === 1 ? "y" : "ies"} for changes`); } catch (err) { console.error("[disk-skills] failed to start watcher:", err); watcher = null;