fix(skills): address QA round 3

- Fix Windows fd leak: close markerFd before unlinkSync in catch block
- Increase pbxproj read limit from 256KB to 1MB (SDKROOT appears after
  file references/build phases in large projects)
- Fix matchPacksForProject docstring: returns catalog order, not sorted
- Add legacy dir to skill telemetry (captureAvailableSkills, detectStaleSkills)
  so pre-migration skills appear in health/telemetry reports
This commit is contained in:
Derek Pearson 2026-03-22 06:22:23 -04:00
parent b442a55bff
commit a4de1d30c5
4 changed files with 16 additions and 7 deletions

View file

@ -495,10 +495,12 @@ function migrateSkillsToEcosystemDir(agentDir: string): void {
// Write migration info to the marker
try { writeFileSync(markerFd, `Migrated ${migrated} skill(s) to ${ecosystemDir} on ${new Date().toISOString()}\n`) } catch { /* non-fatal */ }
} catch {
// can't create ecosystem dir or read legacy dir — remove marker so we retry next launch
// can't create ecosystem dir or read legacy dir — close fd first (required on Windows
// where unlinkSync fails on open handles), then remove marker so we retry next launch
try { closeSync(markerFd); markerFd = -1 } catch { /* non-fatal */ }
try { unlinkSync(markerPath) } catch { /* non-fatal */ }
} finally {
try { closeSync(markerFd) } catch { /* non-fatal */ }
if (markerFd !== -1) { try { closeSync(markerFd) } catch { /* non-fatal */ } }
}
}

View file

@ -377,7 +377,7 @@ const XCODE_SUBDIRS = ["ios", "macos", "app", "apps"] as const;
* Returns deduplicated, canonical platform list (e.g. ["iphoneos"]).
*
* Reading the pbxproj is a lightweight regex scan no full plist parsing needed.
* We read at most 256 KB per file to keep detection fast.
* We read at most 1 MB per file to keep detection fast.
* Searches both the project root and common subdirectories (ios/, macos/, app/).
*/
function detectXcodePlatforms(basePath: string): XcodePlatform[] {
@ -397,7 +397,7 @@ function detectXcodePlatforms(basePath: string): XcodePlatform[] {
if (!entry.isDirectory() || !entry.name.endsWith(".xcodeproj")) continue;
const pbxprojPath = join(dir, entry.name, "project.pbxproj");
try {
const content = readBounded(pbxprojPath, 256 * 1024);
const content = readBounded(pbxprojPath, 1024 * 1024);
// Match SDKROOT = <value>; — both quoted and unquoted forms
const sdkRe = /SDKROOT\s*=\s*"?([a-z]+)"?\s*;/gi;
let m: RegExpExecArray | null;

View file

@ -399,7 +399,7 @@ export const GREENFIELD_STACKS: Array<{
/**
* Match project signals to relevant skill packs.
* Returns packs ordered by relevance (language match first, then file match).
* Returns packs in catalog order (not sorted by match type).
*/
export function matchPacksForProject(signals: ProjectSignals): SkillPack[] {
const matched = new Set<SkillPack>();

View file

@ -31,7 +31,12 @@ const activelyLoadedSkills = new Set<string>();
*/
export function captureAvailableSkills(): void {
const skillsDir = join(homedir(), ".agents", "skills");
availableSkills = listSkillNames(skillsDir);
const legacyDir = join(homedir(), ".gsd", "agent", "skills");
const names = listSkillNames(skillsDir);
// Include skills still in the legacy directory (pre-migration users)
const legacyNames = listSkillNames(legacyDir);
const all = new Set([...names, ...legacyNames]);
availableSkills = [...all];
activelyLoadedSkills.clear();
}
@ -100,7 +105,9 @@ export function detectStaleSkills(
// Check all installed skills, not just those with usage data
const skillsDir = join(homedir(), ".agents", "skills");
const installed = listSkillNames(skillsDir);
const legacyDir = join(homedir(), ".gsd", "agent", "skills");
const installedSet = new Set([...listSkillNames(skillsDir), ...listSkillNames(legacyDir)]);
const installed = [...installedSet];
for (const skill of installed) {
const lastTs = lastUsed.get(skill);