@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.
@@ -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
- { readonly include: string; readonly setup: readonly string[] }
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', { include: 'tmp/probe/**/*.test.ts', setup: ['./tests/setup.ts'] })
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
- { readonly include: string; readonly setup: readonly string[] }
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(root, '.oxlintrc.json')
761
+ const config = resolve(scratch.path, '.oxlintrc.json')
625
762
  const violations = spawnSync(
626
763
  process.execPath,
627
- [binary, '--config', config, '--format', 'json', resolve(scratch.path, 'violations')],
628
- { cwd: root, encoding: 'utf8', timeout: 15_000 },
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', resolve(scratch.path, 'clean')],
633
- { cwd: root, encoding: 'utf8', timeout: 15_000 },
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
- if (!isPolicyFile(root, skill)) {
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
- if (!isPolicyFile(root, metadata)) {
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 ((file === 'readme.md' || file === 'changelog.md') && isPolicyFile(directory, path)) {
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/docs/CHANGELOG.MD', content: '# Changes\n' },
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 trigger sentence whose first subject is a backticked command token. */
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 token after Use',
2093
- membership: 'single-line descriptions in discovered skill frontmatter',
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
- '---\nname: sample\ndescription: Use `--app` when a policy fixture needs it.\n---\n\n# Skill\n',
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
  ],