@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.
- package/dist/bin/main.js +8 -1
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +2 -2
- package/dist/host/agents/orchestration.md +33 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +16 -16
- package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +3 -3
- package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +1 -1
- package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +7 -2
- package/dist/host/claude/agents/builder.md +2 -2
- package/dist/host/claude/agents/orkestrel.md +48 -48
- package/dist/host/claude/rules/application.md +6 -4
- package/dist/host/claude/rules/architecture.md +2 -0
- package/dist/host/claude/rules/documentation.md +6 -0
- package/dist/host/claude/rules/tests.md +11 -1
- package/dist/host/claude/rules/typescript.md +15 -1
- package/dist/host/claude/rules/workspace.md +43 -13
- package/dist/host/claude/rules/writing.md +125 -0
- package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +10 -1
- package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +1 -1
- package/dist/host/configs/policy.ts +185 -0
- package/dist/host/dotfiles/oxlintrc.json +13 -1
- package/dist/host/dotfiles/prettierignore +3 -0
- package/dist/host/guides/scaffold.md +13 -7
- package/dist/host/manifest.json +11 -7
- package/dist/host/tests/config.test.ts +195 -4
- package/dist/host/tests/policy.test.ts +82 -0
- package/dist/host/tests/setupPolicy.ts +863 -21
- package/dist/src/core/index.cjs +68 -6
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +11 -9
- package/dist/src/core/index.d.ts +11 -9
- package/dist/src/core/index.js +68 -6
- package/dist/src/core/index.js.map +1 -1
- package/package.json +2 -2
- 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 =
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
906
|
-
|
|
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
|
|
1326
|
+
* Inspect one provider bridge against its canonical skill twin.
|
|
936
1327
|
*
|
|
937
1328
|
* @param root - The workspace root to inspect.
|
|
938
|
-
* @
|
|
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 [
|
|
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,
|
|
948
|
-
*
|
|
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
|
|
1477
|
+
const scratch = createPolicyScratch({ prefix: 'orkestrel-policy-' })
|
|
955
1478
|
try {
|
|
956
1479
|
for (const file of control.files) {
|
|
957
|
-
|
|
958
|
-
mkdirSync(dirname(path), { recursive: true })
|
|
959
|
-
writeFileSync(path, file.content, 'utf8')
|
|
1480
|
+
scratch.write(file.path, file.content)
|
|
960
1481
|
}
|
|
961
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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',
|