@orkestrel/scaffold 0.0.108 → 0.0.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
  9. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
  10. package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
  11. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
  12. package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
  13. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
  14. package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
  15. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
  16. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
  17. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
  18. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
  19. package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
  20. package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
  21. package/dist/agents/templates/brief.md +1 -1
  22. package/dist/bin/main.js +51 -46
  23. package/dist/bin/main.js.map +1 -1
  24. package/dist/host/AGENTS.md +23 -21
  25. package/dist/host/agents/orchestration.md +43 -18
  26. package/dist/host/agents/platforms/windows.md +3 -3
  27. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  28. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  29. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  30. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  31. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  32. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  33. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  34. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  35. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  36. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
  37. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
  38. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
  48. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  49. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  50. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  51. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  52. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  53. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  54. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  55. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  56. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  58. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  59. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  60. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  63. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  64. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  65. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  66. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  67. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  68. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
  69. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
  70. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
  71. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
  72. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  73. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  74. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  75. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  76. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  77. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  78. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  79. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
  80. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  81. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  82. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  83. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  86. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  87. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  88. package/dist/host/agents/templates/brief.md +1 -1
  89. package/dist/host/agents/transports/claude-cli.md +3 -3
  90. package/dist/host/agents/transports/codex.md +2 -7
  91. package/dist/host/agents/transports/cursor.md +2 -3
  92. package/dist/host/claude/AGENTS.md +2 -2
  93. package/dist/host/claude/agents/analyst.md +3 -3
  94. package/dist/host/claude/agents/astra.md +3 -3
  95. package/dist/host/claude/agents/builder.md +1 -1
  96. package/dist/host/claude/agents/checker.md +1 -1
  97. package/dist/host/claude/agents/distiller.md +1 -1
  98. package/dist/host/claude/agents/grok.md +1 -1
  99. package/dist/host/claude/agents/opus.md +2 -2
  100. package/dist/host/claude/agents/orkestrel.md +57 -90
  101. package/dist/host/claude/agents/researcher.md +1 -1
  102. package/dist/host/claude/agents/reviewer.md +4 -0
  103. package/dist/host/claude/agents/scout.md +1 -1
  104. package/dist/host/claude/agents/verifier.md +1 -1
  105. package/dist/host/claude/rules/application.md +14 -21
  106. package/dist/host/claude/rules/architecture.md +34 -44
  107. package/dist/host/claude/rules/browser.md +3 -4
  108. package/dist/host/claude/rules/campaign.md +2 -2
  109. package/dist/host/claude/rules/documentation.md +25 -12
  110. package/dist/host/claude/rules/names.md +9 -4
  111. package/dist/host/claude/rules/patterns.md +6 -7
  112. package/dist/host/claude/rules/portability.md +1 -1
  113. package/dist/host/claude/rules/quality.md +4 -5
  114. package/dist/host/claude/rules/styles.md +2 -2
  115. package/dist/host/claude/rules/tests.md +57 -114
  116. package/dist/host/claude/rules/typescript.md +7 -9
  117. package/dist/host/claude/rules/workspace.md +21 -19
  118. package/dist/host/claude/rules/writing.md +2 -5
  119. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  120. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  121. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  122. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  123. package/dist/host/codex/agents/analyst.toml +9 -3
  124. package/dist/host/codex/agents/astra.toml +3 -4
  125. package/dist/host/codex/agents/builder.toml +1 -1
  126. package/dist/host/codex/agents/opus.toml +3 -4
  127. package/dist/host/codex/agents/orkestrel.toml +1 -1
  128. package/dist/host/codex/agents/researcher.toml +2 -2
  129. package/dist/host/codex/agents/verifier.toml +1 -1
  130. package/dist/host/configs/helpers.ts +21 -1
  131. package/dist/host/configs/policy.ts +299 -15
  132. package/dist/host/dotfiles/oxlintrc.json +137 -3
  133. package/dist/host/guides/agent.md +111 -134
  134. package/dist/host/guides/capture.md +801 -0
  135. package/dist/host/guides/probe.md +27 -8
  136. package/dist/host/guides/scaffold.md +96 -18
  137. package/dist/host/guides/study.md +534 -440
  138. package/dist/host/guides/test.md +111 -44
  139. package/dist/host/manifest.json +160 -240
  140. package/dist/host/tests/config.test.ts +971 -187
  141. package/dist/host/tests/policy.test.ts +127 -1
  142. package/dist/host/tests/setupPolicy.ts +893 -33
  143. package/dist/src/core/index.cjs +214 -86
  144. package/dist/src/core/index.cjs.map +1 -1
  145. package/dist/src/core/index.d.cts +122 -38
  146. package/dist/src/core/index.d.ts +122 -38
  147. package/dist/src/core/index.js +209 -85
  148. package/dist/src/core/index.js.map +1 -1
  149. package/dist/src/server/index.cjs +20 -27
  150. package/dist/src/server/index.cjs.map +1 -1
  151. package/dist/src/server/index.js +21 -28
  152. package/dist/src/server/index.js.map +1 -1
  153. package/package.json +11 -11
  154. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  155. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  156. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  157. package/dist/host/guides/supervisor.md +0 -4890
@@ -1,4 +1,11 @@
1
1
  import type { Blueprint } from '@orkestrel/scaffold'
2
+ import type {
3
+ PolicyComment,
4
+ PolicyContext,
5
+ PolicyDiagnostic,
6
+ PolicyExpression,
7
+ PolicyScopeCode,
8
+ } from '../configs/policy.js'
2
9
  import {
3
10
  createGuide,
4
11
  extractSourceLines,
@@ -25,6 +32,7 @@ import {
25
32
  import { tmpdir } from 'node:os'
26
33
  import { basename, dirname, join, matchesGlob, relative as relativePath, resolve } from 'node:path'
27
34
  import { fileURLToPath } from 'node:url'
35
+ import { isDeepStrictEqual } from 'node:util'
28
36
  import { parseAst, parseSync } from 'vite'
29
37
  import { stripPolicyCode, textToPolicyHits } from '../configs/policy.js'
30
38
 
@@ -38,6 +46,7 @@ export type PolicyRule =
38
46
  | 'skill'
39
47
  | 'surface'
40
48
  | 'suppression'
49
+ | 'text'
41
50
 
42
51
  /** Describes one workspace file a physical control writes. */
43
52
  export interface PolicySource {
@@ -430,6 +439,72 @@ export function createPolicyRevoked(): object {
430
439
  return proxy
431
440
  }
432
441
 
442
+ /**
443
+ * Creates a producer of revoked proxies over what another producer creates.
444
+ *
445
+ * @param entry - The producer whose value each proxy wraps.
446
+ * @returns A producer whose value refuses every read, which fails a reader once it holds the entry.
447
+ */
448
+ export function createPolicyRefused<T extends object>(entry: () => T): () => T {
449
+ return () => {
450
+ const { proxy, revoke } = Proxy.revocable(entry(), {})
451
+ revoke()
452
+ return proxy
453
+ }
454
+ }
455
+
456
+ /** Creates a lint source over text and its comments that declares no variable. */
457
+ export function createPolicySource(
458
+ text: string,
459
+ comments: readonly PolicyComment[] = [],
460
+ ): PolicyScopeCode {
461
+ return { text, getAllComments: () => comments, getDeclaredVariables: () => [] }
462
+ }
463
+
464
+ /** Creates a lint context over an empty source that appends each diagnostic to the given log. */
465
+ export function createPolicyContext(
466
+ filename: string,
467
+ cwd: string,
468
+ reports: PolicyDiagnostic[],
469
+ ): PolicyContext {
470
+ return {
471
+ filename,
472
+ cwd,
473
+ sourceCode: createPolicySource(''),
474
+ report: (diagnostic) => {
475
+ reports.push(diagnostic)
476
+ },
477
+ }
478
+ }
479
+
480
+ /** Creates a variable declarator named `run` whose value is an arrow function. */
481
+ export function createPolicyDeclarator(): PolicyExpression {
482
+ return {
483
+ type: 'VariableDeclarator',
484
+ range: [0, 0],
485
+ id: { type: 'Identifier', range: [0, 0], name: 'run' },
486
+ init: { type: 'ArrowFunctionExpression', range: [0, 0] },
487
+ }
488
+ }
489
+
490
+ /** Creates a named export of a function declaration named `run`. */
491
+ export function createPolicyExport(): PolicyExpression {
492
+ return {
493
+ type: 'ExportNamedDeclaration',
494
+ range: [47, 63],
495
+ declaration: {
496
+ type: 'FunctionDeclaration',
497
+ range: [0, 0],
498
+ id: { type: 'Identifier', range: [0, 0], name: 'run' },
499
+ },
500
+ }
501
+ }
502
+
503
+ /** Creates the one-sentence block comment a doc reader reads above an export. */
504
+ export function createPolicyComment(): PolicyComment {
505
+ return { type: 'Block', value: '* One.', range: [0, 11] }
506
+ }
507
+
433
508
  /**
434
509
  * Creates one case per list shape for a reader of a caller-supplied list.
435
510
  *
@@ -562,7 +637,7 @@ export const POLICY_LAZY_CASES: readonly PolicyLazyCase[] = Object.freeze([
562
637
  'inspectPolicyConfiguration overrides',
563
638
  (list: unknown[]) =>
564
639
  inspectPolicyConfiguration({
565
- rules: Object.fromEntries(POLICY_WIRING_RULES.map((rule) => [rule, 'error'])),
640
+ rules: POLICY_WIRING_SETTINGS,
566
641
  overrides: list,
567
642
  }),
568
643
  { valid: () => ({}), refusal: createPolicyRevoked },
@@ -581,7 +656,7 @@ export const POLICY_LAZY_CASES: readonly PolicyLazyCase[] = Object.freeze([
581
656
  'inspectPolicyConfiguration ignorePatterns',
582
657
  (list: unknown[]) =>
583
658
  inspectPolicyConfiguration({
584
- rules: Object.fromEntries(POLICY_WIRING_RULES.map((rule) => [rule, 'error'])),
659
+ rules: POLICY_WIRING_SETTINGS,
585
660
  ignorePatterns: list,
586
661
  }),
587
662
  // The reader skips every entry that is not a string, so no entry refuses it and no row uses the
@@ -824,30 +899,67 @@ export const POLICY_SUPPRESSION_DIRECTIVE = ['oxlint', '-disable'].join('')
824
899
  /** Names the formatter directive the text sweep refuses, composed so the instrument does not report itself. */
825
900
  export const POLICY_FORMATTER_DIRECTIVE = ['prettier', '-ignore'].join('')
826
901
 
827
- /** Matches the source, test, config, script, and sheet files inspected for lint and formatter suppression directives. */
902
+ /** Matches the source, test, config, script, skill script, and sheet files inspected for lint and formatter suppression directives. */
828
903
  export const POLICY_SUPPRESSION_GLOB: readonly string[] = Object.freeze([
829
904
  '{src,app,tests,configs,scripts}/**/*.{cjs,css,cts,js,jsx,mjs,mts,scss,ts,tsx,vue}',
830
905
  '*.{cjs,css,cts,js,jsx,mjs,mts,scss,ts,tsx,vue}',
906
+ '.agents/skills/*/scripts/*.ts',
831
907
  ])
832
908
 
833
- /** Lists the rules whose workspace-wide lint wiring must not be weakened by configuration. */
834
- export const POLICY_WIRING_RULES: readonly string[] = Object.freeze([
835
- 'policy/no-mocking',
836
- 'policy/no-keyword-privacy',
837
- 'policy/no-malformed-summary',
838
- 'policy/no-banned-term',
839
- 'typescript/parameter-properties',
840
- 'typescript/explicit-member-accessibility',
909
+ /**
910
+ * Matches the instruction, configuration, and root text files the text sweep reads beside the
911
+ * authored Markdown and the suppression population.
912
+ */
913
+ export const POLICY_TEXT_GLOB: readonly string[] = Object.freeze([
914
+ '{.agents,.claude,.codex,.cursor}/**/*.{toml,yaml,yml,mdc,json}',
915
+ '*.json',
916
+ '.*.json',
917
+ '.gitattributes',
918
+ '.prettierignore',
841
919
  ])
842
920
 
921
+ /**
922
+ * Maps each rule whose workspace-wide lint wiring must not be weakened to the whole top-level
923
+ * setting the shipped configuration declares for it.
924
+ *
925
+ * @remarks
926
+ * A setting carries its options as well as its severity, so a rule listed at `error` with an option
927
+ * that switches its check off differs from the value declared here.
928
+ */
929
+ export const POLICY_WIRING_SETTINGS: Readonly<Record<string, unknown>> = Object.freeze({
930
+ 'policy/no-mocking': 'error',
931
+ 'policy/no-keyword-privacy': 'error',
932
+ 'policy/no-let': 'error',
933
+ 'policy/no-malformed-summary': 'error',
934
+ 'policy/no-banned-term': 'error',
935
+ 'no-var': 'error',
936
+ 'prefer-const': 'error',
937
+ 'no-param-reassign': 'error',
938
+ 'no-ex-assign': 'error',
939
+ 'vitest/no-identical-title': 'error',
940
+ 'vitest/no-focused-tests': 'error',
941
+ 'vitest/no-disabled-tests': 'error',
942
+ 'typescript/parameter-properties': 'error',
943
+ 'typescript/explicit-member-accessibility': ['error', { accessibility: 'no-public' }],
944
+ })
945
+
946
+ /** Lists the rules whose workspace-wide lint wiring must not be weakened by configuration. */
947
+ export const POLICY_WIRING_RULES: readonly string[] = Object.freeze(
948
+ Object.keys(POLICY_WIRING_SETTINGS),
949
+ )
950
+
843
951
  /** Lists the linted workspace roots that ignore patterns must not reach. */
844
952
  export const POLICY_WIRING_ROOTS: readonly string[] = Object.freeze([
845
953
  'src',
846
954
  'app',
847
955
  'tests',
848
956
  'configs',
957
+ '.agents',
849
958
  ])
850
959
 
960
+ /** Matches a character that makes a path segment a glob rather than a literal name. */
961
+ export const POLICY_GLOB_CHARACTER = /[*?[\]{}]/u
962
+
851
963
  /** Matches either lint suppression token or the formatter directive the text sweep refuses. */
852
964
  export const POLICY_SUPPRESSION_PATTERN = new RegExp(
853
965
  [['eslint', '-disable'].join(''), POLICY_SUPPRESSION_DIRECTIVE, POLICY_FORMATTER_DIRECTIVE].join(
@@ -856,6 +968,24 @@ export const POLICY_SUPPRESSION_PATTERN = new RegExp(
856
968
  'u',
857
969
  )
858
970
 
971
+ /**
972
+ * Holds the decoder the text sweep reads bytes through.
973
+ *
974
+ * @remarks
975
+ * `fatal` refuses a byte sequence UTF-8 does not carry, and `ignoreBOM` keeps a leading byte-order
976
+ * mark in the decoded text, where the default would drop it before the sweep could see it.
977
+ */
978
+ export const POLICY_TEXT_DECODER = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true })
979
+
980
+ /** Lists the control code points a text file may carry: tab, line feed, and carriage return. */
981
+ export const POLICY_TEXT_CONTROLS: readonly number[] = Object.freeze([0x09, 0x0a, 0x0d])
982
+
983
+ /** Holds the code point of the replacement character a failed decode leaves behind. */
984
+ export const POLICY_TEXT_REPLACEMENT = 0xfffd
985
+
986
+ /** Holds the code point of the byte-order mark. */
987
+ export const POLICY_TEXT_MARK = 0xfeff
988
+
859
989
  /**
860
990
  * Matches the workspace-authored path population inspected for a name a Windows checkout cannot
861
991
  * hold.
@@ -899,6 +1029,19 @@ export const POLICY_RESERVED_PATTERN = /[<>:"|?*]/u
899
1029
  /** Matches a shell script named as a complete path token inside a manifest script. */
900
1030
  export const POLICY_SHELL_PATTERN = /\.sh\b/u
901
1031
 
1032
+ /** Lists the extensions of the shell, PowerShell, Python, and batch files a workspace keeps under one directory. */
1033
+ export const POLICY_SHELL_EXTENSIONS: readonly string[] = Object.freeze([
1034
+ '.sh',
1035
+ '.bash',
1036
+ '.ps1',
1037
+ '.py',
1038
+ '.bat',
1039
+ '.cmd',
1040
+ ])
1041
+
1042
+ /** Names the one directory whose files are Cloud setup hooks, the shell files a workspace admits. */
1043
+ export const POLICY_SHELL_ROOT = 'scripts'
1044
+
902
1045
  /** Names the directory whose direct Markdown files form the complete rule family. */
903
1046
  export const POLICY_RULE_ROOT = '.claude/rules'
904
1047
 
@@ -1202,6 +1345,165 @@ export function inspectPolicySuppressions(root: string): readonly PolicyViolatio
1202
1345
  )
1203
1346
  }
1204
1347
 
1348
+ /**
1349
+ * Decodes file bytes as UTF-8 without throwing.
1350
+ *
1351
+ * @param bytes - The raw file bytes.
1352
+ * @returns The decoded text with any leading byte-order mark kept, or `undefined` when the bytes
1353
+ * are not valid UTF-8.
1354
+ */
1355
+ export function parsePolicyText(bytes: Uint8Array): string | undefined {
1356
+ try {
1357
+ return POLICY_TEXT_DECODER.decode(bytes)
1358
+ } catch {
1359
+ return undefined
1360
+ }
1361
+ }
1362
+
1363
+ /**
1364
+ * Reports whether one code point is a character the text sweep refuses.
1365
+ *
1366
+ * @remarks
1367
+ * The sweep refuses the replacement character, the byte-order mark, and every C0 or C1 control
1368
+ * character other than the three {@link POLICY_TEXT_CONTROLS} lists.
1369
+ *
1370
+ * @param code - The code point to judge.
1371
+ * @returns True if the code point is refused; false otherwise.
1372
+ */
1373
+ export function matchesPolicyCharacter(code: number): boolean {
1374
+ if (code === POLICY_TEXT_REPLACEMENT || code === POLICY_TEXT_MARK) return true
1375
+ if (code < 0x20) return !POLICY_TEXT_CONTROLS.includes(code)
1376
+ return code >= 0x7f && code <= 0x9f
1377
+ }
1378
+
1379
+ /**
1380
+ * Renders one code point as the `\u` escape a violation names it by.
1381
+ *
1382
+ * @param code - The code point to render.
1383
+ * @returns The escape, four upper-case hexadecimal digits long.
1384
+ */
1385
+ export function renderPolicyCharacter(code: number): string {
1386
+ return `\\u${code.toString(16).toUpperCase().padStart(4, '0')}`
1387
+ }
1388
+
1389
+ /**
1390
+ * Inspects one file's bytes for text that is not clean UTF-8.
1391
+ *
1392
+ * @remarks
1393
+ * Bytes that do not decode report once for the file. Decoded text reports each refused character
1394
+ * once per line, so a line that repeats a character names it once.
1395
+ *
1396
+ * @param path - The workspace-relative path the bytes belong to.
1397
+ * @param bytes - The raw file bytes.
1398
+ * @returns Every text violation in line order.
1399
+ */
1400
+ export function inspectPolicyBytes(path: string, bytes: Uint8Array): readonly PolicyViolation[] {
1401
+ const text = parsePolicyText(bytes)
1402
+ if (text === undefined) return [createPolicyViolation('text', path, 'text decodes as UTF-8')]
1403
+ return text
1404
+ .split(/\r\n|\n/u)
1405
+ .flatMap((line, index) =>
1406
+ [...new Set([...line].map((character) => character.codePointAt(0) ?? 0))]
1407
+ .filter(matchesPolicyCharacter)
1408
+ .map((code) =>
1409
+ createPolicyViolation(
1410
+ 'text',
1411
+ path,
1412
+ `text carries no replacement, byte-order, or control character: ${renderPolicyCharacter(code)}`,
1413
+ index + 1,
1414
+ ),
1415
+ ),
1416
+ )
1417
+ }
1418
+
1419
+ /**
1420
+ * Collects the workspace-relative paths of the authored and source text the sweep reads.
1421
+ *
1422
+ * @param root - The workspace root to read.
1423
+ * @returns The authored Markdown population joined with the source population the suppression
1424
+ * sweep reads and the instruction, configuration, and root text population
1425
+ * {@link POLICY_TEXT_GLOB} names, sorted by path. The three populations share no extension, so no
1426
+ * path repeats.
1427
+ */
1428
+ export function collectPolicyTextPaths(root: string): readonly string[] {
1429
+ return [
1430
+ ...new Set([
1431
+ ...readPolicyProse(root),
1432
+ ...globSync(POLICY_SUPPRESSION_GLOB, { cwd: root }).map(normalizePolicyPath),
1433
+ ...globSync(POLICY_TEXT_GLOB, { cwd: root }).map(normalizePolicyPath),
1434
+ ]),
1435
+ ].sort()
1436
+ }
1437
+
1438
+ /**
1439
+ * Inspects every authored and source text file for text that is not clean UTF-8.
1440
+ *
1441
+ * @param root - The workspace root to inspect.
1442
+ * @returns Every text violation in path and line order.
1443
+ */
1444
+ export function inspectPolicyText(root: string): readonly PolicyViolation[] {
1445
+ return collectPolicyTextPaths(root).flatMap((path) =>
1446
+ inspectPolicyBytes(path, readFileSync(join(root, path))),
1447
+ )
1448
+ }
1449
+
1450
+ /**
1451
+ * Collects the literal path segments the files globs of a configuration's overrides route through.
1452
+ *
1453
+ * @remarks
1454
+ * A segment that carries a glob character is not literal, and neither is an empty or dot segment.
1455
+ *
1456
+ * @param overrides - The parsed `overrides` value of an Oxlint configuration.
1457
+ * @returns Each literal segment, in the order the globs declare them.
1458
+ */
1459
+ export function collectPolicyOverrideSegments(overrides: unknown): readonly string[] {
1460
+ return (Array.isArray(overrides) ? overrides : []).flatMap((override: unknown) => {
1461
+ const files: unknown = isPolicyRecord(override)
1462
+ ? Object.getOwnPropertyDescriptor(override, 'files')?.value
1463
+ : undefined
1464
+ return (Array.isArray(files) ? files : []).flatMap((glob: unknown) =>
1465
+ typeof glob === 'string'
1466
+ ? normalizePolicyPath(glob)
1467
+ .split('/')
1468
+ .filter(
1469
+ (segment) =>
1470
+ segment !== '' &&
1471
+ segment !== '.' &&
1472
+ segment !== '..' &&
1473
+ !POLICY_GLOB_CHARACTER.test(segment),
1474
+ )
1475
+ : [],
1476
+ )
1477
+ })
1478
+ }
1479
+
1480
+ /**
1481
+ * Checks whether an ignore pattern can silence files inside a linted workspace root.
1482
+ *
1483
+ * @remarks
1484
+ * Oxlint reads an ignore pattern with gitignore semantics. A pattern with a slash before its last
1485
+ * character is anchored to the configuration directory, so only its first segment can reach a
1486
+ * root. A pattern without one matches a name at any depth, so it reaches a root when it carries a
1487
+ * glob character or names a segment the overrides route through. A negation reaches nothing.
1488
+ *
1489
+ * @param pattern - One entry of the configuration's `ignorePatterns`.
1490
+ * @param overrides - The parsed `overrides` value of the same configuration.
1491
+ * @returns Whether the pattern can silence a guarded file.
1492
+ */
1493
+ export function isPolicyIgnoreReaching(pattern: string, overrides: unknown): boolean {
1494
+ if (pattern.startsWith('!')) return false
1495
+ const normalized = normalizePolicyPath(pattern)
1496
+ const [first = ''] = normalized.replace(/^\.\//u, '').replace(/^\//u, '').split('/')
1497
+ if (
1498
+ POLICY_WIRING_ROOTS.some((root) => first === root || (first !== '' && matchesGlob(root, first)))
1499
+ ) {
1500
+ return true
1501
+ }
1502
+ const name = normalized.replace(/\/$/u, '')
1503
+ if (name.includes('/')) return false
1504
+ return POLICY_GLOB_CHARACTER.test(name) || collectPolicyOverrideSegments(overrides).includes(name)
1505
+ }
1506
+
1205
1507
  /**
1206
1508
  * Inspects the lint configuration that keeps policy rules active across the workspace.
1207
1509
  *
@@ -1220,29 +1522,23 @@ export function inspectPolicyConfiguration(configuration: unknown): readonly str
1220
1522
  )?.value
1221
1523
  const overrides: unknown = Object.getOwnPropertyDescriptor(configuration, 'overrides')?.value
1222
1524
  return [
1223
- ...POLICY_WIRING_RULES.flatMap((rule) => {
1525
+ ...Object.entries(POLICY_WIRING_SETTINGS).flatMap(([rule, declared]) => {
1224
1526
  const setting = isPolicyRecord(rules)
1225
1527
  ? Object.getOwnPropertyDescriptor(rules, rule)?.value
1226
1528
  : undefined
1227
- const severity = Array.isArray(setting) ? setting[0] : setting
1228
- return severity === 'error' ? [] : [`${rule} must have top-level error severity`]
1529
+ return isDeepStrictEqual(setting, declared)
1530
+ ? []
1531
+ : [`${rule} must have the top-level setting ${JSON.stringify(declared)}`]
1229
1532
  }),
1230
1533
  ...(ignorePatterns !== undefined && !Array.isArray(ignorePatterns)
1231
1534
  ? ['ignorePatterns must be an array when declared']
1232
1535
  : // The mapper runs on each pattern as the iterator yields it, as the loop it replaced did.
1233
1536
  Iterator.from(Array.isArray(ignorePatterns) ? ignorePatterns : [])
1234
- .flatMap((pattern) => {
1235
- if (typeof pattern !== 'string' || pattern.startsWith('!')) return []
1236
- const normalized = normalizePolicyPath(pattern)
1237
- .replace(/^\.\//u, '')
1238
- .replace(/^\//u, '')
1239
- const [first = ''] = normalized.split('/')
1240
- return POLICY_WIRING_ROOTS.some(
1241
- (root) => first === root || (first !== '' && matchesGlob(root, first)),
1242
- )
1537
+ .flatMap((pattern) =>
1538
+ typeof pattern === 'string' && isPolicyIgnoreReaching(pattern, overrides)
1243
1539
  ? [`ignorePatterns must not reach ${pattern}`]
1244
- : []
1245
- })
1540
+ : [],
1541
+ )
1246
1542
  .toArray()),
1247
1543
  ...(overrides !== undefined && !Array.isArray(overrides)
1248
1544
  ? ['overrides must be an array when declared']
@@ -1293,9 +1589,15 @@ export function inspectPolicyWiring(
1293
1589
  return [{ record, declared: Array.isArray(files) ? [files.join(' ')] : [] }]
1294
1590
  })
1295
1591
  .toArray()
1592
+ // A name enables its rule only at error severity: the string, or the first member of an array.
1296
1593
  const enabled = new Set(
1297
1594
  [topRules, ...entries.map(({ record }) => record)].flatMap((record) =>
1298
- isPolicyRecord(record) ? Object.getOwnPropertyNames(record) : [],
1595
+ isPolicyRecord(record)
1596
+ ? Object.getOwnPropertyNames(record).filter((name) => {
1597
+ const setting = Object.getOwnPropertyDescriptor(record, name)?.value
1598
+ return (Array.isArray(setting) ? setting[0] : setting) === 'error'
1599
+ })
1600
+ : [],
1299
1601
  ),
1300
1602
  )
1301
1603
  const declaredPopulations = entries.flatMap(({ declared }) => declared)
@@ -1650,8 +1952,8 @@ export function inspectSkillTemplateTODOs(
1650
1952
  ? { fence: undefined, indexes: state.indexes }
1651
1953
  : state
1652
1954
  }
1653
- const opening = fenceLine.match(/^(`{3,}|~{3,})/u)?.[1]
1654
- if (opening !== undefined) return { fence: opening, indexes: state.indexes }
1955
+ const fence = fenceLine.match(/^(`{3,}|~{3,})/u)?.[1]
1956
+ if (fence !== undefined) return { fence: fence, indexes: state.indexes }
1655
1957
  return { fence: undefined, indexes: [...state.indexes, index] }
1656
1958
  },
1657
1959
  { fence: undefined, indexes: [] },
@@ -2111,6 +2413,74 @@ export function inspectSkillImports(
2111
2413
  })
2112
2414
  }
2113
2415
 
2416
+ /**
2417
+ * Collects every module specifier a syntax tree names, in source order.
2418
+ *
2419
+ * @remarks
2420
+ * A specifier is the literal source of an `import` or `export … from` declaration, or of an
2421
+ * `import()` expression anywhere in the tree. An `import()` whose source is no literal names none.
2422
+ *
2423
+ * @param node - The syntax tree, or any node or list of nodes within it.
2424
+ * @returns Each literal specifier the tree names.
2425
+ */
2426
+ export function collectPolicySpecifiers(node: unknown): readonly string[] {
2427
+ if (Array.isArray(node)) return node.flatMap((child: unknown) => collectPolicySpecifiers(child))
2428
+ if (!isPolicyRecord(node)) return []
2429
+ const type: unknown = Object.getOwnPropertyDescriptor(node, 'type')?.value
2430
+ const source: unknown = Object.getOwnPropertyDescriptor(node, 'source')?.value
2431
+ const value: unknown = isPolicyRecord(source)
2432
+ ? Object.getOwnPropertyDescriptor(source, 'value')?.value
2433
+ : undefined
2434
+ return [
2435
+ ...(typeof value === 'string' &&
2436
+ (type === 'ImportDeclaration' ||
2437
+ type === 'ExportAllDeclaration' ||
2438
+ type === 'ExportNamedDeclaration' ||
2439
+ type === 'ImportExpression')
2440
+ ? [value]
2441
+ : []),
2442
+ ...Object.values(node).flatMap((child: unknown) => collectPolicySpecifiers(child)),
2443
+ ]
2444
+ }
2445
+
2446
+ /**
2447
+ * Inspects one skill script proof for an import of a skill file other than the script it proves.
2448
+ *
2449
+ * @remarks
2450
+ * Each relative `import`, `export … from`, and literal `import()` specifier resolves against the
2451
+ * proof's directory. A script is self-contained and no file but its own proof imports it, so a
2452
+ * proof that reaches another script, or any other file beneath the skill family, shares a helper
2453
+ * the script law places in the test setup modules.
2454
+ *
2455
+ * @param root - The workspace root to inspect.
2456
+ * @param proof - The workspace-relative path of the proof.
2457
+ * @param script - The workspace-relative path of the script the proof mirrors.
2458
+ * @returns One violation per import of another skill file, in source order.
2459
+ */
2460
+ export function inspectSkillProof(
2461
+ root: string,
2462
+ proof: string,
2463
+ script: string,
2464
+ ): readonly PolicyViolation[] {
2465
+ if (!isPolicyFile(root, proof)) return []
2466
+ const directory = dirname(join(root, proof))
2467
+ return collectPolicySpecifiers(parseAst(readFileSync(join(root, proof), 'utf8'), { lang: 'ts' }))
2468
+ .filter((specifier) => specifier.startsWith('.'))
2469
+ .flatMap((specifier) => {
2470
+ const imported = normalizePolicyPath(relativePath(root, resolve(directory, specifier)))
2471
+ return imported.startsWith(`${SKILL_FAMILY_ROOT}/`) &&
2472
+ imported.replace(/\.js$/u, '.ts') !== script
2473
+ ? [
2474
+ createPolicyViolation(
2475
+ 'skill',
2476
+ proof,
2477
+ `skill proof imports no skill file but its own script: ${imported}`,
2478
+ ),
2479
+ ]
2480
+ : []
2481
+ })
2482
+ }
2483
+
2114
2484
  /**
2115
2485
  * Inspects one discovered skill's required files, metadata, token, references, and fenced imports.
2116
2486
  *
@@ -2259,6 +2629,7 @@ export function inspectSkill(
2259
2629
  ...(isPolicyFile(root, proof)
2260
2630
  ? []
2261
2631
  : [createPolicyViolation('skill', proof, `skill script has a mirrored proof: ${proof}`)]),
2632
+ ...inspectSkillProof(root, proof, `${base}/${script}`),
2262
2633
  ]
2263
2634
  })
2264
2635
  const foreign = (content === undefined ? [] : extractForeignSkillScripts(content, name)).flatMap(
@@ -2654,6 +3025,53 @@ export function inspectPolicyScripts(root: string): readonly PolicyViolation[] {
2654
3025
  )
2655
3026
  }
2656
3027
 
3028
+ /**
3029
+ * Inspects a file path population for a shell, PowerShell, Python, or batch file outside the scripts
3030
+ * directory, and for any file inside it other than a `.sh` file.
3031
+ *
3032
+ * @param paths - The workspace-relative file paths to inspect.
3033
+ * @returns One violation per refused path, in path order.
3034
+ */
3035
+ export function inspectPolicyShellPaths(paths: readonly string[]): readonly PolicyViolation[] {
3036
+ return paths.flatMap((path) => {
3037
+ const lower = normalizePolicyPath(path).toLowerCase()
3038
+ const inside = lower.startsWith(`${POLICY_SHELL_ROOT}/`)
3039
+ const shell = POLICY_SHELL_EXTENSIONS.some((extension) => lower.endsWith(extension))
3040
+ if (inside ? lower.endsWith('.sh') : !shell) return []
3041
+ return [
3042
+ createPolicyViolation(
3043
+ 'portability',
3044
+ path,
3045
+ inside
3046
+ ? `${POLICY_SHELL_ROOT} holds .sh files and nothing else`
3047
+ : `shell, PowerShell, Python, and batch files sit only in ${POLICY_SHELL_ROOT}`,
3048
+ ),
3049
+ ]
3050
+ })
3051
+ }
3052
+
3053
+ /**
3054
+ * Inspects the workspace-authored file population for a script no Windows host runs.
3055
+ *
3056
+ * @remarks
3057
+ * Only a path that could be refused is checked against the file system: a directory named like a
3058
+ * shell file, or a directory inside the scripts directory, is no file and is left to the other
3059
+ * sweeps.
3060
+ *
3061
+ * @param root - The workspace root to inspect.
3062
+ * @returns Every shell-file violation in path order.
3063
+ */
3064
+ export function inspectPolicyShells(root: string): readonly PolicyViolation[] {
3065
+ const candidates = readPolicyPaths(root).filter((path) => {
3066
+ const lower = path.toLowerCase()
3067
+ return (
3068
+ lower.startsWith(`${POLICY_SHELL_ROOT}/`) ||
3069
+ POLICY_SHELL_EXTENSIONS.some((extension) => lower.endsWith(extension))
3070
+ )
3071
+ })
3072
+ return inspectPolicyShellPaths(candidates.filter((path) => isPolicyFile(root, path)))
3073
+ }
3074
+
2657
3075
  /**
2658
3076
  * Reads every authored Markdown path in one workspace, sorted by path.
2659
3077
  *
@@ -2842,12 +3260,13 @@ export function readPolicyTerms(content: string): readonly string[] {
2842
3260
  * Inspects every host portability rule across one workspace.
2843
3261
  *
2844
3262
  * @param root - The workspace root to inspect.
2845
- * @returns Every rule-map, filename, and manifest-script violation.
3263
+ * @returns Every rule-map, filename, shell-file, and manifest-script violation.
2846
3264
  */
2847
3265
  export function inspectPolicyPortability(root: string): readonly PolicyViolation[] {
2848
3266
  return [
2849
3267
  ...inspectPolicyRuleMap(root),
2850
3268
  ...inspectPolicyFilenames(root),
3269
+ ...inspectPolicyShells(root),
2851
3270
  ...inspectPolicyScripts(root),
2852
3271
  ]
2853
3272
  }
@@ -3022,7 +3441,7 @@ export function readPolicySurface(root: string): PolicySurfacePopulation {
3022
3441
  : [createPolicyViolation('surface', path, POLICY_SURFACE_STYLES_MESSAGE, line)]
3023
3442
  })
3024
3443
  // Each barrel statement answers one violation, one star-export target, or neither.
3025
- const readings = barrels.flatMap(
3444
+ const results = barrels.flatMap(
3026
3445
  (path): ReadonlyArray<{ readonly violation?: PolicyViolation; readonly target?: string }> => {
3027
3446
  const text = files[path]
3028
3447
  if (text === undefined) return []
@@ -3079,7 +3498,7 @@ export function readPolicySurface(root: string): PolicySurfacePopulation {
3079
3498
  })
3080
3499
  },
3081
3500
  )
3082
- const targets = new Set(readings.flatMap(({ target }) => (target === undefined ? [] : [target])))
3501
+ const targets = new Set(results.flatMap(({ target }) => (target === undefined ? [] : [target])))
3083
3502
  const reads = [...targets].flatMap((path) => {
3084
3503
  const text = files[path]
3085
3504
  return text === undefined ? [] : [collectPolicyDeclarations(root, path, text)]
@@ -3088,7 +3507,7 @@ export function readPolicySurface(root: string): PolicySurfacePopulation {
3088
3507
  declarations: reads.flatMap((read) => read.declarations),
3089
3508
  violations: [
3090
3509
  ...entryViolations,
3091
- ...readings.flatMap(({ violation }) => (violation === undefined ? [] : [violation])),
3510
+ ...results.flatMap(({ violation }) => (violation === undefined ? [] : [violation])),
3092
3511
  ...reads.flatMap((read) => (read.violation === undefined ? [] : [read.violation])),
3093
3512
  ],
3094
3513
  }
@@ -3221,12 +3640,14 @@ export function inspectPolicySurface(root: string): readonly PolicyViolation[] {
3221
3640
  * Inspects every policy rule across one workspace.
3222
3641
  *
3223
3642
  * @param root - The workspace root to inspect.
3224
- * @returns Every mirror, suppression, skill, bridge, portability, prose, and surface violation.
3643
+ * @returns Every mirror, suppression, text, skill, bridge, portability, prose, and surface
3644
+ * violation.
3225
3645
  */
3226
3646
  export function inspectPolicyWorkspace(root: string): readonly PolicyViolation[] {
3227
3647
  return [
3228
3648
  ...inspectPolicyMirrors(root),
3229
3649
  ...inspectPolicySuppressions(root),
3650
+ ...inspectPolicyText(root),
3230
3651
  ...inspectSkillFamily(root),
3231
3652
  ...inspectSkillBridges(root),
3232
3653
  ...inspectPolicyPortability(root),
@@ -3277,6 +3698,17 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
3277
3698
  },
3278
3699
  ],
3279
3700
  },
3701
+ {
3702
+ label: 'rejects a suppression directive in a skill script',
3703
+ membership: 'skill scripts in the suppression population',
3704
+ rule: 'suppression',
3705
+ files: [
3706
+ {
3707
+ path: '.agents/skills/sample/scripts/control.ts',
3708
+ content: `// ${POLICY_SUPPRESSION_DIRECTIVE}\ndebugger\n`,
3709
+ },
3710
+ ],
3711
+ },
3280
3712
  {
3281
3713
  label: 'rejects a suppression directive in a root TSX file',
3282
3714
  membership: 'root code files in the suppression population',
@@ -3629,6 +4061,61 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
3629
4061
  },
3630
4062
  ],
3631
4063
  },
4064
+ {
4065
+ label: 'rejects a skill proof that imports a skill file other than its own script',
4066
+ membership: 'relative imports of skill proofs against the script each proof mirrors',
4067
+ rule: 'skill',
4068
+ files: [
4069
+ {
4070
+ path: '.agents/skills/sample/SKILL.md',
4071
+ content: SKILL_POLICY_TEXT + '\nRun `scripts/probe.ts`.\n',
4072
+ },
4073
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
4074
+ {
4075
+ path: '.agents/skills/sample/scripts/probe.ts',
4076
+ content: '// Usage: node .agents/skills/sample/scripts/probe.ts\n',
4077
+ },
4078
+ {
4079
+ path: 'tests/agents/skills/sample/scripts/probe.test.ts',
4080
+ content:
4081
+ "import { read } from '../../../../../.agents/skills/other/scripts/helpers.ts'\nvoid read\n",
4082
+ },
4083
+ ],
4084
+ violations: [
4085
+ {
4086
+ rule: 'skill',
4087
+ path: 'tests/agents/skills/sample/scripts/probe.test.ts',
4088
+ message:
4089
+ 'skill proof imports no skill file but its own script: .agents/skills/other/scripts/helpers.ts',
4090
+ },
4091
+ ],
4092
+ },
4093
+ {
4094
+ label: 'accepts a skill proof that imports its own script and a test setup module',
4095
+ membership: 'relative imports of skill proofs against the script each proof mirrors',
4096
+ rule: 'skill',
4097
+ files: [
4098
+ {
4099
+ path: '.agents/skills/sample/SKILL.md',
4100
+ content: SKILL_POLICY_TEXT + '\nRun `scripts/probe.ts`.\n',
4101
+ },
4102
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
4103
+ {
4104
+ path: '.agents/skills/sample/scripts/probe.ts',
4105
+ content: '// Usage: node .agents/skills/sample/scripts/probe.ts\n',
4106
+ },
4107
+ {
4108
+ path: 'tests/agents/skills/sample/scripts/probe.test.ts',
4109
+ content: [
4110
+ "import { read } from '../../../../../.agents/skills/sample/scripts/probe.ts'",
4111
+ "import { runSkillScript } from '../../../../setupServer.js'",
4112
+ 'void read',
4113
+ 'void runSkillScript',
4114
+ ].join('\n'),
4115
+ },
4116
+ ],
4117
+ violations: [],
4118
+ },
3632
4119
  {
3633
4120
  label: 'rejects an unexported value binding in a skill fence',
3634
4121
  membership: 'named value imports in canonical skill fences',
@@ -4508,6 +4995,379 @@ export const PORTABILITY_POLICY_CONTROLS: readonly PolicyControl[] = Object.free
4508
4995
  },
4509
4996
  ],
4510
4997
  },
4998
+ ...[
4999
+ ['a shell file outside scripts', 'src/worker/run.sh'],
5000
+ ['a bash file outside scripts', 'configs/setup.bash'],
5001
+ ['a PowerShell file outside scripts', 'tests/probe.ps1'],
5002
+ ['a Python file outside scripts', 'guides/build.py'],
5003
+ ['a batch file outside scripts', 'build.bat'],
5004
+ ['a command file outside scripts', '.claude/hook.cmd'],
5005
+ ].map(([label, path]): PolicyControl => ({
5006
+ label: `rejects ${label}`,
5007
+ membership: 'shell, PowerShell, Python, and batch extensions in the file path population',
5008
+ rule: 'portability',
5009
+ message: 'shell, PowerShell, Python, and batch files sit only in scripts',
5010
+ files: [{ path: path ?? '', content: '' }],
5011
+ })),
5012
+ {
5013
+ label: 'rejects a non-shell file inside scripts',
5014
+ membership: 'every file beneath the scripts directory',
5015
+ rule: 'portability',
5016
+ message: 'scripts holds .sh files and nothing else',
5017
+ files: [{ path: 'scripts/probe.ts', content: '' }],
5018
+ },
5019
+ ])
5020
+
5021
+ /**
5022
+ * Holds the message prefix a text violation carries, written out so the controls read the
5023
+ * production message from a second copy.
5024
+ */
5025
+ export const TEXT_POLICY_MESSAGE = 'text carries no replacement, byte-order, or control character'
5026
+
5027
+ /**
5028
+ * Lists the physical controls for every text-integrity refusal and admission the workspace route
5029
+ * reaches.
5030
+ *
5031
+ * @remarks
5032
+ * Every character a control plants is written as a `\u` escape here, so this file carries none of
5033
+ * the characters it tests for. Each excluded-directory control also writes the arrival file, whose
5034
+ * refusal proves the sweep ran over the control workspace rather than reporting nothing because it
5035
+ * found nothing. A file whose bytes are not UTF-8 cannot be written through a text fixture, so the
5036
+ * proofs of that refusal write the bytes themselves.
5037
+ */
5038
+ export const TEXT_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
5039
+ {
5040
+ label: 'rejects a bell control character in a source file',
5041
+ membership: 'source files in the suppression population, C0 controls beyond the three allowed',
5042
+ rule: 'text',
5043
+ files: [{ path: 'src/control.ts', content: "const bell = 'a\u0007b'\n" }],
5044
+ violations: [
5045
+ {
5046
+ rule: 'text',
5047
+ path: 'src/control.ts',
5048
+ line: 1,
5049
+ message: `${TEXT_POLICY_MESSAGE}: \\u0007`,
5050
+ },
5051
+ ],
5052
+ },
5053
+ {
5054
+ label: 'rejects an escape control character in authored Markdown',
5055
+ membership: 'authored Markdown outside the excluded directories, C0 controls',
5056
+ rule: 'text',
5057
+ files: [{ path: 'README.md', content: '# Front page\n\ncolor \u001B[31m\n' }],
5058
+ violations: [
5059
+ {
5060
+ rule: 'text',
5061
+ path: 'README.md',
5062
+ line: 3,
5063
+ message: `${TEXT_POLICY_MESSAGE}: \\u001B`,
5064
+ },
5065
+ ],
5066
+ },
5067
+ {
5068
+ label: 'rejects a C1 control character',
5069
+ membership: 'source files in the suppression population, C1 controls',
5070
+ rule: 'text',
5071
+ files: [{ path: 'src/control.ts', content: "const a = 1\nconst next = 'a\u0085b'\n" }],
5072
+ violations: [
5073
+ {
5074
+ rule: 'text',
5075
+ path: 'src/control.ts',
5076
+ line: 2,
5077
+ message: `${TEXT_POLICY_MESSAGE}: \\u0085`,
5078
+ },
5079
+ ],
5080
+ },
5081
+ {
5082
+ label: 'rejects the delete control character',
5083
+ membership: 'source files in the suppression population, the control above the ASCII range',
5084
+ rule: 'text',
5085
+ files: [{ path: 'src/control.ts', content: "const del = 'a\u007Fb'\n" }],
5086
+ violations: [
5087
+ {
5088
+ rule: 'text',
5089
+ path: 'src/control.ts',
5090
+ line: 1,
5091
+ message: `${TEXT_POLICY_MESSAGE}: \\u007F`,
5092
+ },
5093
+ ],
5094
+ },
5095
+ {
5096
+ label: 'rejects a replacement character',
5097
+ membership: 'authored Markdown outside the excluded directories, decoded text',
5098
+ rule: 'text',
5099
+ files: [{ path: 'README.md', content: '# Front page\n\nbroken \uFFFD text\n' }],
5100
+ violations: [
5101
+ {
5102
+ rule: 'text',
5103
+ path: 'README.md',
5104
+ line: 3,
5105
+ message: `${TEXT_POLICY_MESSAGE}: \\uFFFD`,
5106
+ },
5107
+ ],
5108
+ },
5109
+ {
5110
+ label: 'rejects a byte-order mark that opens a file',
5111
+ membership: 'source files in the suppression population, the first character of a file',
5112
+ rule: 'text',
5113
+ files: [{ path: 'src/control.ts', content: '\uFEFFexport {}\n' }],
5114
+ violations: [
5115
+ {
5116
+ rule: 'text',
5117
+ path: 'src/control.ts',
5118
+ line: 1,
5119
+ message: `${TEXT_POLICY_MESSAGE}: \\uFEFF`,
5120
+ },
5121
+ ],
5122
+ },
5123
+ {
5124
+ label: 'rejects a byte-order mark inside a literal',
5125
+ membership: 'source files in the suppression population, a character within a line',
5126
+ rule: 'text',
5127
+ files: [{ path: 'src/control.ts', content: "const path = '\uFEFFleading.txt'\n" }],
5128
+ violations: [
5129
+ {
5130
+ rule: 'text',
5131
+ path: 'src/control.ts',
5132
+ line: 1,
5133
+ message: `${TEXT_POLICY_MESSAGE}: \\uFEFF`,
5134
+ },
5135
+ ],
5136
+ },
5137
+ {
5138
+ label: 'names each refused character once on a line that repeats it',
5139
+ membership: 'refused characters grouped by line, in order of first appearance',
5140
+ rule: 'text',
5141
+ files: [{ path: 'src/control.ts', content: "const mixed = 'a\u0007b\u0007c\u001B'\n" }],
5142
+ violations: [
5143
+ {
5144
+ rule: 'text',
5145
+ path: 'src/control.ts',
5146
+ line: 1,
5147
+ message: `${TEXT_POLICY_MESSAGE}: \\u0007`,
5148
+ },
5149
+ {
5150
+ rule: 'text',
5151
+ path: 'src/control.ts',
5152
+ line: 1,
5153
+ message: `${TEXT_POLICY_MESSAGE}: \\u001B`,
5154
+ },
5155
+ ],
5156
+ },
5157
+ {
5158
+ label: 'accepts tab, line feed, carriage return, and intentional Unicode',
5159
+ membership: 'the characters a text file may carry, composed and decomposed forms included',
5160
+ rule: 'text',
5161
+ files: [
5162
+ {
5163
+ path: 'src/control.ts',
5164
+ content: "const note = 'café\tcafé — \u{1F600}'\r\n\t\n",
5165
+ },
5166
+ { path: 'README.md', content: '# Front page\r\n\r\nplain é — text\n' },
5167
+ ],
5168
+ violations: [],
5169
+ },
5170
+ {
5171
+ label: 'accepts a control character below an excluded directory',
5172
+ membership: 'Markdown below a directory name the sweep never descends into',
5173
+ rule: 'text',
5174
+ files: [
5175
+ { path: 'README.md', content: '# Front page\n\ncolor \u001B[31m\n' },
5176
+ { path: 'node_modules/sample/README.md', content: '# Installed\n\nbell \u0007\n' },
5177
+ ],
5178
+ violations: [
5179
+ {
5180
+ rule: 'text',
5181
+ path: 'README.md',
5182
+ line: 3,
5183
+ message: `${TEXT_POLICY_MESSAGE}: \\u001B`,
5184
+ },
5185
+ ],
5186
+ },
5187
+ ...[
5188
+ {
5189
+ label: 'rejects a bell control character in a skill script',
5190
+ membership: 'skill scripts in the suppression population, C0 controls',
5191
+ path: '.agents/skills/sample/scripts/control.ts',
5192
+ content: "const bell = 'a\u0007b'\n",
5193
+ },
5194
+ {
5195
+ label: 'rejects a bell control character in a root JSON file',
5196
+ membership: 'root JSON files in the text population, C0 controls',
5197
+ path: 'tsconfig.json',
5198
+ content: '{\n\t"note": "a\u0007b"\n}\n',
5199
+ },
5200
+ {
5201
+ label: 'rejects a bell control character in a root dot JSON file',
5202
+ membership: 'root dot-prefixed JSON files in the text population, C0 controls',
5203
+ path: '.oxlintrc.json',
5204
+ content: '{\n\t"note": "a\u0007b"\n}\n',
5205
+ },
5206
+ {
5207
+ label: 'rejects a bell control character in a JSON file below an instruction folder',
5208
+ membership: 'JSON files below .agents, .claude, .codex, and .cursor, C0 controls',
5209
+ path: '.claude/settings.json',
5210
+ content: '{\n\t"note": "a\u0007b"\n}\n',
5211
+ },
5212
+ {
5213
+ label: 'rejects a bell control character in a TOML role file',
5214
+ membership: 'TOML files below .agents, .claude, .codex, and .cursor, C0 controls',
5215
+ path: '.codex/agents/sample.toml',
5216
+ content: 'name = "sample"\nnote = "a\u0007b"\n',
5217
+ },
5218
+ {
5219
+ label: 'rejects a bell control character in a skill metadata file',
5220
+ membership: 'YAML files below .agents, .claude, .codex, and .cursor, C0 controls',
5221
+ path: '.agents/skills/sample/agents/openai.yaml',
5222
+ content: "interface:\n display_name: 'a\u0007b'\n",
5223
+ },
5224
+ {
5225
+ label: 'rejects a bell control character in a Cursor rule file',
5226
+ membership: 'MDC files below .agents, .claude, .codex, and .cursor, C0 controls',
5227
+ path: '.cursor/rules/sample.mdc',
5228
+ content: '---\nalwaysApply: true\n---\na\u0007b\n',
5229
+ },
5230
+ {
5231
+ label: 'rejects a bell control character in the attributes file',
5232
+ membership: 'the root .gitattributes file, C0 controls',
5233
+ path: '.gitattributes',
5234
+ content: '* text=auto eol=lf\n# a\u0007b\n',
5235
+ },
5236
+ {
5237
+ label: 'rejects a bell control character in the formatter ignore file',
5238
+ membership: 'the root .prettierignore file, C0 controls',
5239
+ path: '.prettierignore',
5240
+ content: 'dist/\n# a\u0007b\n',
5241
+ },
5242
+ ].map(({ label, membership, path, content }): PolicyControl => ({
5243
+ label,
5244
+ membership,
5245
+ rule: 'text',
5246
+ files: [{ path, content }],
5247
+ violations: [
5248
+ {
5249
+ rule: 'text',
5250
+ path,
5251
+ line: content.split('\n').findIndex((line) => line.includes('\u0007')) + 1,
5252
+ message: `${TEXT_POLICY_MESSAGE}: \\u0007`,
5253
+ },
5254
+ ],
5255
+ })),
5256
+ {
5257
+ label: 'accepts a control character in JSON outside the instruction folders and the root',
5258
+ membership: 'JSON files below src and tmp, which the text population does not reach',
5259
+ rule: 'text',
5260
+ files: [
5261
+ { path: 'README.md', content: '# Front page\n\ncolor \u001B[31m\n' },
5262
+ { path: 'src/data.json', content: '{\n\t"note": "a\u0007b"\n}\n' },
5263
+ { path: 'tmp/probe.json', content: '{\n\t"note": "a\u0007b"\n}\n' },
5264
+ ],
5265
+ violations: [
5266
+ {
5267
+ rule: 'text',
5268
+ path: 'README.md',
5269
+ line: 3,
5270
+ message: `${TEXT_POLICY_MESSAGE}: \\u001B`,
5271
+ },
5272
+ ],
5273
+ },
5274
+ ])
5275
+
5276
+ /** Pairs a setup-module import fence with the specifiers it refuses and the ones it admits. */
5277
+ export interface PolicyImportCase {
5278
+ /** Names the text of the fence's message the lint configuration carries. */
5279
+ readonly marker: string
5280
+ /** Lists each module specifier with whether the fence refuses it. */
5281
+ readonly cases: ReadonlyArray<readonly [string, boolean]>
5282
+ /** States whether a relative specifier outside the fence is also a case that must be admitted. */
5283
+ readonly controlled: boolean
5284
+ }
5285
+
5286
+ /** Pairs a setup-module named-import fence with the import clauses it refuses and the ones it admits. */
5287
+ export interface PolicyNameCase {
5288
+ /** Names the text of the fence's message the lint configuration carries. */
5289
+ readonly marker: string
5290
+ /** Lists the import clauses the fence refuses, each written as the text between the braces. */
5291
+ readonly refused: readonly string[]
5292
+ /** Lists the import clauses the fence admits, each written as the text between the braces. */
5293
+ readonly admitted: readonly string[]
5294
+ }
5295
+
5296
+ /**
5297
+ * Lists the refused and admitted specifiers of every setup-module `no-restricted-imports` pattern.
5298
+ *
5299
+ * @remarks
5300
+ * The isolated-pattern proof reads a case from here when a pattern's message carries the marker, and
5301
+ * refuses a configuration that carries no pattern for a marker, so a deleted fence cannot pass by
5302
+ * leaving nothing to test.
5303
+ */
5304
+ export const POLICY_IMPORT_CASES: readonly PolicyImportCase[] = Object.freeze([
5305
+ {
5306
+ marker: 'tests/setup.ts must stay host-independent: import no node:',
5307
+ controlled: true,
5308
+ cases: [
5309
+ ['node:fs', true],
5310
+ ['node:fs/promises', true],
5311
+ ['node:path', true],
5312
+ ['nodes:fs', false],
5313
+ ['fs', false],
5314
+ ['vitest', false],
5315
+ ['./setupServer.js', false],
5316
+ ],
5317
+ },
5318
+ {
5319
+ marker: 'tests/setup.ts must stay host-independent: import no Vue',
5320
+ controlled: true,
5321
+ cases: [
5322
+ ['vue', true],
5323
+ ['vue/server-renderer', true],
5324
+ ['vue?raw', true],
5325
+ ['@vue/test-utils', true],
5326
+ ['vue-router', false],
5327
+ ['vuex', false],
5328
+ ['node:fs', false],
5329
+ ['./vue.js', false],
5330
+ ],
5331
+ },
5332
+ {
5333
+ marker: 'the browser and styles setup modules hold no Node',
5334
+ controlled: true,
5335
+ cases: [
5336
+ ['node:fs', true],
5337
+ ['node:child_process', true],
5338
+ ['nodes:fs', false],
5339
+ ['vue', false],
5340
+ ['playwright', false],
5341
+ ['./setup.js', false],
5342
+ ],
5343
+ },
5344
+ {
5345
+ marker: 'configs/policy.ts imports nothing',
5346
+ controlled: false,
5347
+ cases: [
5348
+ ['node:fs', true],
5349
+ ['./helpers.js', true],
5350
+ ['vitest', true],
5351
+ ['@orkestrel/test', true],
5352
+ ],
5353
+ },
5354
+ ])
5355
+
5356
+ /** Lists the refused and admitted named imports of every setup-module `no-restricted-imports` path. */
5357
+ export const POLICY_NAME_CASES: readonly PolicyNameCase[] = Object.freeze([
5358
+ {
5359
+ marker: 'nothing an assertion is',
5360
+ refused: [
5361
+ 'describe',
5362
+ 'it',
5363
+ 'test',
5364
+ 'expect',
5365
+ 'describe as suite',
5366
+ 'expect, vi',
5367
+ 'type TestContext, it',
5368
+ ],
5369
+ admitted: ['vi', 'TestRunner', 'afterEach', 'beforeAll', 'bench'],
5370
+ },
4511
5371
  ])
4512
5372
 
4513
5373
  /**