fix(#1858): detect flat commands/gsd-<stem>.md layout in _resolveManifest
_resolveManifest only recognized the nested source layout (commands/gsd/*.md) and the installed-runtime skills layout (skills/gsd-<stem>/SKILL.md). A flat source install (Claude local project shape: commands/gsd-<stem>.md, no commands/gsd/ subdir) matched neither branch, so the manifest came back empty and resolveSurface materialized the full profile to an empty Set — silently reporting every skill-bearing capability as surfaced:false / enabled:false / active:false. The nyquist/code-review/security/ui verify:post and execute:post hooks never fired even with their workflow.* toggles on. Add a third branch: when commandsGsdDir is absent, scan dirname(commandsGsdDir) for gsd-<stem>.md files, strip the gsd- prefix, and build the same Map shape the nested loader produces (requires via shared parseRequires, companion _calls_agents_<stem> via shared parseCallsAgents). Falls through to the installed-skills branch when the flat dir has no gsd-*.md files (precedence: nested > flat-source > installed). Also export parseCallsAgents from install-profiles so capability-state reuses the SAME parser the nested loader uses (no drift; mirrors the existing parseRequires export+reuse pattern).
This commit is contained in:
@@ -46,7 +46,7 @@ const { loadConfig } = configLoaderMod;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import installProfilesMod = require('./install-profiles.cjs');
|
||||
const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires } = installProfilesMod;
|
||||
const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires, parseCallsAgents } = installProfilesMod;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import surfaceMod = require('./surface.cjs');
|
||||
@@ -384,22 +384,80 @@ function _loadInstalledSkillsManifest(configDir: string): Map<string, string[]>
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* #1858 — Build a skill dependency manifest from a FLAT commands/gsd-<stem>.md
|
||||
* source layout (the Claude local project install shape, where the `gsd-`
|
||||
* prefix is baked into each filename at the commands/ level and there is no
|
||||
* commands/gsd/ subdir). Strips the `gsd-` prefix so stems match the nested
|
||||
* loader's output (gsd-validate-phase.md → validate-phase, same as nested
|
||||
* validate-phase.md).
|
||||
*
|
||||
* Map shape is identical to loadSkillsManifest: each stem maps to its
|
||||
* `requires` deps (parsed via the same shared parseRequires) and carries a
|
||||
* companion `_calls_agents_<stem>` key (parsed via parseCallsAgents) so the
|
||||
* flat and nested paths cannot drift.
|
||||
*
|
||||
* Returns an empty Map when the parent directory does not exist or contains
|
||||
* no gsd-*.md files (so _resolveManifest can use size>0 as the "flat layout
|
||||
* present" signal and fall through to the installed-skills branch otherwise).
|
||||
*/
|
||||
function _loadFlatCommandsGsdManifest(commandsParentDir: string): Map<string, string[]> {
|
||||
const manifest = new Map<string, string[]>();
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(commandsParentDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return manifest;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
if (!entry.name.startsWith('gsd-')) continue;
|
||||
if (!entry.name.endsWith('.md')) continue;
|
||||
// Strip 'gsd-' prefix (4 chars) and '.md' suffix (3 chars) → stem.
|
||||
const stem = entry.name.slice(4, -3);
|
||||
if (!stem) continue;
|
||||
let content = '';
|
||||
try {
|
||||
content = fs.readFileSync(path.join(commandsParentDir, entry.name), 'utf8');
|
||||
} catch {
|
||||
// Unreadable file — register with empty deps + agents (parity with
|
||||
// loadSkillsManifest's readFileSync catch branch).
|
||||
}
|
||||
manifest.set(stem, content ? parseRequires(content) : []);
|
||||
manifest.set(`_calls_agents_${stem}`, content ? parseCallsAgents(content) : []);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the skill dependency manifest for capability-state resolution.
|
||||
*
|
||||
* Resolution order (fixes #1160 — installed-runtime capability surface):
|
||||
* 1. If commandsGsdDir exists, load from source (repo-checkout behavior).
|
||||
* 2. Otherwise, fall back to installed skills at configDir/skills/gsd-[stem]/SKILL.md.
|
||||
* Resolution order:
|
||||
* 1. If commandsGsdDir exists, load from the nested source layout
|
||||
* (repo-checkout behavior: <repo>/commands/gsd/*.md).
|
||||
* 2. #1858 — otherwise, if the flat source layout is present (gsd-<stem>.md
|
||||
* files in dirname(commandsGsdDir)), load from there. This is the Claude
|
||||
* local project install shape where commands/gsd/ does not exist but
|
||||
* commands/gsd-<stem>.md files do.
|
||||
* 3. #1160 — otherwise, fall back to installed skills at
|
||||
* configDir/skills/gsd-[stem]/SKILL.md.
|
||||
*
|
||||
* In an installed runtime the commands/gsd source tree is absent; only the
|
||||
* skills/ layout exists. Returning an empty manifest caused resolveSurface to
|
||||
* materialise the full-sentinel to an empty Set, making every capability appear
|
||||
* unsurfaced even when the skill was physically installed.
|
||||
* In an installed runtime both source trees are absent; only the skills/
|
||||
* layout exists. Returning an empty manifest caused resolveSurface to
|
||||
* materialise the full-sentinel to an empty Set, making every skill-bearing
|
||||
* capability appear unsurfaced even when the skill was physically installed
|
||||
* (#1160) or authored as a flat command file (#1858).
|
||||
*/
|
||||
function _resolveManifest(commandsGsdDir: string, configDir: string): Map<string, string[]> {
|
||||
if (fs.existsSync(commandsGsdDir)) {
|
||||
return loadSkillsManifest(commandsGsdDir);
|
||||
}
|
||||
// #1858: flat source layout — gsd-<stem>.md files at dirname(commandsGsdDir).
|
||||
// Only claim the flat branch when it actually has gsd-*.md files; otherwise
|
||||
// fall through to the installed-skills branch (a commands/ dir with no gsd
|
||||
// files must not shadow an installed skills/ tree).
|
||||
const flat = _loadFlatCommandsGsdManifest(path.dirname(commandsGsdDir));
|
||||
if (flat.size > 0) return flat;
|
||||
return _loadInstalledSkillsManifest(configDir);
|
||||
}
|
||||
|
||||
@@ -619,6 +677,7 @@ export = {
|
||||
// Exported for tests
|
||||
_resolveCommandsGsdDir,
|
||||
_loadInstalledSkillsManifest,
|
||||
_loadFlatCommandsGsdManifest,
|
||||
_resolveManifest,
|
||||
_isSafePropKey,
|
||||
};
|
||||
|
||||
@@ -875,6 +875,7 @@ export = {
|
||||
writeActiveProfile,
|
||||
// Shared internals
|
||||
parseRequires,
|
||||
parseCallsAgents,
|
||||
cleanupStagedSkills,
|
||||
// Back-compat / deprecated
|
||||
MINIMAL_SKILL_ALLOWLIST,
|
||||
|
||||
Reference in New Issue
Block a user