@orkestrel/scaffold 0.0.39 → 0.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/bin/main.js +8 -1
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/AGENTS.md +2 -2
  4. package/dist/host/agents/orchestration.md +33 -2
  5. package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +16 -16
  6. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +3 -3
  7. package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +1 -1
  8. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +7 -2
  9. package/dist/host/claude/agents/builder.md +2 -2
  10. package/dist/host/claude/agents/orkestrel.md +48 -48
  11. package/dist/host/claude/rules/application.md +6 -4
  12. package/dist/host/claude/rules/architecture.md +2 -0
  13. package/dist/host/claude/rules/documentation.md +6 -0
  14. package/dist/host/claude/rules/tests.md +11 -1
  15. package/dist/host/claude/rules/typescript.md +15 -1
  16. package/dist/host/claude/rules/workspace.md +43 -13
  17. package/dist/host/claude/rules/writing.md +125 -0
  18. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +10 -1
  19. package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +1 -1
  20. package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +1 -1
  21. package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +1 -1
  22. package/dist/host/configs/policy.ts +185 -0
  23. package/dist/host/dotfiles/oxlintrc.json +13 -1
  24. package/dist/host/dotfiles/prettierignore +3 -0
  25. package/dist/host/guides/scaffold.md +13 -7
  26. package/dist/host/manifest.json +11 -7
  27. package/dist/host/tests/config.test.ts +195 -4
  28. package/dist/host/tests/policy.test.ts +82 -0
  29. package/dist/host/tests/setupPolicy.ts +863 -21
  30. package/dist/src/core/index.cjs +68 -6
  31. package/dist/src/core/index.cjs.map +1 -1
  32. package/dist/src/core/index.d.cts +11 -9
  33. package/dist/src/core/index.d.ts +11 -9
  34. package/dist/src/core/index.js +68 -6
  35. package/dist/src/core/index.js.map +1 -1
  36. package/package.json +2 -2
  37. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +0 -129
@@ -8,11 +8,12 @@ import {
8
8
  writeFileSync,
9
9
  } from 'node:fs'
10
10
  import { tmpdir } from 'node:os'
11
- import { basename, dirname, join } from 'node:path'
11
+ import { basename, dirname, join, matchesGlob } from 'node:path'
12
12
  import * as ts from 'typescript'
13
13
 
14
14
  /** A rule the fleet placement instrument can decide from syntax and a file path. */
15
15
  export type PolicyRule =
16
+ | 'bridge'
16
17
  | 'class'
17
18
  | 'constant'
18
19
  | 'data'
@@ -23,6 +24,7 @@ export type PolicyRule =
23
24
  | 'mirror'
24
25
  | 'parser'
25
26
  | 'skill'
27
+ | 'suppression'
26
28
  | 'type'
27
29
 
28
30
  /** One TypeScript source supplied to the placement instrument. */
@@ -45,16 +47,72 @@ export interface PolicyControl {
45
47
  readonly membership: string
46
48
  readonly rule: PolicyRule
47
49
  readonly files: readonly PolicySource[]
50
+ readonly message?: string
51
+ }
52
+
53
+ /** Describes a contained temporary directory owned by one vendored test. */
54
+ export interface PolicyScratchInterface {
55
+ readonly path: string
56
+ write(target: string, text: string): void
57
+ destroy(): void
58
+ }
59
+
60
+ /**
61
+ * Creates a contained temporary directory owned by one vendored test.
62
+ *
63
+ * @param options - The temporary directory name prefix.
64
+ * @returns The owned scratch directory.
65
+ */
66
+ export function createPolicyScratch(options: { readonly prefix: string }): PolicyScratchInterface {
67
+ const root = mkdtempSync(join(tmpdir(), options.prefix))
68
+ return {
69
+ path: root,
70
+ write(target, text) {
71
+ const normalized = normalizePolicyPath(target)
72
+ const segments = normalized.split('/')
73
+ if (
74
+ normalized === '' ||
75
+ normalized.startsWith('/') ||
76
+ segments.some((segment) => segment === '..')
77
+ ) {
78
+ throw new Error('Scratch target must stay within its root')
79
+ }
80
+ const path = join(root, ...segments)
81
+ mkdirSync(dirname(path), { recursive: true })
82
+ writeFileSync(path, text, 'utf8')
83
+ },
84
+ destroy() {
85
+ rmSync(root, { recursive: true, force: true })
86
+ },
87
+ }
88
+ }
89
+
90
+ /** Parsed skill frontmatter and the exact scalar source used for bridge comparison. */
91
+ export interface SkillFrontmatter {
92
+ readonly keys: readonly string[]
93
+ readonly name: string | undefined
94
+ readonly description: string | undefined
95
+ readonly source: {
96
+ readonly name: string | undefined
97
+ readonly description: string | undefined
98
+ }
48
99
  }
49
100
 
50
101
  /** The directory whose immediate child directories form the complete skill family. */
51
102
  export const SKILL_FAMILY_ROOT = '.agents/skills'
52
103
 
104
+ /** The directory whose immediate child directories form the Claude skill bridge family. */
105
+ export const SKILL_BRIDGE_ROOT = '.claude/skills'
106
+
53
107
  /** Minimal valid skill text for physical family controls. */
54
- export const SKILL_POLICY_TEXT = '# Skill\n'
108
+ export const SKILL_POLICY_TEXT =
109
+ '---\nname: sample\ndescription: Use this skill for a policy fixture.\n---\n\n# Skill\n'
55
110
 
56
111
  /** Skill text naming one reference for physical family controls. */
57
- export const SKILL_REFERENCE_TEXT = '# Skill\n\nRead references/example.md.\n'
112
+ export const SKILL_REFERENCE_TEXT = `${SKILL_POLICY_TEXT}\nRead references/example.md.\n`
113
+
114
+ /** Minimal valid provider bridge text for physical bridge controls. */
115
+ export const SKILL_BRIDGE_TEXT = `${SKILL_POLICY_TEXT}\nRead \`.agents/skills/sample/SKILL.md\`.\n`
58
116
 
59
117
  /** Canonical skill metadata whose three values each carry YAML's escaped apostrophe. */
60
118
  export const SKILL_APOSTROPHE_METADATA =
@@ -172,6 +230,37 @@ export const POLICY_TESTS_MODULE_GLOB = `tests/**/${POLICY_TESTS_MODULE_PREFIX}*
172
230
  /** The mirrored module-test population inspected under either workspace axis. */
173
231
  export const POLICY_TEST_GLOB = 'tests/{app,src}/**/*.test.ts'
174
232
 
233
+ // Compose suppression tokens so the instrument does not report its own definitions or controls.
234
+ export const POLICY_SUPPRESSION_DIRECTIVE = ['oxlint', '-disable'].join('')
235
+
236
+ /** Source, test, config, and script files inspected for lint suppression directives. */
237
+ export const POLICY_SUPPRESSION_GLOB: readonly string[] = Object.freeze([
238
+ '{src,app,tests,configs,scripts}/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}',
239
+ '*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}',
240
+ ])
241
+
242
+ /** Rules whose workspace-wide lint wiring must not be weakened by configuration. */
243
+ export const POLICY_WIRING_RULES: readonly string[] = Object.freeze([
244
+ 'policy/no-mocking',
245
+ 'policy/no-keyword-privacy',
246
+ 'typescript/parameter-properties',
247
+ 'typescript/explicit-member-accessibility',
248
+ ])
249
+
250
+ /** Linted workspace roots that ignore patterns must not reach. */
251
+ export const POLICY_WIRING_ROOTS: readonly string[] = Object.freeze([
252
+ 'src',
253
+ 'app',
254
+ 'tests',
255
+ 'configs',
256
+ ])
257
+
258
+ /** Either lint suppression token the text sweep refuses. */
259
+ export const POLICY_SUPPRESSION_PATTERN = new RegExp(
260
+ [['eslint', '-disable'].join(''), POLICY_SUPPRESSION_DIRECTIVE].join('|'),
261
+ 'u',
262
+ )
263
+
175
264
  /**
176
265
  * Normalize platform separators for stable matching and diagnostics.
177
266
  *
@@ -731,6 +820,104 @@ export function inspectPolicyMirrors(root: string): readonly PolicyViolation[] {
731
820
  return inspectPolicyMirrorPaths(tests, modules)
732
821
  }
733
822
 
823
+ /**
824
+ * Inspect code-shaped workspace files for lint suppression directives.
825
+ *
826
+ * @param root - The workspace root to inspect.
827
+ * @returns Every suppression occurrence in path and line order.
828
+ */
829
+ export function inspectPolicySuppressions(root: string): readonly PolicyViolation[] {
830
+ const violations: PolicyViolation[] = []
831
+ const paths = globSync(POLICY_SUPPRESSION_GLOB, { cwd: root }).map(normalizePolicyPath).sort()
832
+ for (const path of paths) {
833
+ const lines = readFileSync(join(root, path), 'utf8').split('\n')
834
+ for (let index = 0; index < lines.length; index += 1) {
835
+ const line = lines[index]
836
+ if (line !== undefined && POLICY_SUPPRESSION_PATTERN.test(line)) {
837
+ violations.push({
838
+ rule: 'suppression',
839
+ path,
840
+ line: index + 1,
841
+ message: 'file carries a lint suppression directive',
842
+ })
843
+ }
844
+ }
845
+ }
846
+ return violations
847
+ }
848
+
849
+ /**
850
+ * Inspect the lint configuration that keeps policy rules active across the workspace.
851
+ *
852
+ * @param configuration - The parsed Oxlint configuration to inspect.
853
+ * @returns Every wiring violation in rule and configuration order.
854
+ */
855
+ export function inspectPolicyConfiguration(configuration: unknown): readonly string[] {
856
+ const violations: string[] = []
857
+ if (typeof configuration !== 'object' || configuration === null || Array.isArray(configuration)) {
858
+ return ['Oxlint configuration must be a record']
859
+ }
860
+
861
+ const rules: unknown = Object.getOwnPropertyDescriptor(configuration, 'rules')?.value
862
+ for (const rule of POLICY_WIRING_RULES) {
863
+ const setting =
864
+ typeof rules === 'object' && rules !== null && !Array.isArray(rules)
865
+ ? Object.getOwnPropertyDescriptor(rules, rule)?.value
866
+ : undefined
867
+ const severity = Array.isArray(setting) ? setting[0] : setting
868
+ if (severity !== 'error') violations.push(`${rule} must have top-level error severity`)
869
+ }
870
+
871
+ const ignorePatterns: unknown = Object.getOwnPropertyDescriptor(
872
+ configuration,
873
+ 'ignorePatterns',
874
+ )?.value
875
+ if (ignorePatterns !== undefined && !Array.isArray(ignorePatterns)) {
876
+ violations.push('ignorePatterns must be an array when declared')
877
+ } else if (Array.isArray(ignorePatterns)) {
878
+ for (const pattern of ignorePatterns) {
879
+ if (typeof pattern !== 'string' || pattern.startsWith('!')) continue
880
+ const normalized = normalizePolicyPath(pattern).replace(/^\.\//u, '').replace(/^\//u, '')
881
+ const [first = ''] = normalized.split('/')
882
+ if (
883
+ POLICY_WIRING_ROOTS.some(
884
+ (root) => first === root || (first !== '' && matchesGlob(root, first)),
885
+ )
886
+ ) {
887
+ violations.push(`ignorePatterns must not reach ${pattern}`)
888
+ }
889
+ }
890
+ }
891
+
892
+ const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
893
+ if (overrides !== undefined && !Array.isArray(overrides)) {
894
+ violations.push('overrides must be an array when declared')
895
+ } else if (Array.isArray(overrides)) {
896
+ for (const override of overrides) {
897
+ if (typeof override !== 'object' || override === null || Array.isArray(override)) {
898
+ violations.push('override entries must be records')
899
+ continue
900
+ }
901
+ const overrideRules: unknown = Object.getOwnPropertyDescriptor(override, 'rules')?.value
902
+ if (
903
+ overrideRules === undefined ||
904
+ typeof overrideRules !== 'object' ||
905
+ overrideRules === null ||
906
+ Array.isArray(overrideRules)
907
+ ) {
908
+ continue
909
+ }
910
+ for (const rule of POLICY_WIRING_RULES) {
911
+ if (Object.getOwnPropertyDescriptor(overrideRules, rule) !== undefined) {
912
+ violations.push(`overrides must not configure ${rule}`)
913
+ }
914
+ }
915
+ }
916
+ }
917
+
918
+ return violations
919
+ }
920
+
734
921
  /**
735
922
  * Resolve an exact-case directory beneath a physical root.
736
923
  *
@@ -769,6 +956,22 @@ export function isPolicyFile(root: string, path: string): boolean {
769
956
  )
770
957
  }
771
958
 
959
+ /**
960
+ * Read the immediate child directories beneath one workspace-relative path.
961
+ *
962
+ * @param root - The workspace root to inspect.
963
+ * @param path - The workspace-relative parent directory.
964
+ * @returns The sorted immediate child directory names.
965
+ */
966
+ export function readPolicyDirectories(root: string, path: string): readonly string[] {
967
+ const directory = resolvePolicyDirectory(root, path)
968
+ if (directory === undefined) return []
969
+ return readdirSync(directory, { withFileTypes: true })
970
+ .filter((entry) => entry.isDirectory())
971
+ .map((entry) => entry.name)
972
+ .sort()
973
+ }
974
+
772
975
  /**
773
976
  * Discover the skill family from immediate directories in the workspace tree.
774
977
  *
@@ -776,11 +979,117 @@ export function isPolicyFile(root: string, path: string): boolean {
776
979
  * @returns The sorted directory names that belong to the skill family.
777
980
  */
778
981
  export function readSkillFamily(root: string): readonly string[] {
779
- const directory = resolvePolicyDirectory(root, SKILL_FAMILY_ROOT)
982
+ return readPolicyDirectories(root, SKILL_FAMILY_ROOT)
983
+ }
984
+
985
+ /**
986
+ * Parse one skill document's frontmatter without interpreting arbitrary body lines as keys.
987
+ *
988
+ * @param content - The raw SKILL.md text.
989
+ * @returns The parsed fields and exact scalar source, or `undefined` for an unsupported shape.
990
+ */
991
+ export function parseSkillFrontmatter(content: string): SkillFrontmatter | undefined {
992
+ const lines = content.replaceAll('\r\n', '\n').split('\n')
993
+ const rawLines = content.split('\n')
994
+ if (lines[0] !== '---') return undefined
995
+ const boundary = lines.indexOf('---', 1)
996
+ if (boundary === -1) return undefined
997
+ const keys: string[] = []
998
+ let name: string | undefined
999
+ let description: string | undefined
1000
+ let nameSource: string | undefined
1001
+ let descriptionSource: string | undefined
1002
+
1003
+ for (let index = 1; index < boundary; index += 1) {
1004
+ const line = lines[index]
1005
+ if (line === undefined) return undefined
1006
+ const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*):(.*)$/u)
1007
+ const key = match?.[1]
1008
+ const scalar = match?.[2]
1009
+ if (key === undefined || scalar === undefined) return undefined
1010
+ if (scalar !== '' && !scalar.startsWith(' ')) return undefined
1011
+ keys.push(key)
1012
+ let value = scalar === '' ? '' : scalar.slice(1)
1013
+ let source = rawLines[index]?.slice(line.indexOf(':') + 1)
1014
+ if (source === undefined) return undefined
1015
+ if (value === '>-') {
1016
+ if (key !== 'description') return undefined
1017
+ const folded: string[] = []
1018
+ const sourceLines: string[] = [source]
1019
+ for (index += 1; index < boundary; index += 1) {
1020
+ const continuation = lines[index]
1021
+ if (continuation === undefined) return undefined
1022
+ if (continuation.trim() === '') {
1023
+ folded.push('')
1024
+ const rawContinuation = rawLines[index]
1025
+ if (rawContinuation === undefined) return undefined
1026
+ sourceLines.push(rawContinuation)
1027
+ continue
1028
+ }
1029
+ if (!continuation.startsWith(' ')) {
1030
+ index -= 1
1031
+ break
1032
+ }
1033
+ folded.push(continuation.slice(2))
1034
+ const rawContinuation = rawLines[index]
1035
+ if (rawContinuation === undefined) return undefined
1036
+ sourceLines.push(rawContinuation)
1037
+ }
1038
+ value = ''
1039
+ let blanks = 0
1040
+ for (const foldedLine of folded) {
1041
+ if (foldedLine === '') {
1042
+ blanks += 1
1043
+ continue
1044
+ }
1045
+ if (value !== '') value += blanks === 0 ? ' ' : '\n'.repeat(blanks)
1046
+ value += foldedLine
1047
+ blanks = 0
1048
+ }
1049
+ source = sourceLines.join('\n')
1050
+ } else if (key === 'description' && (/^['"]/u.test(value) || /^[>|][+-]?$/u.test(value))) {
1051
+ return undefined
1052
+ }
1053
+ if (key === 'name') {
1054
+ name = value
1055
+ nameSource = source
1056
+ } else if (key === 'description') {
1057
+ description = value
1058
+ descriptionSource = source
1059
+ }
1060
+ }
1061
+
1062
+ return {
1063
+ keys,
1064
+ name,
1065
+ description,
1066
+ source: { name: nameSource, description: descriptionSource },
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Test whether a description carries a sentence that begins with the case-sensitive word `Use`.
1072
+ *
1073
+ * @param description - The parsed skill description.
1074
+ * @returns True when the description contains the canonical trigger sentence.
1075
+ */
1076
+ export function matchesSkillTrigger(description: string): boolean {
1077
+ return /(?:^|[.!?]\s+)Use \S/u.test(description)
1078
+ }
1079
+
1080
+ /**
1081
+ * Read the direct Markdown files owned by one skill's references directory.
1082
+ *
1083
+ * @param root - The workspace root to inspect.
1084
+ * @param name - The discovered skill directory name.
1085
+ * @returns Each direct references/name.md path in sorted order.
1086
+ */
1087
+ export function readSkillReferences(root: string, name: string): readonly string[] {
1088
+ const directory = resolvePolicyDirectory(root, `${SKILL_FAMILY_ROOT}/${name}/references`)
780
1089
  if (directory === undefined) return []
781
1090
  return readdirSync(directory, { withFileTypes: true })
782
- .filter((entry) => entry.isDirectory())
783
- .map((entry) => entry.name)
1091
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
1092
+ .map((entry) => `references/${entry.name}`)
784
1093
  .sort()
785
1094
  }
786
1095
 
@@ -869,10 +1178,51 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
869
1178
  const skill = `${base}/SKILL.md`
870
1179
  const metadata = `${base}/agents/openai.yaml`
871
1180
  const violations: PolicyViolation[] = []
1181
+ let content: string | undefined
872
1182
  if (!isPolicyFile(root, skill)) {
873
1183
  violations.push(
874
1184
  createPolicyViolation('skill', skill, 'skill requires an exact-case regular SKILL.md'),
875
1185
  )
1186
+ } else {
1187
+ content = readFileSync(join(root, skill), 'utf8')
1188
+ const frontmatter = parseSkillFrontmatter(content)
1189
+ if (frontmatter === undefined) {
1190
+ violations.push(
1191
+ createPolicyViolation('skill', skill, 'SKILL.md frontmatter exists and parses'),
1192
+ )
1193
+ } else {
1194
+ const keys = new Set(frontmatter.keys)
1195
+ if (
1196
+ frontmatter.keys.length !== 2 ||
1197
+ keys.size !== 2 ||
1198
+ !keys.has('name') ||
1199
+ !keys.has('description')
1200
+ ) {
1201
+ violations.push(
1202
+ createPolicyViolation(
1203
+ 'skill',
1204
+ skill,
1205
+ 'SKILL.md frontmatter contains exactly name and description',
1206
+ ),
1207
+ )
1208
+ }
1209
+ if (frontmatter.name !== name) {
1210
+ violations.push(
1211
+ createPolicyViolation('skill', skill, 'SKILL.md frontmatter name matches its directory'),
1212
+ )
1213
+ }
1214
+ if (frontmatter.description === undefined || frontmatter.description.trim() === '') {
1215
+ violations.push(createPolicyViolation('skill', skill, 'SKILL.md description is non-empty'))
1216
+ } else if (!matchesSkillTrigger(frontmatter.description)) {
1217
+ violations.push(
1218
+ createPolicyViolation(
1219
+ 'skill',
1220
+ skill,
1221
+ 'SKILL.md description names when to use the skill in a sentence beginning Use',
1222
+ ),
1223
+ )
1224
+ }
1225
+ }
876
1226
  }
877
1227
  if (!isPolicyFile(root, metadata)) {
878
1228
  violations.push(
@@ -902,8 +1252,9 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
902
1252
  )
903
1253
  }
904
1254
  }
905
- if (isPolicyFile(root, skill)) {
906
- for (const reference of extractSkillReferences(readFileSync(join(root, skill), 'utf8'))) {
1255
+ const named = content === undefined ? [] : extractSkillReferences(content)
1256
+ if (content !== undefined) {
1257
+ for (const reference of named) {
907
1258
  const path = `${base}/${reference}`
908
1259
  if (!isPolicyFile(root, path)) {
909
1260
  violations.push(
@@ -916,6 +1267,46 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
916
1267
  }
917
1268
  }
918
1269
  }
1270
+ for (const reference of readSkillReferences(root, name)) {
1271
+ if (!named.includes(reference)) {
1272
+ violations.push(
1273
+ createPolicyViolation(
1274
+ 'skill',
1275
+ `${base}/${reference}`,
1276
+ `references Markdown file is named by SKILL.md: ${reference}`,
1277
+ ),
1278
+ )
1279
+ }
1280
+ }
1281
+ const references = resolvePolicyDirectory(root, `${base}/references`)
1282
+ if (references !== undefined) {
1283
+ for (const entry of readdirSync(references, { withFileTypes: true })) {
1284
+ if (entry.isDirectory()) {
1285
+ violations.push(
1286
+ createPolicyViolation(
1287
+ 'skill',
1288
+ `${base}/references/${entry.name}`,
1289
+ 'skill references directory contains no subdirectories',
1290
+ ),
1291
+ )
1292
+ }
1293
+ }
1294
+ }
1295
+ const directory = resolvePolicyDirectory(root, base)
1296
+ if (directory !== undefined) {
1297
+ for (const path of globSync('**/*', { cwd: directory }).map(normalizePolicyPath).sort()) {
1298
+ const file = basename(path).toLowerCase()
1299
+ if ((file === 'readme.md' || file === 'changelog.md') && isPolicyFile(directory, path)) {
1300
+ violations.push(
1301
+ createPolicyViolation(
1302
+ 'skill',
1303
+ `${base}/${path}`,
1304
+ 'skill directory contains no README.md or CHANGELOG.md',
1305
+ ),
1306
+ )
1307
+ }
1308
+ }
1309
+ }
919
1310
  return violations
920
1311
  }
921
1312
 
@@ -932,40 +1323,194 @@ export function inspectSkillFamily(root: string): readonly PolicyViolation[] {
932
1323
  }
933
1324
 
934
1325
  /**
935
- * Inspect source placement and test mirrors across one workspace.
1326
+ * Inspect one provider bridge against its canonical skill twin.
936
1327
  *
937
1328
  * @param root - The workspace root to inspect.
938
- * @returns Every source-placement and mirror violation.
1329
+ * @param name - The shared canonical and bridge directory name.
1330
+ * @returns Every bridge violation in frontmatter, body, and directory order.
1331
+ */
1332
+ export function inspectBridge(root: string, name: string): readonly PolicyViolation[] {
1333
+ const canonicalPath = `${SKILL_FAMILY_ROOT}/${name}/SKILL.md`
1334
+ const bridgeBase = `${SKILL_BRIDGE_ROOT}/${name}`
1335
+ const bridgePath = `${bridgeBase}/SKILL.md`
1336
+ if (!isPolicyFile(root, bridgePath)) {
1337
+ return [
1338
+ createPolicyViolation('bridge', bridgePath, 'bridge requires an exact-case regular SKILL.md'),
1339
+ ]
1340
+ }
1341
+ const content = readFileSync(join(root, bridgePath), 'utf8')
1342
+ const bridge = parseSkillFrontmatter(content)
1343
+ const canonical = isPolicyFile(root, canonicalPath)
1344
+ ? parseSkillFrontmatter(readFileSync(join(root, canonicalPath), 'utf8'))
1345
+ : undefined
1346
+ const violations: PolicyViolation[] = []
1347
+ if (bridge === undefined) {
1348
+ violations.push(
1349
+ createPolicyViolation('bridge', bridgePath, 'bridge SKILL.md frontmatter parses'),
1350
+ )
1351
+ } else {
1352
+ const keys = new Set(bridge.keys)
1353
+ if (
1354
+ bridge.keys.length !== 2 ||
1355
+ keys.size !== 2 ||
1356
+ !keys.has('name') ||
1357
+ !keys.has('description')
1358
+ ) {
1359
+ violations.push(
1360
+ createPolicyViolation(
1361
+ 'bridge',
1362
+ bridgePath,
1363
+ 'bridge SKILL.md frontmatter contains exactly name and description',
1364
+ ),
1365
+ )
1366
+ }
1367
+ }
1368
+ if (bridge !== undefined && canonical !== undefined) {
1369
+ if (bridge.source.name !== canonical.source.name) {
1370
+ violations.push(
1371
+ createPolicyViolation(
1372
+ 'bridge',
1373
+ bridgePath,
1374
+ 'bridge frontmatter name matches its canonical twin',
1375
+ ),
1376
+ )
1377
+ }
1378
+ if (bridge.source.description !== canonical.source.description) {
1379
+ violations.push(
1380
+ createPolicyViolation(
1381
+ 'bridge',
1382
+ bridgePath,
1383
+ 'bridge frontmatter description matches its canonical twin',
1384
+ ),
1385
+ )
1386
+ }
1387
+ }
1388
+ const normalized = content.replaceAll('\r\n', '\n')
1389
+ const boundary = normalized.indexOf('\n---', 3)
1390
+ const body = boundary === -1 ? normalized : normalized.slice(boundary + '\n---'.length)
1391
+ if (!body.includes(canonicalPath)) {
1392
+ violations.push(
1393
+ createPolicyViolation(
1394
+ 'bridge',
1395
+ bridgePath,
1396
+ `bridge body names its canonical workflow: ${canonicalPath}`,
1397
+ ),
1398
+ )
1399
+ }
1400
+ if (resolvePolicyDirectory(root, `${bridgeBase}/references`) !== undefined) {
1401
+ violations.push(
1402
+ createPolicyViolation(
1403
+ 'bridge',
1404
+ `${bridgeBase}/references`,
1405
+ 'bridge owns no references directory',
1406
+ ),
1407
+ )
1408
+ }
1409
+ return violations
1410
+ }
1411
+
1412
+ /**
1413
+ * Inspect the provider bridge set and every bridge shared with the canonical skill family.
1414
+ *
1415
+ * @param root - The workspace root to inspect.
1416
+ * @returns Every bridge-set and bridge-content violation in directory order.
1417
+ */
1418
+ export function inspectSkillBridges(root: string): readonly PolicyViolation[] {
1419
+ const canonical = readSkillFamily(root)
1420
+ const bridges = readPolicyDirectories(root, SKILL_BRIDGE_ROOT)
1421
+ const bridgeSet = new Set(bridges)
1422
+ const canonicalSet = new Set(canonical)
1423
+ const violations: PolicyViolation[] = []
1424
+ for (const name of canonical) {
1425
+ if (!bridgeSet.has(name)) {
1426
+ violations.push(
1427
+ createPolicyViolation(
1428
+ 'bridge',
1429
+ `${SKILL_BRIDGE_ROOT}/${name}`,
1430
+ 'canonical skill has a matching provider bridge directory',
1431
+ ),
1432
+ )
1433
+ } else {
1434
+ violations.push(...inspectBridge(root, name))
1435
+ }
1436
+ }
1437
+ for (const name of bridges) {
1438
+ if (!canonicalSet.has(name)) {
1439
+ violations.push(
1440
+ createPolicyViolation(
1441
+ 'bridge',
1442
+ `${SKILL_BRIDGE_ROOT}/${name}`,
1443
+ 'provider bridge directory has a canonical skill twin',
1444
+ ),
1445
+ )
1446
+ }
1447
+ }
1448
+ return violations
1449
+ }
1450
+
1451
+ /**
1452
+ * Inspect every policy rule across one workspace.
1453
+ *
1454
+ * @param root - The workspace root to inspect.
1455
+ * @returns Every source, mirror, suppression, skill, and bridge violation.
939
1456
  */
940
1457
  export function inspectPolicyWorkspace(root: string): readonly PolicyViolation[] {
941
- return [...inspectPolicySources(readPolicySources(root)), ...inspectPolicyMirrors(root)]
1458
+ return [
1459
+ ...inspectPolicySources(readPolicySources(root)),
1460
+ ...inspectPolicyMirrors(root),
1461
+ ...inspectPolicySuppressions(root),
1462
+ ...inspectSkillFamily(root),
1463
+ ...inspectSkillBridges(root),
1464
+ ]
942
1465
  }
943
1466
 
944
1467
  /**
945
1468
  * Write a control to a real temporary workspace and run the production sweep over it.
946
1469
  *
947
- * The control's rule selects the sweep: `skill` inspects the family, every other rule inspects
948
- * source placement and mirrors.
1470
+ * The control's rule selects the sweep: `skill` inspects the canonical family, `bridge` inspects
1471
+ * provider bridges, and every other rule inspects the whole workspace route.
949
1472
  *
950
1473
  * @param control - The physical fixture and expected rule boundary.
951
1474
  * @returns Every violation reported through the production workspace route.
952
1475
  */
953
1476
  export function inspectPolicyControl(control: PolicyControl): readonly PolicyViolation[] {
954
- const root = mkdtempSync(join(tmpdir(), 'orkestrel-policy-'))
1477
+ const scratch = createPolicyScratch({ prefix: 'orkestrel-policy-' })
955
1478
  try {
956
1479
  for (const file of control.files) {
957
- const path = join(root, ...normalizePolicyPath(file.path).split('/'))
958
- mkdirSync(dirname(path), { recursive: true })
959
- writeFileSync(path, file.content, 'utf8')
1480
+ scratch.write(file.path, file.content)
960
1481
  }
961
- return control.rule === 'skill' ? inspectSkillFamily(root) : inspectPolicyWorkspace(root)
1482
+ if (control.rule === 'skill') return inspectSkillFamily(scratch.path)
1483
+ if (control.rule === 'bridge') return inspectSkillBridges(scratch.path)
1484
+ return inspectPolicyWorkspace(scratch.path)
962
1485
  } finally {
963
- rmSync(root, { recursive: true, force: true })
1486
+ scratch.destroy()
964
1487
  }
965
1488
  }
966
1489
 
967
1490
  /** Physical negative controls, one for each rule the instrument claims to enforce. */
968
1491
  export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
1492
+ {
1493
+ label: 'rejects a suppression directive in a scanned source file',
1494
+ membership: 'source, test, config, and script files in the suppression population',
1495
+ rule: 'suppression',
1496
+ files: [
1497
+ {
1498
+ path: 'scripts/control.ts',
1499
+ content: `// ${POLICY_SUPPRESSION_DIRECTIVE}\ndebugger\n`,
1500
+ },
1501
+ ],
1502
+ },
1503
+ {
1504
+ label: 'rejects a suppression directive in a root TSX file',
1505
+ membership: 'root code files in the suppression population',
1506
+ rule: 'suppression',
1507
+ files: [
1508
+ {
1509
+ path: 'probeRoot.tsx',
1510
+ content: `// ${POLICY_SUPPRESSION_DIRECTIVE}\ndebugger\n`,
1511
+ },
1512
+ ],
1513
+ },
969
1514
  {
970
1515
  label: 'rejects a type outside types.ts',
971
1516
  membership: 'top-level type declarations whose filename is not types.ts',
@@ -1192,6 +1737,144 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
1192
1737
 
1193
1738
  /** Physical in-family controls for every skill-family assertion class. */
1194
1739
  export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
1740
+ {
1741
+ label: 'rejects a SKILL.md without frontmatter',
1742
+ membership: 'exact-case regular SKILL.md files in discovered skill directories',
1743
+ rule: 'skill',
1744
+ files: [
1745
+ { path: '.agents/skills/sample/SKILL.md', content: '# Skill\n' },
1746
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1747
+ ],
1748
+ },
1749
+ {
1750
+ label: 'rejects an unsupported description scalar shape',
1751
+ membership: 'description scalars in discovered skill frontmatter',
1752
+ rule: 'skill',
1753
+ files: [
1754
+ {
1755
+ path: '.agents/skills/sample/SKILL.md',
1756
+ content:
1757
+ '---\nname: sample\ndescription: |\n Use this skill for a policy fixture.\n---\n\n# Skill\n',
1758
+ },
1759
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1760
+ ],
1761
+ },
1762
+ {
1763
+ label: 'rejects extra frontmatter keys',
1764
+ membership: 'parsed frontmatter keys in discovered skill documents',
1765
+ rule: 'skill',
1766
+ message: 'SKILL.md frontmatter contains exactly name and description',
1767
+ files: [
1768
+ {
1769
+ path: '.agents/skills/sample/SKILL.md',
1770
+ content:
1771
+ '---\nname: sample\ndescription: Use this skill for a policy fixture.\nlicense: MIT\n---\n\n# Skill\n',
1772
+ },
1773
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1774
+ ],
1775
+ },
1776
+ {
1777
+ label: 'rejects a frontmatter name that differs from its directory',
1778
+ membership: 'parsed names in discovered skill frontmatter',
1779
+ rule: 'skill',
1780
+ message: 'SKILL.md frontmatter name matches its directory',
1781
+ files: [
1782
+ {
1783
+ path: '.agents/skills/sample/SKILL.md',
1784
+ content:
1785
+ '---\nname: other\ndescription: Use this skill for a policy fixture.\n---\n\n# Skill\n',
1786
+ },
1787
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1788
+ ],
1789
+ },
1790
+ {
1791
+ label: 'rejects an empty skill description',
1792
+ membership: 'parsed descriptions in discovered skill frontmatter',
1793
+ rule: 'skill',
1794
+ message: 'SKILL.md description is non-empty',
1795
+ files: [
1796
+ {
1797
+ path: '.agents/skills/sample/SKILL.md',
1798
+ content: '---\nname: sample\ndescription: \n---\n\n# Skill\n',
1799
+ },
1800
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1801
+ ],
1802
+ },
1803
+ {
1804
+ label: 'rejects a description without a Use sentence',
1805
+ membership: 'immediate directories beneath .agents/skills',
1806
+ rule: 'skill',
1807
+ message: 'SKILL.md description names when to use the skill in a sentence beginning Use',
1808
+ files: [
1809
+ {
1810
+ path: '.agents/skills/sample/SKILL.md',
1811
+ content:
1812
+ '---\nname: sample\ndescription: Exercise the skill family policy.\n---\n\n# Skill\n',
1813
+ },
1814
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1815
+ ],
1816
+ },
1817
+ {
1818
+ label: 'rejects a single-quoted description scalar',
1819
+ membership: 'description scalars in discovered skill frontmatter',
1820
+ rule: 'skill',
1821
+ message: 'SKILL.md frontmatter exists and parses',
1822
+ files: [
1823
+ {
1824
+ path: '.agents/skills/sample/SKILL.md',
1825
+ content:
1826
+ "---\nname: sample\ndescription: 'Use this skill for a policy fixture.'\n---\n\n# Skill\n",
1827
+ },
1828
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1829
+ ],
1830
+ },
1831
+ {
1832
+ label: 'rejects a double-quoted description scalar',
1833
+ membership: 'description scalars in discovered skill frontmatter',
1834
+ rule: 'skill',
1835
+ message: 'SKILL.md frontmatter exists and parses',
1836
+ files: [
1837
+ {
1838
+ path: '.agents/skills/sample/SKILL.md',
1839
+ content:
1840
+ '---\nname: sample\ndescription: "Use this skill for a policy fixture."\n---\n\n# Skill\n',
1841
+ },
1842
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1843
+ ],
1844
+ },
1845
+ {
1846
+ label: 'rejects an unnamed Markdown reference file',
1847
+ membership: 'Markdown files directly beneath a discovered skill references directory',
1848
+ rule: 'skill',
1849
+ message: 'references Markdown file is named by SKILL.md: references/orphan.md',
1850
+ files: [
1851
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
1852
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1853
+ { path: '.agents/skills/sample/references/orphan.md', content: '# Orphan\n' },
1854
+ ],
1855
+ },
1856
+ {
1857
+ label: 'rejects a nested references directory',
1858
+ membership: 'directories directly beneath a discovered skill references directory',
1859
+ rule: 'skill',
1860
+ message: 'skill references directory contains no subdirectories',
1861
+ files: [
1862
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
1863
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1864
+ { path: '.agents/skills/sample/references/nested/detail.md', content: '# Detail\n' },
1865
+ ],
1866
+ },
1867
+ {
1868
+ label: 'rejects an auxiliary changelog in a skill directory',
1869
+ membership: 'files at any depth inside a discovered skill directory',
1870
+ rule: 'skill',
1871
+ message: 'skill directory contains no README.md or CHANGELOG.md',
1872
+ files: [
1873
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
1874
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1875
+ { path: '.agents/skills/sample/docs/CHANGELOG.MD', content: '# Changes\n' },
1876
+ ],
1877
+ },
1195
1878
  {
1196
1879
  label: 'rejects a missing exact-case SKILL.md',
1197
1880
  membership: 'immediate directories beneath .agents/skills',
@@ -1251,12 +1934,126 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
1251
1934
  },
1252
1935
  {
1253
1936
  label: 'rejects a dangling exact-case SKILL.md reference',
1254
- membership: 'immediate directories beneath .agents/skills',
1937
+ membership: 'references/name.md tokens extracted from canonical SKILL.md text',
1255
1938
  rule: 'skill',
1256
1939
  files: [
1257
1940
  { path: '.agents/skills/sample/SKILL.md', content: SKILL_REFERENCE_TEXT },
1258
1941
  { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
1259
- { path: '.agents/skills/sample/references/Example.md', content: '# Example\n' },
1942
+ ],
1943
+ },
1944
+ ])
1945
+
1946
+ /** Physical controls for provider-bridge assertions. */
1947
+ export const BRIDGE_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
1948
+ {
1949
+ label: 'rejects a canonical skill without a provider bridge',
1950
+ membership: 'immediate directories beneath .agents/skills',
1951
+ rule: 'bridge',
1952
+ message: 'canonical skill has a matching provider bridge directory',
1953
+ files: [{ path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT }],
1954
+ },
1955
+ {
1956
+ label: 'rejects a provider bridge without a canonical skill',
1957
+ membership: 'immediate directories beneath .claude/skills',
1958
+ rule: 'bridge',
1959
+ message: 'provider bridge directory has a canonical skill twin',
1960
+ files: [
1961
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
1962
+ { path: '.claude/skills/sample/SKILL.md', content: SKILL_BRIDGE_TEXT },
1963
+ {
1964
+ path: '.claude/skills/extra/SKILL.md',
1965
+ content:
1966
+ '---\nname: extra\ndescription: Use this skill for a policy fixture.\n---\n\nRead `.agents/skills/extra/SKILL.md`.\n',
1967
+ },
1968
+ ],
1969
+ },
1970
+ {
1971
+ label: 'rejects a bridge without an exact-case SKILL.md',
1972
+ membership: 'provider bridge directories shared with the canonical family',
1973
+ rule: 'bridge',
1974
+ message: 'bridge requires an exact-case regular SKILL.md',
1975
+ files: [
1976
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
1977
+ { path: '.claude/skills/sample/skill.md', content: SKILL_BRIDGE_TEXT },
1978
+ ],
1979
+ },
1980
+ {
1981
+ label: 'rejects malformed bridge frontmatter',
1982
+ membership: 'exact-case regular SKILL.md files in shared provider bridge directories',
1983
+ rule: 'bridge',
1984
+ message: 'bridge SKILL.md frontmatter parses',
1985
+ files: [
1986
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
1987
+ {
1988
+ path: '.claude/skills/sample/SKILL.md',
1989
+ content: '# Bridge\n\nRead `.agents/skills/sample/SKILL.md`.\n',
1990
+ },
1991
+ ],
1992
+ },
1993
+ {
1994
+ label: 'rejects extra bridge frontmatter keys',
1995
+ membership: 'parsed frontmatter keys in shared provider bridge directories',
1996
+ rule: 'bridge',
1997
+ message: 'bridge SKILL.md frontmatter contains exactly name and description',
1998
+ files: [
1999
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
2000
+ {
2001
+ path: '.claude/skills/sample/SKILL.md',
2002
+ content:
2003
+ '---\nname: sample\ndescription: Use this skill for a policy fixture.\nlicense: MIT\n---\n\nRead `.agents/skills/sample/SKILL.md`.\n',
2004
+ },
2005
+ ],
2006
+ },
2007
+ {
2008
+ label: 'rejects a bridge name that drifts from its canonical twin',
2009
+ membership: 'parsed frontmatter in shared provider bridge directories',
2010
+ rule: 'bridge',
2011
+ message: 'bridge frontmatter name matches its canonical twin',
2012
+ files: [
2013
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
2014
+ {
2015
+ path: '.claude/skills/sample/SKILL.md',
2016
+ content:
2017
+ '---\nname: other\ndescription: Use this skill for a policy fixture.\n---\n\nRead `.agents/skills/sample/SKILL.md`.\n',
2018
+ },
2019
+ ],
2020
+ },
2021
+ {
2022
+ label: 'rejects a bridge description that drifts from its canonical twin',
2023
+ membership: 'matching immediate directories beneath .agents/skills and .claude/skills',
2024
+ rule: 'bridge',
2025
+ message: 'bridge frontmatter description matches its canonical twin',
2026
+ files: [
2027
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
2028
+ {
2029
+ path: '.claude/skills/sample/SKILL.md',
2030
+ content:
2031
+ '---\nname: sample\ndescription: >-\n Use this skill for a policy fixture.\n---\n\nRead `.agents/skills/sample/SKILL.md`.\n',
2032
+ },
2033
+ ],
2034
+ },
2035
+ {
2036
+ label: 'rejects a bridge body without its canonical workflow path',
2037
+ membership: 'bodies of exact-case regular bridge SKILL.md files',
2038
+ rule: 'bridge',
2039
+ message: 'bridge body names its canonical workflow: .agents/skills/sample/SKILL.md',
2040
+ files: [
2041
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
2042
+ {
2043
+ path: '.claude/skills/sample/SKILL.md',
2044
+ content: `${SKILL_POLICY_TEXT}\nRead the canonical workflow.\n`,
2045
+ },
2046
+ ],
2047
+ },
2048
+ {
2049
+ label: 'rejects a references directory owned by a provider bridge',
2050
+ membership: 'shared provider bridge directories',
2051
+ rule: 'bridge',
2052
+ message: 'bridge owns no references directory',
2053
+ files: [
2054
+ { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
2055
+ { path: '.claude/skills/sample/SKILL.md', content: SKILL_BRIDGE_TEXT },
2056
+ { path: '.claude/skills/sample/references/detail.md', content: '# Detail\n' },
1260
2057
  ],
1261
2058
  },
1262
2059
  ])
@@ -1272,6 +2069,51 @@ export const SKILL_POLICY_APOSTROPHE: PolicyControl = Object.freeze({
1272
2069
  ],
1273
2070
  })
1274
2071
 
2072
+ /** A folded description containing a colon, proving continuation lines do not become keys. */
2073
+ export const SKILL_POLICY_FOLDED: PolicyControl = Object.freeze({
2074
+ label: 'accepts a folded description containing a colon',
2075
+ membership: 'folded description scalars in discovered skill frontmatter',
2076
+ rule: 'skill',
2077
+ files: [
2078
+ {
2079
+ path: '.agents/skills/sample/SKILL.md',
2080
+ content:
2081
+ '---\nname: sample\ndescription: >-\n Use this skill when a continuation contains: a colon.\n---\n\n# Skill\n',
2082
+ },
2083
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
2084
+ ],
2085
+ })
2086
+
2087
+ /** A trigger sentence whose first subject is a backticked command token. */
2088
+ export const SKILL_POLICY_BACKTICKED: PolicyControl = Object.freeze({
2089
+ label: 'accepts a backticked token after Use',
2090
+ membership: 'single-line descriptions in discovered skill frontmatter',
2091
+ rule: 'skill',
2092
+ files: [
2093
+ {
2094
+ path: '.agents/skills/sample/SKILL.md',
2095
+ content:
2096
+ '---\nname: sample\ndescription: Use `--app` when a policy fixture needs it.\n---\n\n# Skill\n',
2097
+ },
2098
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
2099
+ ],
2100
+ })
2101
+
2102
+ /** A folded description whose blank scalar line separates two paragraphs. */
2103
+ export const SKILL_POLICY_PARAGRAPHS: PolicyControl = Object.freeze({
2104
+ label: 'accepts a folded description containing two paragraphs',
2105
+ membership: 'folded description scalars in discovered skill frontmatter',
2106
+ rule: 'skill',
2107
+ files: [
2108
+ {
2109
+ path: '.agents/skills/sample/SKILL.md',
2110
+ content:
2111
+ '---\nname: sample\ndescription: >-\n First paragraph.\n\n Use `--app` when a policy fixture needs it.\n---\n\n# Skill\n',
2112
+ },
2113
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
2114
+ ],
2115
+ })
2116
+
1275
2117
  /** A bridge skill outside the discovered family, used to prove the membership boundary. */
1276
2118
  export const SKILL_POLICY_EXCLUSION: PolicyControl = Object.freeze({
1277
2119
  label: 'excludes .claude/skills from the skill family',