@orkestrel/scaffold 0.0.46 → 0.0.48
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 +179 -44
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/orchestration.md +3 -1
- package/dist/host/claude/agents/codex.md +4 -4
- package/dist/host/claude/agents/orkestrel.md +4 -4
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/quality.md +3 -0
- package/dist/host/claude/rules/tests.md +19 -0
- package/dist/host/claude/rules/workspace.md +28 -21
- package/dist/host/claude/settings.json +781 -2
- package/dist/host/configs/policy.ts +170 -10
- package/dist/host/dotfiles/oxlintrc.json +9 -0
- package/dist/host/guides/scaffold.md +143 -44
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/host/tests/config.test.ts +149 -11
- package/dist/host/tests/policy.test.ts +7 -0
- package/dist/host/tests/setupPolicy.ts +222 -8
- package/dist/src/core/index.cjs +420 -24
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +102 -9
- package/dist/src/core/index.d.ts +102 -9
- package/dist/src/core/index.js +416 -25
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +41 -49
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +9 -8
- package/dist/src/server/index.d.ts +9 -8
- package/dist/src/server/index.js +42 -50
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -7
|
@@ -18,7 +18,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
18
18
|
import { build, loadConfigFromFile } from 'vite'
|
|
19
19
|
import { RuleTester } from 'oxlint/plugins-dev'
|
|
20
20
|
import * as configHelpers from '../configs/helpers.js'
|
|
21
|
-
import { MOCKING_RULE, PRIVACY_RULE } from '../configs/policy.js'
|
|
21
|
+
import { MOCKING_RULE, NESTED_RULE, PRIVACY_RULE } from '../configs/policy.js'
|
|
22
22
|
import configuration, { resolveWorkspacePath } from '../vite.config.js'
|
|
23
23
|
import tsconfig from '../tsconfig.json' with { type: 'json' }
|
|
24
24
|
import { createPolicyScratch, inspectPolicyConfiguration } from './setupPolicy.js'
|
|
@@ -62,7 +62,13 @@ describe('root configuration', () => {
|
|
|
62
62
|
it('registers every workspace project with its fixed include and setup files', () => {
|
|
63
63
|
const expected = new Map<
|
|
64
64
|
string,
|
|
65
|
-
{
|
|
65
|
+
{
|
|
66
|
+
readonly benchmark?: readonly string[]
|
|
67
|
+
readonly include: string
|
|
68
|
+
readonly parallel?: boolean
|
|
69
|
+
readonly pool?: string
|
|
70
|
+
readonly setup: readonly string[]
|
|
71
|
+
}
|
|
66
72
|
>()
|
|
67
73
|
if (existsSync(resolve(root, 'src/core'))) {
|
|
68
74
|
expected.set('src:core', {
|
|
@@ -139,7 +145,13 @@ describe('root configuration', () => {
|
|
|
139
145
|
setup: ['./tests/setup.ts', './tests/setupService.ts'],
|
|
140
146
|
})
|
|
141
147
|
}
|
|
142
|
-
expected.set('probe', {
|
|
148
|
+
expected.set('probe', {
|
|
149
|
+
benchmark: ['tmp/probe/**/*.test.ts', 'tests/**/*.test.ts'],
|
|
150
|
+
include: 'tmp/probe/**/*.test.ts',
|
|
151
|
+
parallel: false,
|
|
152
|
+
pool: 'threads',
|
|
153
|
+
setup: ['./tests/setup.ts'],
|
|
154
|
+
})
|
|
143
155
|
// A row that is a configuration rather than a factory. A workspace with a
|
|
144
156
|
// browser application emits one, because that factory refuses overrides and
|
|
145
157
|
// so is not a value Vitest may call. It is required here, in a workspace that
|
|
@@ -174,7 +186,13 @@ describe('root configuration', () => {
|
|
|
174
186
|
const controlled = projects.concat(control, concrete)
|
|
175
187
|
const configured = new Map<
|
|
176
188
|
string,
|
|
177
|
-
{
|
|
189
|
+
{
|
|
190
|
+
readonly benchmark?: readonly string[]
|
|
191
|
+
readonly include: string
|
|
192
|
+
readonly parallel?: boolean
|
|
193
|
+
readonly pool?: string
|
|
194
|
+
readonly setup: readonly string[]
|
|
195
|
+
}
|
|
178
196
|
>()
|
|
179
197
|
for (const [requiredLabel] of expected) {
|
|
180
198
|
const factoryName = requiredLabel.replace(/:([a-z])/gu, (_match, letter: string) =>
|
|
@@ -227,6 +245,34 @@ describe('root configuration', () => {
|
|
|
227
245
|
if (effective.length !== 1 || typeof effective[0] !== 'string') {
|
|
228
246
|
throw new Error(`${label} does not resolve to one effective include`)
|
|
229
247
|
}
|
|
248
|
+
if (label === 'probe') {
|
|
249
|
+
const benchmark: unknown = Object.getOwnPropertyDescriptor(test, 'benchmark')?.value
|
|
250
|
+
const parallel: unknown = Object.getOwnPropertyDescriptor(test, 'fileParallelism')?.value
|
|
251
|
+
const pool: unknown = Object.getOwnPropertyDescriptor(test, 'pool')?.value
|
|
252
|
+
if (typeof benchmark !== 'object' || benchmark === null) {
|
|
253
|
+
throw new Error('The probe project carries no benchmark block')
|
|
254
|
+
}
|
|
255
|
+
const benchmarkInclude: unknown = Object.getOwnPropertyDescriptor(
|
|
256
|
+
benchmark,
|
|
257
|
+
'include',
|
|
258
|
+
)?.value
|
|
259
|
+
if (
|
|
260
|
+
!Array.isArray(benchmarkInclude) ||
|
|
261
|
+
!benchmarkInclude.every((path) => typeof path === 'string') ||
|
|
262
|
+
typeof parallel !== 'boolean' ||
|
|
263
|
+
typeof pool !== 'string'
|
|
264
|
+
) {
|
|
265
|
+
throw new Error('The probe project carries an invalid benchmark configuration')
|
|
266
|
+
}
|
|
267
|
+
configured.set(label, {
|
|
268
|
+
benchmark: benchmarkInclude,
|
|
269
|
+
include: effective[0],
|
|
270
|
+
parallel,
|
|
271
|
+
pool,
|
|
272
|
+
setup: [...new Set(setup)],
|
|
273
|
+
})
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
230
276
|
configured.set(label, { include: effective[0], setup: [...new Set(setup)] })
|
|
231
277
|
}
|
|
232
278
|
|
|
@@ -573,23 +619,114 @@ describe('policy plugin', () => {
|
|
|
573
619
|
],
|
|
574
620
|
})
|
|
575
621
|
|
|
622
|
+
tester.run('no-nested-functions', NESTED_RULE, {
|
|
623
|
+
valid: [
|
|
624
|
+
{
|
|
625
|
+
name: 'accepts a module-scope function',
|
|
626
|
+
code: 'function projectValue() { return 1 }',
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: 'accepts an anonymous callback passed directly',
|
|
630
|
+
code: 'function projectValues() { return values.map((value) => value + 1) }',
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: 'accepts an anonymous arrow returned directly',
|
|
634
|
+
code: 'function createProjector() { return () => 1 }',
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: 'accepts the sanctioned policy visitor delegation',
|
|
638
|
+
code: [
|
|
639
|
+
'function reportNode(context, node) { context.report({ node }) }',
|
|
640
|
+
'const RULE = {',
|
|
641
|
+
'create(context) {',
|
|
642
|
+
'return { CallExpression: (node) => reportNode(context, node) }',
|
|
643
|
+
'}',
|
|
644
|
+
'}',
|
|
645
|
+
].join('\n'),
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: 'accepts function syntax inside a class expression',
|
|
649
|
+
code: 'function projectValue() { return class { read() { const value = () => 1; return value() } } }',
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: 'accepts class accessors inside a factory',
|
|
653
|
+
code: 'function createAccessor() { class Accessor { get value() { return 1 } set value(value) { consume(value) } } return Accessor }',
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
invalid: [
|
|
657
|
+
{
|
|
658
|
+
name: 'accepts object accessors while rejecting nested function expressions',
|
|
659
|
+
code: [
|
|
660
|
+
'function createAccessor() {',
|
|
661
|
+
' const control = function () { return 1 }',
|
|
662
|
+
' return {',
|
|
663
|
+
' get value() {',
|
|
664
|
+
' const nested = function () { return 2 }',
|
|
665
|
+
' return nested()',
|
|
666
|
+
' },',
|
|
667
|
+
' set value(value) { consume(value) },',
|
|
668
|
+
' }',
|
|
669
|
+
'}',
|
|
670
|
+
].join('\n'),
|
|
671
|
+
errors: [
|
|
672
|
+
{ messageId: 'nested', line: 2, column: 18 },
|
|
673
|
+
{ messageId: 'nested', line: 5, column: 21 },
|
|
674
|
+
],
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: 'rejects a local function declaration',
|
|
678
|
+
code: 'function projectValue() { function readValue() { return 1 } return readValue() }',
|
|
679
|
+
errors: [{ messageId: 'nested' }],
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: 'rejects a function assigned to a local binding',
|
|
683
|
+
code: 'function projectValue() { const readValue = () => 1; return readValue() }',
|
|
684
|
+
errors: [{ messageId: 'nested' }],
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: 'rejects a named function expression argument',
|
|
688
|
+
code: 'function projectValue() { return read(function readValue() { return 1 }) }',
|
|
689
|
+
errors: [{ messageId: 'nested' }],
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
name: 'rejects a callback parameter default function',
|
|
693
|
+
code: 'function projectValue() { return values.map((value = () => 1) => value()) }',
|
|
694
|
+
errors: [{ messageId: 'nested' }],
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
name: 'rejects an assignment two direct callbacks down',
|
|
698
|
+
code: 'function projectValue() { return values.map((value) => read((nested) => { const project = () => nested; return project() })) }',
|
|
699
|
+
errors: [{ messageId: 'nested' }],
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: 'rejects a function assigned inside a class-declaration method',
|
|
703
|
+
code: 'class Project { read() { const value = () => 1; return value() } }',
|
|
704
|
+
errors: [{ messageId: 'nested' }],
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
})
|
|
708
|
+
|
|
576
709
|
it('loads every configured policy rule through the real binary', () => {
|
|
577
710
|
const scratch = createPolicyScratch({ prefix: 'orkestrel-config-policy-' })
|
|
578
711
|
try {
|
|
712
|
+
scratch.write('.oxlintrc.json', readFileSync(resolve(root, '.oxlintrc.json'), 'utf8'))
|
|
713
|
+
scratch.write('configs/policy.ts', readFileSync(resolve(root, 'configs/policy.ts'), 'utf8'))
|
|
579
714
|
scratch.write(
|
|
580
|
-
'violations/fixture.ts',
|
|
715
|
+
'src/violations/fixture.ts',
|
|
581
716
|
[
|
|
582
717
|
"vi.mock('./x')",
|
|
583
718
|
'class PrivateMember { private value = 1 }',
|
|
584
719
|
'class ParameterMember { constructor(readonly value: string) {} }',
|
|
585
720
|
'class PublicMember { public value = 1 }',
|
|
721
|
+
'function OuterFunction() { const nested = () => undefined; return nested() }',
|
|
586
722
|
'void PrivateMember',
|
|
587
723
|
'void ParameterMember',
|
|
588
724
|
'void PublicMember',
|
|
725
|
+
'void OuterFunction',
|
|
589
726
|
].join('\n'),
|
|
590
727
|
)
|
|
591
728
|
scratch.write(
|
|
592
|
-
'clean/fixture.ts',
|
|
729
|
+
'src/clean/fixture.ts',
|
|
593
730
|
[
|
|
594
731
|
'class CleanMember {',
|
|
595
732
|
'\t#value = 1',
|
|
@@ -621,16 +758,16 @@ describe('policy plugin', () => {
|
|
|
621
758
|
throw new Error('The oxlint package declares no bin.oxlint entry')
|
|
622
759
|
}
|
|
623
760
|
const binary = resolve(dirname(manifestPath), entry)
|
|
624
|
-
const config = resolve(
|
|
761
|
+
const config = resolve(scratch.path, '.oxlintrc.json')
|
|
625
762
|
const violations = spawnSync(
|
|
626
763
|
process.execPath,
|
|
627
|
-
[binary, '--config', config, '--format', 'json',
|
|
628
|
-
{ cwd:
|
|
764
|
+
[binary, '--config', config, '--format', 'json', 'src/violations'],
|
|
765
|
+
{ cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
|
|
629
766
|
)
|
|
630
767
|
const clean = spawnSync(
|
|
631
768
|
process.execPath,
|
|
632
|
-
[binary, '--config', config, '--format', 'json',
|
|
633
|
-
{ cwd:
|
|
769
|
+
[binary, '--config', config, '--format', 'json', 'src/clean'],
|
|
770
|
+
{ cwd: scratch.path, encoding: 'utf8', timeout: 15_000 },
|
|
634
771
|
)
|
|
635
772
|
const reports: string[][] = []
|
|
636
773
|
for (const result of [violations, clean]) {
|
|
@@ -664,6 +801,7 @@ describe('policy plugin', () => {
|
|
|
664
801
|
for (const rule of [
|
|
665
802
|
'policy(no-mocking)',
|
|
666
803
|
'policy(no-keyword-privacy)',
|
|
804
|
+
'policy(no-nested-functions)',
|
|
667
805
|
'typescript(parameter-properties)',
|
|
668
806
|
'typescript(explicit-member-accessibility)',
|
|
669
807
|
]) {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
SKILL_POLICY_BACKTICKED,
|
|
20
20
|
SKILL_POLICY_CONTROLS,
|
|
21
21
|
SKILL_POLICY_EXCLUSION,
|
|
22
|
+
SKILL_POLICY_FENCED,
|
|
22
23
|
SKILL_POLICY_FOLDED,
|
|
23
24
|
SKILL_POLICY_PARAGRAPHS,
|
|
24
25
|
stemToPolicyCandidates,
|
|
@@ -359,6 +360,7 @@ describe('skill family policy', () => {
|
|
|
359
360
|
const violations = inspectPolicyControl(control)
|
|
360
361
|
expect(violations).toHaveLength(1)
|
|
361
362
|
expect(violations[0]?.rule).toBe(control.rule)
|
|
363
|
+
expect(control.line === undefined || violations[0]?.line === control.line).toBe(true)
|
|
362
364
|
expect(control.message === undefined || violations[0]?.message === control.message).toBe(true)
|
|
363
365
|
})
|
|
364
366
|
}
|
|
@@ -375,6 +377,10 @@ describe('skill family policy', () => {
|
|
|
375
377
|
expect(inspectPolicyControl(SKILL_POLICY_BACKTICKED)).toEqual([])
|
|
376
378
|
})
|
|
377
379
|
|
|
380
|
+
it(`${SKILL_POLICY_FENCED.label} [membership: ${SKILL_POLICY_FENCED.membership}]`, () => {
|
|
381
|
+
expect(inspectPolicyControl(SKILL_POLICY_FENCED)).toEqual([])
|
|
382
|
+
})
|
|
383
|
+
|
|
378
384
|
it('parses a folded description containing more than one paragraph', () => {
|
|
379
385
|
const skill = SKILL_POLICY_PARAGRAPHS.files.find((file) => file.path.endsWith('/SKILL.md'))
|
|
380
386
|
const frontmatter = parseSkillFrontmatter(skill?.content ?? '')
|
|
@@ -401,6 +407,7 @@ describe('skill bridge policy', () => {
|
|
|
401
407
|
const violations = inspectPolicyControl(control)
|
|
402
408
|
expect(violations).toHaveLength(1)
|
|
403
409
|
expect(violations[0]?.rule).toBe(control.rule)
|
|
410
|
+
expect(control.line === undefined || violations[0]?.line === control.line).toBe(true)
|
|
404
411
|
expect(control.message === undefined || violations[0]?.message === control.message).toBe(true)
|
|
405
412
|
})
|
|
406
413
|
}
|
|
@@ -47,6 +47,8 @@ export interface PolicyControl {
|
|
|
47
47
|
readonly membership: string
|
|
48
48
|
readonly rule: PolicyRule
|
|
49
49
|
readonly files: readonly PolicySource[]
|
|
50
|
+
readonly directories?: readonly string[]
|
|
51
|
+
readonly line?: number
|
|
50
52
|
readonly message?: string
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -1169,6 +1171,67 @@ export function extractSkillReferences(content: string): readonly string[] {
|
|
|
1169
1171
|
return [...references].sort()
|
|
1170
1172
|
}
|
|
1171
1173
|
|
|
1174
|
+
/**
|
|
1175
|
+
* Inspects one skill document for template TODOs outside Markdown code.
|
|
1176
|
+
*
|
|
1177
|
+
* Coverage includes each literal `TODO` outside a matched pair of single backticks on one line and
|
|
1178
|
+
* outside a backtick or tilde fence indented by no more than three spaces. A fence starts with at
|
|
1179
|
+
* least three matching markers and ends on a later line starting with the same marker. Four spaces
|
|
1180
|
+
* start an indented code block, which this fence scanner does not interpret. An unterminated fence
|
|
1181
|
+
* excludes the rest of the file. Inline spans cannot cross lines, and the line discipline cannot
|
|
1182
|
+
* validate escaped or repeated delimiters or distinguish a backtick inside a code span's language
|
|
1183
|
+
* tag.
|
|
1184
|
+
*
|
|
1185
|
+
* @param rule - The canonical-skill or provider-bridge population being inspected.
|
|
1186
|
+
* @param path - The workspace-relative skill document path.
|
|
1187
|
+
* @param content - The raw Markdown text.
|
|
1188
|
+
* @returns Every template-TODO violation in line and occurrence order.
|
|
1189
|
+
*/
|
|
1190
|
+
export function inspectSkillTemplateTODOs(
|
|
1191
|
+
rule: 'bridge' | 'skill',
|
|
1192
|
+
path: string,
|
|
1193
|
+
content: string,
|
|
1194
|
+
): readonly PolicyViolation[] {
|
|
1195
|
+
const violations: PolicyViolation[] = []
|
|
1196
|
+
const lines = content.split(/\r\n|\r|\n/u)
|
|
1197
|
+
let fence: string | undefined
|
|
1198
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1199
|
+
const line = lines[index]
|
|
1200
|
+
if (line === undefined) continue
|
|
1201
|
+
const fenceLine = line.replace(/^ {0,3}/u, '')
|
|
1202
|
+
if (fence !== undefined) {
|
|
1203
|
+
if (fenceLine.startsWith(fence)) fence = undefined
|
|
1204
|
+
continue
|
|
1205
|
+
}
|
|
1206
|
+
const opening = fenceLine.match(/^(`{3,}|~{3,})/u)?.[1]
|
|
1207
|
+
if (opening !== undefined) {
|
|
1208
|
+
fence = opening
|
|
1209
|
+
continue
|
|
1210
|
+
}
|
|
1211
|
+
let cursor = 0
|
|
1212
|
+
while (cursor < line.length) {
|
|
1213
|
+
const openingBacktick = line.indexOf('`', cursor)
|
|
1214
|
+
const end = openingBacktick === -1 ? line.length : openingBacktick
|
|
1215
|
+
for (
|
|
1216
|
+
let todo = line.indexOf('TODO', cursor);
|
|
1217
|
+
todo !== -1 && todo < end;
|
|
1218
|
+
todo = line.indexOf('TODO', todo + 4)
|
|
1219
|
+
) {
|
|
1220
|
+
violations.push({
|
|
1221
|
+
rule,
|
|
1222
|
+
path,
|
|
1223
|
+
line: index + 1,
|
|
1224
|
+
message: 'skill documents contain no template TODOs',
|
|
1225
|
+
})
|
|
1226
|
+
}
|
|
1227
|
+
if (openingBacktick === -1) break
|
|
1228
|
+
const closingBacktick = line.indexOf('`', openingBacktick + 1)
|
|
1229
|
+
cursor = closingBacktick === -1 ? openingBacktick + 1 : closingBacktick + 1
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return violations
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1172
1235
|
/**
|
|
1173
1236
|
* Inspect one discovered skill's required files, metadata, token, and references.
|
|
1174
1237
|
*
|
|
@@ -1182,7 +1245,8 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1182
1245
|
const metadata = `${base}/agents/openai.yaml`
|
|
1183
1246
|
const violations: PolicyViolation[] = []
|
|
1184
1247
|
let content: string | undefined
|
|
1185
|
-
|
|
1248
|
+
const hasSkill = isPolicyFile(root, skill)
|
|
1249
|
+
if (!hasSkill) {
|
|
1186
1250
|
violations.push(
|
|
1187
1251
|
createPolicyViolation('skill', skill, 'skill requires an exact-case regular SKILL.md'),
|
|
1188
1252
|
)
|
|
@@ -1226,8 +1290,10 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1226
1290
|
)
|
|
1227
1291
|
}
|
|
1228
1292
|
}
|
|
1293
|
+
violations.push(...inspectSkillTemplateTODOs('skill', skill, content))
|
|
1229
1294
|
}
|
|
1230
|
-
|
|
1295
|
+
const hasMetadata = isPolicyFile(root, metadata)
|
|
1296
|
+
if (!hasMetadata) {
|
|
1231
1297
|
violations.push(
|
|
1232
1298
|
createPolicyViolation(
|
|
1233
1299
|
'skill',
|
|
@@ -1267,6 +1333,10 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1267
1333
|
`SKILL.md reference resolves to an exact-case regular file: ${reference}`,
|
|
1268
1334
|
),
|
|
1269
1335
|
)
|
|
1336
|
+
} else {
|
|
1337
|
+
violations.push(
|
|
1338
|
+
...inspectSkillTemplateTODOs('skill', path, readFileSync(join(root, path), 'utf8')),
|
|
1339
|
+
)
|
|
1270
1340
|
}
|
|
1271
1341
|
}
|
|
1272
1342
|
}
|
|
@@ -1298,8 +1368,28 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1298
1368
|
const directory = resolvePolicyDirectory(root, base)
|
|
1299
1369
|
if (directory !== undefined) {
|
|
1300
1370
|
for (const path of globSync('**/*', { cwd: directory }).map(normalizePolicyPath).sort()) {
|
|
1371
|
+
if (resolvePolicyDirectory(directory, path) !== undefined) {
|
|
1372
|
+
if (
|
|
1373
|
+
path === 'agents' ||
|
|
1374
|
+
path === 'references' ||
|
|
1375
|
+
path.startsWith('references/') ||
|
|
1376
|
+
(!hasSkill && path.toLowerCase() === 'skill.md') ||
|
|
1377
|
+
(!hasMetadata && path.toLowerCase() === 'agents/openai.yaml')
|
|
1378
|
+
) {
|
|
1379
|
+
continue
|
|
1380
|
+
}
|
|
1381
|
+
violations.push(
|
|
1382
|
+
createPolicyViolation(
|
|
1383
|
+
'skill',
|
|
1384
|
+
`${base}/${path}`,
|
|
1385
|
+
'skill directory contains only agents/ and references/ directories',
|
|
1386
|
+
),
|
|
1387
|
+
)
|
|
1388
|
+
continue
|
|
1389
|
+
}
|
|
1390
|
+
if (!isPolicyFile(directory, path)) continue
|
|
1301
1391
|
const file = basename(path).toLowerCase()
|
|
1302
|
-
if (
|
|
1392
|
+
if (file === 'readme.md' || file === 'changelog.md') {
|
|
1303
1393
|
violations.push(
|
|
1304
1394
|
createPolicyViolation(
|
|
1305
1395
|
'skill',
|
|
@@ -1307,7 +1397,26 @@ export function inspectSkill(root: string, name: string): readonly PolicyViolati
|
|
|
1307
1397
|
'skill directory contains no README.md or CHANGELOG.md',
|
|
1308
1398
|
),
|
|
1309
1399
|
)
|
|
1400
|
+
continue
|
|
1310
1401
|
}
|
|
1402
|
+
if (
|
|
1403
|
+
path === 'SKILL.md' ||
|
|
1404
|
+
path === 'agents/openai.yaml' ||
|
|
1405
|
+
/^references\/[^/]+\.md$/u.test(path) ||
|
|
1406
|
+
(!hasSkill &&
|
|
1407
|
+
(path.toLowerCase() === 'skill.md' || path.toLowerCase().startsWith('skill.md/'))) ||
|
|
1408
|
+
(!hasMetadata && path.toLowerCase() === 'agents/openai.yaml') ||
|
|
1409
|
+
(path.startsWith('references/') && path.slice('references/'.length).includes('/'))
|
|
1410
|
+
) {
|
|
1411
|
+
continue
|
|
1412
|
+
}
|
|
1413
|
+
violations.push(
|
|
1414
|
+
createPolicyViolation(
|
|
1415
|
+
'skill',
|
|
1416
|
+
`${base}/${path}`,
|
|
1417
|
+
'skill directory contains only SKILL.md, agents/openai.yaml, and references/*.md',
|
|
1418
|
+
),
|
|
1419
|
+
)
|
|
1311
1420
|
}
|
|
1312
1421
|
}
|
|
1313
1422
|
return violations
|
|
@@ -1400,6 +1509,7 @@ export function inspectBridge(root: string, name: string): readonly PolicyViolat
|
|
|
1400
1509
|
),
|
|
1401
1510
|
)
|
|
1402
1511
|
}
|
|
1512
|
+
violations.push(...inspectSkillTemplateTODOs('bridge', bridgePath, content))
|
|
1403
1513
|
if (resolvePolicyDirectory(root, `${bridgeBase}/references`) !== undefined) {
|
|
1404
1514
|
violations.push(
|
|
1405
1515
|
createPolicyViolation(
|
|
@@ -1482,6 +1592,11 @@ export function inspectPolicyControl(control: PolicyControl): readonly PolicyVio
|
|
|
1482
1592
|
for (const file of control.files) {
|
|
1483
1593
|
scratch.write(file.path, file.content)
|
|
1484
1594
|
}
|
|
1595
|
+
for (const directory of control.directories ?? []) {
|
|
1596
|
+
const marker = `${directory}/.policy-control`
|
|
1597
|
+
scratch.write(marker, '')
|
|
1598
|
+
rmSync(join(scratch.path, marker))
|
|
1599
|
+
}
|
|
1485
1600
|
if (control.rule === 'skill') return inspectSkillFamily(scratch.path)
|
|
1486
1601
|
if (control.rule === 'bridge') return inspectSkillBridges(scratch.path)
|
|
1487
1602
|
return inspectPolicyWorkspace(scratch.path)
|
|
@@ -1856,6 +1971,52 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
1856
1971
|
{ path: '.agents/skills/sample/references/orphan.md', content: '# Orphan\n' },
|
|
1857
1972
|
],
|
|
1858
1973
|
},
|
|
1974
|
+
{
|
|
1975
|
+
label: 'rejects a template TODO in skill prose',
|
|
1976
|
+
membership:
|
|
1977
|
+
'TODO occurrences outside inline backtick spans and fences indented no more than three spaces in canonical SKILL.md files and the references/*.md files they name',
|
|
1978
|
+
rule: 'skill',
|
|
1979
|
+
message: 'skill documents contain no template TODOs',
|
|
1980
|
+
files: [
|
|
1981
|
+
{
|
|
1982
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
1983
|
+
content: `${SKILL_POLICY_TEXT}\nTODO: describe the workflow\n`,
|
|
1984
|
+
},
|
|
1985
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
1986
|
+
],
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
label: 'rejects a template TODO in a CR-only skill reference',
|
|
1990
|
+
membership:
|
|
1991
|
+
'TODO occurrences outside inline backtick spans and fenced code blocks in named canonical skill references using CR line endings',
|
|
1992
|
+
rule: 'skill',
|
|
1993
|
+
line: 5,
|
|
1994
|
+
message: 'skill documents contain no template TODOs',
|
|
1995
|
+
files: [
|
|
1996
|
+
{ path: '.agents/skills/sample/SKILL.md', content: SKILL_REFERENCE_TEXT },
|
|
1997
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
1998
|
+
{
|
|
1999
|
+
path: '.agents/skills/sample/references/example.md',
|
|
2000
|
+
content: '# Example\r```text\rTODO: fenced\r```\rTODO: describe the workflow\r',
|
|
2001
|
+
},
|
|
2002
|
+
],
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
label: 'rejects a template TODO in a four-space-indented fence opener',
|
|
2006
|
+
membership:
|
|
2007
|
+
'TODO occurrences inside a four-space-indented fence opener and closer, which forms an indented code block outside the fence population, in discovered skill documents',
|
|
2008
|
+
rule: 'skill',
|
|
2009
|
+
message: 'skill documents contain no template TODOs',
|
|
2010
|
+
files: [
|
|
2011
|
+
{
|
|
2012
|
+
path: '.agents/skills/sample/SKILL.md',
|
|
2013
|
+
content:
|
|
2014
|
+
SKILL_POLICY_TEXT +
|
|
2015
|
+
'\n3. Return the verdict.\n\n ```text\n TODO: describe the workflow\n ```\n',
|
|
2016
|
+
},
|
|
2017
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2018
|
+
],
|
|
2019
|
+
},
|
|
1859
2020
|
{
|
|
1860
2021
|
label: 'rejects a nested references directory',
|
|
1861
2022
|
membership: 'directories directly beneath a discovered skill references directory',
|
|
@@ -1875,8 +2036,30 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
1875
2036
|
files: [
|
|
1876
2037
|
{ path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
|
|
1877
2038
|
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
1878
|
-
{ path: '.agents/skills/sample/
|
|
2039
|
+
{ path: '.agents/skills/sample/CHANGELOG.MD', content: '# Changes\n' },
|
|
2040
|
+
],
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
label: 'rejects a non-contract file in a skill directory',
|
|
2044
|
+
membership: 'regular files at any depth inside a discovered skill directory',
|
|
2045
|
+
rule: 'skill',
|
|
2046
|
+
message: 'skill directory contains only SKILL.md, agents/openai.yaml, and references/*.md',
|
|
2047
|
+
files: [
|
|
2048
|
+
{ path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
|
|
2049
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2050
|
+
{ path: '.agents/skills/sample/run.sh', content: '#!/bin/sh\n' },
|
|
2051
|
+
],
|
|
2052
|
+
},
|
|
2053
|
+
{
|
|
2054
|
+
label: 'rejects an empty non-contract directory in a skill directory',
|
|
2055
|
+
membership: 'directories at any depth inside a discovered skill directory',
|
|
2056
|
+
rule: 'skill',
|
|
2057
|
+
message: 'skill directory contains only agents/ and references/ directories',
|
|
2058
|
+
files: [
|
|
2059
|
+
{ path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
|
|
2060
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
1879
2061
|
],
|
|
2062
|
+
directories: ['.agents/skills/sample/assets'],
|
|
1880
2063
|
},
|
|
1881
2064
|
{
|
|
1882
2065
|
label: 'rejects a missing exact-case SKILL.md',
|
|
@@ -2048,6 +2231,20 @@ export const BRIDGE_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
|
|
|
2048
2231
|
},
|
|
2049
2232
|
],
|
|
2050
2233
|
},
|
|
2234
|
+
{
|
|
2235
|
+
label: 'rejects a template TODO in bridge prose',
|
|
2236
|
+
membership:
|
|
2237
|
+
'TODO occurrences outside inline backtick spans and fenced code blocks in exact-case bridge SKILL.md files shared with the canonical family',
|
|
2238
|
+
rule: 'bridge',
|
|
2239
|
+
message: 'skill documents contain no template TODOs',
|
|
2240
|
+
files: [
|
|
2241
|
+
{ path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
|
|
2242
|
+
{
|
|
2243
|
+
path: '.claude/skills/sample/SKILL.md',
|
|
2244
|
+
content: `${SKILL_BRIDGE_TEXT}\nTODO: describe the bridge\n`,
|
|
2245
|
+
},
|
|
2246
|
+
],
|
|
2247
|
+
},
|
|
2051
2248
|
{
|
|
2052
2249
|
label: 'rejects a references directory owned by a provider bridge',
|
|
2053
2250
|
membership: 'shared provider bridge directories',
|
|
@@ -2087,16 +2284,33 @@ export const SKILL_POLICY_FOLDED: PolicyControl = Object.freeze({
|
|
|
2087
2284
|
],
|
|
2088
2285
|
})
|
|
2089
2286
|
|
|
2090
|
-
/** A
|
|
2287
|
+
/** A healthy skill reference whose prose carries the documented backticked TODO form. */
|
|
2091
2288
|
export const SKILL_POLICY_BACKTICKED: PolicyControl = Object.freeze({
|
|
2092
|
-
label: 'accepts a backticked
|
|
2093
|
-
membership: '
|
|
2289
|
+
label: 'accepts a backticked TODO in skill prose',
|
|
2290
|
+
membership: 'TODO occurrences inside matched inline backtick spans in discovered skill documents',
|
|
2291
|
+
rule: 'skill',
|
|
2292
|
+
files: [
|
|
2293
|
+
{ path: '.agents/skills/sample/SKILL.md', content: SKILL_REFERENCE_TEXT },
|
|
2294
|
+
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2295
|
+
{
|
|
2296
|
+
path: '.agents/skills/sample/references/example.md',
|
|
2297
|
+
content: '- every `TODO`, deferred branch, placeholder, or documented omission in scope.\n',
|
|
2298
|
+
},
|
|
2299
|
+
],
|
|
2300
|
+
})
|
|
2301
|
+
|
|
2302
|
+
/** A healthy skill whose fenced example carries a template-TODO spelling. */
|
|
2303
|
+
export const SKILL_POLICY_FENCED: PolicyControl = Object.freeze({
|
|
2304
|
+
label: 'accepts a TODO in a three-space-indented fenced skill example',
|
|
2305
|
+
membership:
|
|
2306
|
+
'TODO occurrences inside fenced code blocks indented no more than three spaces in discovered skill documents',
|
|
2094
2307
|
rule: 'skill',
|
|
2095
2308
|
files: [
|
|
2096
2309
|
{
|
|
2097
2310
|
path: '.agents/skills/sample/SKILL.md',
|
|
2098
2311
|
content:
|
|
2099
|
-
|
|
2312
|
+
SKILL_POLICY_TEXT +
|
|
2313
|
+
'\n3. Return the verdict.\n\n ```text\n TODO: describe the workflow\n ```\n',
|
|
2100
2314
|
},
|
|
2101
2315
|
{ path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
|
|
2102
2316
|
],
|