Skip to content

Commit 2e6a8a0

Browse files
committed
Restructure docs and derive prompt identities from paths
1 parent 01d0da9 commit 2e6a8a0

File tree

89 files changed

+1047
-680
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1047
-680
lines changed

cli/src/inputs/input-agentskills-export-fallback.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,12 @@ describe('skill input plugin export fallback', () => {
5252
fs.mkdirSync(srcSkillDir, {recursive: true})
5353
fs.mkdirSync(distSkillDir, {recursive: true})
5454
fs.writeFileSync(path.join(srcSkillDir, 'skill.src.mdx'), `export default {
55-
name: 'demo',
5655
description: 'source export description',
5756
}
5857
5958
Source skill
6059
`, 'utf8')
6160
fs.writeFileSync(path.join(distSkillDir, 'skill.mdx'), `export default {
62-
name: 'demo',
6361
description: 'dist export description',
6462
}
6563

cli/src/inputs/input-agentskills.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ describe('skill input plugin', () => {
5353
const [skill] = result.skills ?? []
5454

5555
expect(result.skills?.length ?? 0).toBe(1)
56+
expect(skill?.skillName).toBe('demo')
5657
expect(skill?.content).toContain('Skill dist')
5758
expect(skill?.content).not.toContain('Skill source')
5859
expect(skill?.content).not.toContain('export const x = 1')
60+
expect(skill?.yamlFrontMatter?.name).toBe('demo')
5961
expect(skill?.yamlFrontMatter?.description).toBe('dist skill')
6062
expect(skill?.childDocs?.map(childDoc => childDoc.relativePath)).toEqual(['guide.mdx'])
6163
expect(skill?.childDocs?.[0]?.content).toContain('Guide dist')
@@ -139,7 +141,7 @@ describe('skill input plugin', () => {
139141
fs.mkdirSync(srcSkillDir, {recursive: true})
140142
fs.mkdirSync(distSkillDir, {recursive: true})
141143
fs.writeFileSync(path.join(srcSkillDir, 'skill.src.mdx'), '---\ndescription: src skill\n---\nSkill source', 'utf8')
142-
fs.writeFileSync(path.join(distSkillDir, 'skill.mdx'), '---\nname: demo\ndescription: dist skill\nscope: workspace\n---\nSkill dist', 'utf8')
144+
fs.writeFileSync(path.join(distSkillDir, 'skill.mdx'), '---\ndescription: dist skill\nscope: workspace\n---\nSkill dist', 'utf8')
143145

144146
const plugin = new SkillInputCapability()
145147
await expect(plugin.collect(createContext(tempWorkspace, createMockLogger()))).rejects.toThrow('Field "scope" must be "project" or "global"')
@@ -148,4 +150,30 @@ describe('skill input plugin', () => {
148150
fs.rmSync(tempWorkspace, {recursive: true, force: true})
149151
}
150152
})
153+
154+
it('warns and ignores authored skill name metadata', async () => {
155+
const warnings: string[] = []
156+
const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'tnmsc-skill-input-name-warning-test-'))
157+
const srcSkillDir = path.join(tempWorkspace, 'aindex', 'skills', 'demo')
158+
const distSkillDir = path.join(tempWorkspace, 'aindex', 'dist', 'skills', 'demo')
159+
160+
try {
161+
fs.mkdirSync(srcSkillDir, {recursive: true})
162+
fs.mkdirSync(distSkillDir, {recursive: true})
163+
fs.writeFileSync(path.join(srcSkillDir, 'skill.src.mdx'), '---\nname: custom-demo\ndescription: src skill\n---\nSkill source', 'utf8')
164+
fs.writeFileSync(path.join(distSkillDir, 'skill.mdx'), '---\nname: custom-demo\ndescription: dist skill\n---\nSkill dist', 'utf8')
165+
166+
const plugin = new SkillInputCapability()
167+
const result = await plugin.collect(createContext(tempWorkspace, createMockLogger(warnings)))
168+
const [skill] = result.skills ?? []
169+
170+
expect(skill?.skillName).toBe('demo')
171+
expect(skill?.yamlFrontMatter?.name).toBe('demo')
172+
expect(skill?.yamlFrontMatter?.description).toBe('dist skill')
173+
expect(warnings).toContain('SKILL_NAME_IGNORED')
174+
}
175+
finally {
176+
fs.rmSync(tempWorkspace, {recursive: true, force: true})
177+
}
178+
})
151179
})

cli/src/inputs/input-agentskills.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,35 @@ function mergeDefinedSkillMetadata(
111111
return merged
112112
}
113113

114+
function warnIgnoredSkillName(options: {
115+
readonly logger: ILogger
116+
readonly warnedDerivedNames?: Set<string>
117+
readonly sourcePath: string
118+
readonly authoredName: string
119+
readonly skillName: string
120+
}): void {
121+
const {logger, warnedDerivedNames, sourcePath, authoredName, skillName} = options
122+
if (warnedDerivedNames?.has(sourcePath) === true) return
123+
124+
warnedDerivedNames?.add(sourcePath)
125+
logger.warn(buildConfigDiagnostic({
126+
code: 'SKILL_NAME_IGNORED',
127+
title: 'Skill authored name is ignored',
128+
reason: diagnosticLines(
129+
`tnmsc ignores the authored skill name "${authoredName}" in favor of the directory-derived name "${skillName}".`
130+
),
131+
configPath: sourcePath,
132+
exactFix: diagnosticLines(
133+
'Remove the `name` field from the skill front matter or exported metadata.',
134+
'Rename the skill directory if you need a different skill name.'
135+
),
136+
details: {
137+
authoredName,
138+
derivedName: skillName
139+
}
140+
}))
141+
}
142+
114143
const MIME_TYPES: Record<string, string> = { // MIME types for resources
115144
'.ts': 'text/typescript',
116145
'.tsx': 'text/typescript',
@@ -557,16 +586,21 @@ async function createSkillPrompt(
557586
name: string,
558587
skillDir: string,
559588
skillAbsoluteDir: string,
589+
sourceSkillAbsoluteDir: string,
560590
ctx: InputCapabilityContext,
561591
mcpConfig?: SkillMcpConfig,
562592
childDocs: SkillPrompt['childDocs'] = [],
563593
resources: SkillPrompt['resources'] = [],
564594
seriName?: string | string[] | null,
565-
compiledMetadata?: Record<string, unknown>
595+
compiledMetadata?: Record<string, unknown>,
596+
warnedDerivedNames?: Set<string>
566597
): Promise<SkillPrompt> {
567598
const {logger, globalScope, fs} = ctx
568599

569600
const distFilePath = nodePath.join(skillAbsoluteDir, 'skill.mdx')
601+
const sourceFilePath = fs.existsSync(nodePath.join(sourceSkillAbsoluteDir, 'skill.src.mdx'))
602+
? nodePath.join(sourceSkillAbsoluteDir, 'skill.src.mdx')
603+
: distFilePath
570604
let rawContent = content
571605
let parsed: ReturnType<typeof parseMarkdown<SkillYAMLFrontMatter>> | undefined,
572606
distMetadata: Record<string, unknown> | undefined
@@ -593,6 +627,22 @@ async function createSkillPrompt(
593627
distMetadata
594628
) // Merge fallback export parsing with compiled metadata so empty metadata objects do not mask valid fields
595629

630+
const authoredNames = new Set<string>()
631+
const yamlName = parsed?.yamlFrontMatter?.['name']
632+
if (typeof yamlName === 'string' && yamlName.trim().length > 0) authoredNames.add(yamlName)
633+
const exportedName = exportMetadata.name
634+
if (typeof exportedName === 'string' && exportedName.trim().length > 0) authoredNames.add(exportedName)
635+
636+
for (const authoredName of authoredNames) {
637+
warnIgnoredSkillName({
638+
logger,
639+
sourcePath: sourceFilePath,
640+
authoredName,
641+
skillName: name,
642+
...warnedDerivedNames != null && {warnedDerivedNames}
643+
})
644+
}
645+
596646
const finalDescription = parsed?.yamlFrontMatter?.description ?? exportMetadata?.description
597647

598648
if (finalDescription == null || finalDescription.trim().length === 0) { // Strict validation: description must exist and not be empty
@@ -634,6 +684,7 @@ async function createSkillPrompt(
634684
content,
635685
length: content.length,
636686
filePathKind: FilePathKind.Relative,
687+
skillName: name,
637688
yamlFrontMatter: mergedFrontMatter,
638689
markdownAst: parsed?.markdownAst,
639690
markdownContents: parsed?.markdownContents ?? [],
@@ -694,6 +745,7 @@ export class SkillInputCapability extends AbstractInputCapability {
694745

695746
const flatSkills: SkillPrompt[] = []
696747
const reader = createLocalizedPromptReader(fs, pathModule, logger, globalScope)
748+
const warnedDerivedNames = new Set<string>()
697749
const skillArtifactCache = new Map<string, {
698750
readonly childDocs: SkillChildDoc[]
699751
readonly resources: SkillResource[]
@@ -749,12 +801,14 @@ export class SkillInputCapability extends AbstractInputCapability {
749801
name,
750802
distSkillDir,
751803
skillDistDir,
804+
pathModule.join(srcSkillDir, name),
752805
ctx,
753806
mcpConfig,
754807
childDocs,
755808
resources,
756809
void 0,
757-
metadata
810+
metadata,
811+
warnedDerivedNames
758812
)
759813
}
760814
}

cli/src/inputs/input-subagent.test.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@ describe('subagent input plugin', () => {
3434

3535
const srcFile = path.join(srcDir, 'demo.src.mdx')
3636
const distFile = path.join(distDir, 'demo.mdx')
37-
fs.writeFileSync(srcFile, '---\nname: demo\ndescription: src\n---\nSubAgent source', 'utf8')
38-
fs.writeFileSync(distFile, '---\nname: demo\ndescription: dist\n---\nexport const x = 1\n\nSubAgent dist', 'utf8')
37+
fs.writeFileSync(srcFile, '---\ndescription: src\n---\nSubAgent source', 'utf8')
38+
fs.writeFileSync(distFile, '---\ndescription: dist\n---\nexport const x = 1\n\nSubAgent dist', 'utf8')
3939

4040
const plugin = new SubAgentInputCapability()
4141
const result = await plugin.collect(createContext(tempWorkspace))
4242

4343
expect(result.subAgents?.length ?? 0).toBe(1)
4444
expect(result.subAgents?.[0]?.agentName).toBe('demo')
45+
expect(result.subAgents?.[0]?.canonicalName).toBe('demo')
4546
expect(result.subAgents?.[0]?.content).toContain('SubAgent dist')
4647
expect(result.subAgents?.[0]?.content).not.toContain('SubAgent source')
4748
expect(result.subAgents?.[0]?.content).not.toContain('export const x = 1')
@@ -64,8 +65,8 @@ describe('subagent input plugin', () => {
6465

6566
const srcFile = path.join(srcDir, 'boot.src.mdx')
6667
const distFile = path.join(distDir, 'boot.mdx')
67-
fs.writeFileSync(srcFile, '---\nname: boot\ndescription: qa boot src\n---\nSubAgent source', 'utf8')
68-
fs.writeFileSync(distFile, '---\nname: boot\ndescription: qa boot dist\n---\nSubAgent dist', 'utf8')
68+
fs.writeFileSync(srcFile, '---\ndescription: qa boot src\n---\nSubAgent source', 'utf8')
69+
fs.writeFileSync(distFile, '---\ndescription: qa boot dist\n---\nSubAgent dist', 'utf8')
6970

7071
const plugin = new SubAgentInputCapability()
7172
const result = await plugin.collect(createContext(tempWorkspace))
@@ -74,6 +75,7 @@ describe('subagent input plugin', () => {
7475
expect(result.subAgents?.length ?? 0).toBe(1)
7576
expect(subAgent?.agentPrefix).toBe('qa')
7677
expect(subAgent?.agentName).toBe('boot')
78+
expect(subAgent?.canonicalName).toBe('qa-boot')
7779
expect(subAgent?.content).toContain('SubAgent dist')
7880
expect(subAgent?.content).not.toContain('SubAgent source')
7981
}
@@ -94,8 +96,8 @@ describe('subagent input plugin', () => {
9496

9597
const srcFile = path.join(srcDir, 'demo.src.mdx')
9698
const distFile = path.join(distDir, 'demo.mdx')
97-
fs.writeFileSync(srcFile, '---\nname: demo\ndescription: src\n---\nSubAgent source', 'utf8')
98-
fs.writeFileSync(distFile, '---\nname: demo\ndescription: dist\n---\nexport const x = 1\n\nSubAgent dist', 'utf8')
99+
fs.writeFileSync(srcFile, '---\ndescription: src\n---\nSubAgent source', 'utf8')
100+
fs.writeFileSync(distFile, '---\ndescription: dist\n---\nexport const x = 1\n\nSubAgent dist', 'utf8')
99101

100102
const plugin = new SubAgentInputCapability()
101103
const result = await plugin.collect(createContext(tempWorkspace))
@@ -118,7 +120,7 @@ describe('subagent input plugin', () => {
118120
fs.mkdirSync(distDir, {recursive: true})
119121
fs.writeFileSync(
120122
path.join(distDir, 'demo.mdx'),
121-
'---\nname: demo\ndescription: dist only\n---\nDist only subagent',
123+
'---\ndescription: dist only\n---\nDist only subagent',
122124
'utf8'
123125
)
124126

@@ -127,6 +129,7 @@ describe('subagent input plugin', () => {
127129

128130
expect(result.subAgents?.length ?? 0).toBe(1)
129131
expect(result.subAgents?.[0]?.agentName).toBe('demo')
132+
expect(result.subAgents?.[0]?.canonicalName).toBe('demo')
130133
expect(result.subAgents?.[0]?.content).toContain('Dist only subagent')
131134
expect(result.subAgents?.[0]?.yamlFrontMatter?.description).toBe('dist only')
132135
}
@@ -143,7 +146,7 @@ describe('subagent input plugin', () => {
143146
fs.mkdirSync(srcDir, {recursive: true})
144147
fs.writeFileSync(
145148
path.join(srcDir, 'demo.src.mdx'),
146-
'---\nname: demo\ndescription: source only\n---\nSource only subagent',
149+
'---\ndescription: source only\n---\nSource only subagent',
147150
'utf8'
148151
)
149152

@@ -163,7 +166,7 @@ describe('subagent input plugin', () => {
163166
fs.mkdirSync(distDir, {recursive: true})
164167
fs.writeFileSync(
165168
path.join(distDir, 'demo.mdx'),
166-
'---\nname: demo\ndescription: dist only\nscope: workspace\n---\nDist only subagent',
169+
'---\ndescription: dist only\nscope: workspace\n---\nDist only subagent',
167170
'utf8'
168171
)
169172

@@ -174,4 +177,48 @@ describe('subagent input plugin', () => {
174177
fs.rmSync(tempWorkspace, {recursive: true, force: true})
175178
}
176179
})
180+
181+
it('warns and ignores authored subagent names', async () => {
182+
const tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'tnmsc-subagent-name-warning-test-'))
183+
const warnings: string[] = []
184+
const aindexDir = path.join(tempWorkspace, 'aindex')
185+
const srcDir = path.join(aindexDir, 'subagents', 'qa')
186+
const distDir = path.join(aindexDir, 'dist', 'subagents', 'qa')
187+
188+
try {
189+
fs.mkdirSync(srcDir, {recursive: true})
190+
fs.mkdirSync(distDir, {recursive: true})
191+
192+
fs.writeFileSync(path.join(srcDir, 'boot.src.mdx'), '---\nname: review-helper\ndescription: src\n---\nSubAgent source', 'utf8')
193+
fs.writeFileSync(path.join(distDir, 'boot.mdx'), '---\nname: review-helper\ndescription: dist\n---\nSubAgent dist', 'utf8')
194+
195+
const logger = {
196+
trace: () => {},
197+
debug: () => {},
198+
info: () => {},
199+
warn: diagnostic => warnings.push(diagnostic.code),
200+
error: () => {},
201+
fatal: () => {}
202+
}
203+
204+
const options = mergeConfig({workspaceDir: tempWorkspace})
205+
const plugin = new SubAgentInputCapability()
206+
const result = await plugin.collect({
207+
logger,
208+
fs,
209+
path,
210+
glob,
211+
userConfigOptions: options,
212+
dependencyContext: {}
213+
} as InputCapabilityContext)
214+
215+
const [subAgent] = result.subAgents ?? []
216+
expect(subAgent?.canonicalName).toBe('qa-boot')
217+
expect('name' in (subAgent?.yamlFrontMatter ?? {})).toBe(false)
218+
expect(warnings).toContain('SUBAGENT_NAME_IGNORED')
219+
}
220+
finally {
221+
fs.rmSync(tempWorkspace, {recursive: true, force: true})
222+
}
223+
})
177224
})

0 commit comments

Comments
 (0)