Merge remote-tracking branch 'origin/master' into fix/continuable-subagent-policy-inheritance

This commit is contained in:
Hypatia May
2026-08-10 19:40:14 +08:00
158 changed files with 2478 additions and 530 deletions

View File

@@ -599,6 +599,7 @@ function docSyncLeafGates(options: {
pnpmScript('agent-note-format', 'verify-agent-note-format', { label: 'agent note format' }),
pnpmScript('archived-agent-notes', 'verify-archived-agent-notes', { label: 'archived agent notes' }),
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }),
pnpmScript('skill-invocation-metadata', 'verify-skill-invocation-metadata', { label: 'skill invocation metadata' }),
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,53 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { collectSkillInvocationMetadataViolations } from './verify-skill-invocation-metadata.ts'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function fixtureRoot(): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-skill-invocation-metadata-'))
roots.push(root)
return root
}
function writeSkill(root: string, name: string, frontmatter: string, policy = ''): void {
const directory = join(root, '.agents/skills', name)
mkdirSync(join(directory, 'agents'), { recursive: true })
writeFileSync(join(directory, 'SKILL.md'), `---\nname: ${name}\ndescription: Test skill\n${frontmatter}---\n\nTest.\n`)
writeFileSync(
join(directory, 'agents/openai.yaml'),
`interface:\n display_name: "Test"\n${policy}`,
)
}
describe('cross-product skill invocation metadata gate', () => {
it('accepts aligned default and manual-only policies', () => {
const root = fixtureRoot()
writeSkill(root, 'default-skill', '')
writeSkill(
root,
'manual-skill',
'disable-model-invocation: true\nuser-invocable: true\n',
'policy:\n allow_implicit_invocation: false\n',
)
expect(collectSkillInvocationMetadataViolations(root)).toEqual([])
})
it('rejects either direction of a manual-only policy mismatch', () => {
const root = fixtureRoot()
writeSkill(root, 'claude-only', 'disable-model-invocation: true\n')
writeSkill(root, 'codex-only', '', 'policy:\n allow_implicit_invocation: false\n')
expect(collectSkillInvocationMetadataViolations(root)).toEqual([
'.agents/skills/claude-only: Claude Code manual-only=true but Codex manual-only=false',
'.agents/skills/codex-only: Claude Code manual-only=false but Codex manual-only=true',
])
})
})

View File

@@ -0,0 +1,122 @@
/**
* Keep Claude Code and Codex invocation metadata aligned for repository skills.
* @module scripts/verify-skill-invocation-metadata
*/
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { load } from 'js-yaml'
const ROOT = resolve(import.meta.dirname, '..')
/** Return an object-shaped YAML value, or undefined for every other shape. */
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: undefined
}
/** Parse a skill's YAML frontmatter as an object. */
function parseSkillFrontmatter(source: string): Record<string, unknown> {
const lines = source.split('\n')
if (lines[0] !== '---') throw new Error('SKILL.md must start with YAML frontmatter')
const end = lines.indexOf('---', 1)
if (end < 0) throw new Error('SKILL.md frontmatter is not closed')
const metadata = asRecord(load(lines.slice(1, end).join('\n')))
if (metadata === undefined) throw new Error('SKILL.md frontmatter must be a YAML object')
return metadata
}
/** Find repository skill directories that carry Codex product metadata. */
function skillDirectories(root: string): string[] {
const skillsRoot = resolve(root, '.agents/skills')
if (!existsSync(skillsRoot)) return []
return readdirSync(skillsRoot, { withFileTypes: true })
.filter(entry => entry.isDirectory() && existsSync(resolve(skillsRoot, entry.name, 'agents/openai.yaml')))
.map(entry => entry.name)
.sort()
}
/**
* Report cross-product invocation-policy mismatches for repository skills.
* @param root - Repository root containing `.agents/skills`.
* @returns diagnostics for malformed metadata or policies that expose a skill differently.
*/
export function collectSkillInvocationMetadataViolations(root: string): string[] {
const violations: string[] = []
for (const skill of skillDirectories(root)) {
const relativeRoot = `.agents/skills/${skill}`
const skillFile = resolve(root, relativeRoot, 'SKILL.md')
const openaiFile = resolve(root, relativeRoot, 'agents/openai.yaml')
if (!existsSync(skillFile)) {
violations.push(`${relativeRoot}: agents/openai.yaml has no sibling SKILL.md`)
continue
}
let frontmatter: Record<string, unknown>
let openai: Record<string, unknown>
try {
frontmatter = parseSkillFrontmatter(readFileSync(skillFile, 'utf8'))
}
catch (error) {
violations.push(`${relativeRoot}/SKILL.md: ${error instanceof Error ? error.message : String(error)}`)
continue
}
try {
const parsed = asRecord(load(readFileSync(openaiFile, 'utf8')))
if (parsed === undefined) throw new Error('agents/openai.yaml must be a YAML object')
openai = parsed
}
catch (error) {
violations.push(`${relativeRoot}/agents/openai.yaml: ${error instanceof Error ? error.message : String(error)}`)
continue
}
const disableModelInvocation = frontmatter['disable-model-invocation']
if (disableModelInvocation !== undefined && typeof disableModelInvocation !== 'boolean') {
violations.push(`${relativeRoot}/SKILL.md: disable-model-invocation must be a boolean`)
continue
}
const userInvocable = frontmatter['user-invocable']
if (userInvocable !== undefined && typeof userInvocable !== 'boolean') {
violations.push(`${relativeRoot}/SKILL.md: user-invocable must be a boolean`)
continue
}
const policy = asRecord(openai.policy)
const allowImplicitInvocation = policy?.allow_implicit_invocation
if (allowImplicitInvocation !== undefined && typeof allowImplicitInvocation !== 'boolean') {
violations.push(`${relativeRoot}/agents/openai.yaml: policy.allow_implicit_invocation must be a boolean`)
continue
}
const claudeManualOnly = disableModelInvocation === true
const codexManualOnly = allowImplicitInvocation === false
if (claudeManualOnly !== codexManualOnly) {
violations.push(
`${relativeRoot}: Claude Code manual-only=${String(claudeManualOnly)}`
+ ` but Codex manual-only=${String(codexManualOnly)}`,
)
}
if (claudeManualOnly && userInvocable === false) {
violations.push(`${relativeRoot}/SKILL.md: a manual-only skill must remain user-invocable`)
}
}
return violations
}
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
const skills = skillDirectories(ROOT)
const violations = collectSkillInvocationMetadataViolations(ROOT)
if (violations.length > 0) {
process.stderr.write('verify-skill-invocation-metadata: violations found:\n')
for (const violation of violations) process.stderr.write(` ${violation}\n`)
process.exit(1)
}
process.stdout.write(
`verify-skill-invocation-metadata: ${String(skills.length)} cross-product skill policy pair(s) aligned.\n`,
)
}