@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
@@ -756,6 +756,26 @@ export function readCompilerOutput(compiler: string, args: readonly string[]): s
756
756
  return result.stdout ?? ''
757
757
  }
758
758
 
759
+ /**
760
+ * Resolves the TypeScript command from its exported package manifest.
761
+ * @param path - The module or manifest path whose installation supplies TypeScript.
762
+ * @returns The absolute compiler command path on TypeScript 6 and 7.
763
+ * @throws An error when the package declares no `bin.tsc` command.
764
+ */
765
+ export function resolveCompiler(path: string): string {
766
+ const load = createRequire(path)
767
+ const manifest = load.resolve('typescript/package.json')
768
+ const declared: unknown = load(manifest)
769
+ if (typeof declared !== 'object' || declared === null || !('bin' in declared)) {
770
+ throw new Error('The TypeScript package declares no bin.tsc command')
771
+ }
772
+ const bin = declared.bin
773
+ if (typeof bin !== 'object' || bin === null || !('tsc' in bin) || typeof bin.tsc !== 'string') {
774
+ throw new Error('The TypeScript package declares no bin.tsc command')
775
+ }
776
+ return resolvePath(dirname(manifest), bin.tsc)
777
+ }
778
+
759
779
  /**
760
780
  * Parses a `tsc --showConfig` reading into the compiler scope a face's roll-up requires.
761
781
  *
@@ -886,7 +906,7 @@ export function declarationRollup(options: DeclarationRollupOptions): Plugin {
886
906
  )
887
907
  }
888
908
  const load = createRequire(import.meta.url)
889
- const compiler = load.resolve('typescript/bin/tsc')
909
+ const compiler = resolveCompiler(import.meta.url)
890
910
  const scope = parseProjectScope(
891
911
  readCompilerOutput(compiler, ['--showConfig', '-p', options.project]),
892
912
  options.project,
@@ -12,8 +12,20 @@ export interface PolicyExpression extends PolicyNode {
12
12
  readonly key?: PolicyExpression
13
13
  readonly id?: PolicyExpression | null
14
14
  readonly method?: boolean
15
- readonly kind?: 'const' | 'get' | 'init' | 'let' | 'set' | 'var'
15
+ readonly kind?:
16
+ | 'const'
17
+ | 'get'
18
+ | 'global'
19
+ | 'init'
20
+ | 'let'
21
+ | 'module'
22
+ | 'namespace'
23
+ | 'set'
24
+ | 'var'
16
25
  readonly expression?: boolean
26
+ readonly readonly?: boolean
27
+ readonly test?: PolicyExpression
28
+ readonly alternate?: PolicyExpression | null
17
29
  readonly object?: PolicyExpression
18
30
  readonly property?: PolicyExpression
19
31
  readonly computed?: boolean
@@ -25,6 +37,7 @@ export interface PolicyExpression extends PolicyNode {
25
37
  readonly quasis?: readonly PolicyExpression[]
26
38
  readonly expressions?: readonly PolicyExpression[]
27
39
  readonly accessibility?: 'private' | 'protected' | 'public' | null
40
+ readonly declare?: boolean
28
41
  readonly declaration?: PolicyExpression
29
42
  readonly declarations?: readonly PolicyExpression[]
30
43
  readonly init?: PolicyExpression | null
@@ -52,12 +65,30 @@ export interface PolicyComment extends PolicyNode {
52
65
  readonly value: string
53
66
  }
54
67
 
68
+ /** Describes one reference the scope manager records to a declared variable. */
69
+ export interface PolicyReference {
70
+ readonly identifier: PolicyNode
71
+ readonly init?: boolean
72
+ isWrite(): boolean
73
+ }
74
+
75
+ /** Describes one variable the scope manager records for a declaration. */
76
+ export interface PolicyVariable {
77
+ readonly identifiers: readonly PolicyNode[]
78
+ readonly references: readonly PolicyReference[]
79
+ }
80
+
55
81
  /** Lists the Oxlint source-text operations the comment rules read. */
56
82
  export interface PolicySourceCode {
57
83
  readonly text: string
58
84
  getAllComments(): readonly PolicyComment[]
59
85
  }
60
86
 
87
+ /** Adds the Oxlint scope operation the binding rule reads. */
88
+ export interface PolicyScopeCode extends PolicySourceCode {
89
+ getDeclaredVariables(node: PolicyNode): readonly PolicyVariable[]
90
+ }
91
+
61
92
  /** Pairs one doc block with the declared name its first sentence must not repeat. */
62
93
  export interface PolicyDoc {
63
94
  readonly comment: PolicyComment
@@ -89,7 +120,7 @@ export interface PolicyContext {
89
120
  readonly filename: string
90
121
  /** Names the directory Oxlint resolves `filename` against. */
91
122
  readonly cwd: string
92
- readonly sourceCode: PolicySourceCode
123
+ readonly sourceCode: PolicyScopeCode
93
124
  report(diagnostic: PolicyDiagnostic): void
94
125
  }
95
126
 
@@ -130,6 +161,7 @@ export interface PolicyVisitor {
130
161
  readonly TSEnumDeclaration?: (node: PolicyNode) => void
131
162
  readonly TSInterfaceDeclaration?: (node: PolicyNode) => void
132
163
  readonly TSModuleDeclaration?: (node: PolicyNode) => void
164
+ readonly TSPropertySignature?: (node: PolicyNode) => void
133
165
  readonly TSTypeAliasDeclaration?: (node: PolicyNode) => void
134
166
  readonly VariableDeclaration?: (node: PolicyNode) => void
135
167
  }
@@ -266,6 +298,68 @@ export const POLICY_ENDING_GLOBS: readonly string[] = Object.freeze([
266
298
  'src/**/*.ts',
267
299
  ])
268
300
 
301
+ /** Lists every source extension the nested-function rule reads. */
302
+ export const POLICY_CODE_EXTENSIONS: readonly string[] = Object.freeze([
303
+ 'cjs',
304
+ 'cts',
305
+ 'js',
306
+ 'jsx',
307
+ 'mjs',
308
+ 'mts',
309
+ 'ts',
310
+ 'tsx',
311
+ 'vue',
312
+ ])
313
+
314
+ /**
315
+ * Matches the lint population the nested-function rule runs over, as the Oxlint configuration
316
+ * declares it.
317
+ */
318
+ export const POLICY_NESTED_GLOBS: readonly string[] = Object.freeze(
319
+ ['src', 'app', 'tests', 'configs'].map(
320
+ (root) => `${root}/**/*.{${POLICY_CODE_EXTENSIONS.join(',')}}`,
321
+ ),
322
+ )
323
+
324
+ /**
325
+ * Matches the lint population the skill-script rules run over, as the Oxlint configuration declares
326
+ * it.
327
+ */
328
+ export const POLICY_SCRIPT_GLOBS: readonly string[] = Object.freeze([
329
+ '.agents/skills/*/scripts/*.ts',
330
+ ])
331
+
332
+ /** Matches the workspace-relative path of a skill script, the file shape the script rule reads. */
333
+ export const POLICY_SCRIPT_PATTERN = /^\.agents\/skills\/[^/]+\/scripts\/[^/]+\.ts$/u
334
+
335
+ /** Holds the one entry guard a skill script runs `main` under, as its source text reads. */
336
+ export const POLICY_ENTRY_GUARD =
337
+ 'process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename'
338
+
339
+ /** Lists every statement type that declares a name at module scope. */
340
+ export const POLICY_DECLARATION_TYPES: readonly string[] = Object.freeze([
341
+ 'ClassDeclaration',
342
+ 'FunctionDeclaration',
343
+ 'TSDeclareFunction',
344
+ 'TSEnumDeclaration',
345
+ 'TSInterfaceDeclaration',
346
+ 'TSModuleDeclaration',
347
+ 'TSTypeAliasDeclaration',
348
+ 'VariableDeclaration',
349
+ ])
350
+
351
+ /**
352
+ * Lists the type nodes a property signature's search for its owning declaration never crosses,
353
+ * because a parameter, call, or construct signature beyond one is an inline type.
354
+ */
355
+ export const POLICY_INLINE_TYPES: readonly string[] = Object.freeze([
356
+ 'TSCallSignatureDeclaration',
357
+ 'TSConstructSignatureDeclaration',
358
+ 'TSConstructorType',
359
+ 'TSFunctionType',
360
+ 'TSMethodSignature',
361
+ ])
362
+
269
363
  /**
270
364
  * Matches the file name shape an implementation file takes, holding the class that matches its
271
365
  * stem.
@@ -870,12 +964,37 @@ export function isPolicyVoiced(word: string): boolean {
870
964
  }
871
965
 
872
966
  /**
873
- * Pairs every exported top-level statement with the doc block written directly above it.
967
+ * Reads the doc block written directly above one top-level statement.
874
968
  *
875
969
  * @remarks
876
970
  * A statement takes the last comment that closes before it, and takes it only where that comment is
877
971
  * a doc block and nothing but whitespace separates the two. A blank line between them is still
878
- * whitespace, so the pairing survives one.
972
+ * whitespace, so the pairing survives one. Every comment is read where a `findLast` would stop at
973
+ * the nearest one.
974
+ *
975
+ * @param statement - The top-level statement to pair.
976
+ * @param comments - Every comment of the linted file, in source order.
977
+ * @param sourceCode - The source-text reader supplying the text between a comment and the statement.
978
+ * @returns The paired doc block, or `undefined` where the statement has none.
979
+ */
980
+ export function statementToPolicyDoc(
981
+ statement: PolicyExpression,
982
+ comments: readonly PolicyComment[],
983
+ sourceCode: PolicySourceCode,
984
+ ): PolicyComment | undefined {
985
+ const start = statement.range[0]
986
+ const previous = Iterator.from(comments)
987
+ .flatMap((comment) => (comment.range[1] <= start ? [comment] : []))
988
+ .toArray()
989
+ .at(-1)
990
+ if (previous === undefined || previous.type !== 'Block') return undefined
991
+ if (!previous.value.startsWith('*')) return undefined
992
+ if (sourceCode.text.slice(previous.range[1], start).trim() !== '') return undefined
993
+ return previous
994
+ }
995
+
996
+ /**
997
+ * Pairs every exported top-level statement with the doc block written directly above it.
879
998
  *
880
999
  * @param node - The program node whose top-level statements are read.
881
1000
  * @param sourceCode - The source-text reader supplying the comments and the text between them.
@@ -886,24 +1005,18 @@ export function programToPolicyDocs(
886
1005
  sourceCode: PolicySourceCode,
887
1006
  ): readonly PolicyDoc[] {
888
1007
  // The iterator consumes one entry at a time and reads a hole as an `undefined` entry, as a
889
- // `for…of` over the list would. The comments are read only for an export statement, once per
890
- // statement, and every comment is read where a `findLast` would stop at the nearest one.
1008
+ // `for…of` over the list would. The comments are read once, and a statement pairs with one only
1009
+ // where it is an export.
891
1010
  const comments = sourceCode.getAllComments()
892
1011
  return Iterator.from(programToPolicyStatements(node))
893
1012
  .flatMap((statement) => {
894
1013
  if (!statement.type.startsWith('Export')) return []
895
- const start = statement.range[0]
896
- const previous = Iterator.from(comments)
897
- .flatMap((comment) => (comment.range[1] <= start ? [comment] : []))
898
- .toArray()
899
- .at(-1)
900
- if (previous === undefined || previous.type !== 'Block') return []
901
- if (!previous.value.startsWith('*')) return []
902
- if (sourceCode.text.slice(previous.range[1], start).trim() !== '') return []
1014
+ const comment = statementToPolicyDoc(statement, comments, sourceCode)
1015
+ if (comment === undefined) return []
903
1016
  const declaration = statement.declaration
904
1017
  return [
905
1018
  {
906
- comment: previous,
1019
+ comment,
907
1020
  name:
908
1021
  identifierToPolicyName(declaration?.id) ??
909
1022
  identifierToPolicyName(declaration?.declarations?.[0]?.id),
@@ -1023,6 +1136,54 @@ export function reportPrivacy(context: PolicyContext, node: PolicyExpression): v
1023
1136
  }
1024
1137
  }
1025
1138
 
1139
+ /** Checks whether code writes a declared variable anywhere but in its own declaration. */
1140
+ export function isPolicyReassigned(variable: PolicyVariable): boolean {
1141
+ return variable.references.some(
1142
+ (reference) =>
1143
+ reference.isWrite() &&
1144
+ reference.init !== true &&
1145
+ !variable.identifiers.includes(reference.identifier),
1146
+ )
1147
+ }
1148
+
1149
+ /**
1150
+ * Checks whether a node sits inside a `declare` module, namespace, or global augmentation.
1151
+ *
1152
+ * @remarks
1153
+ * A declaration inside such a block carries no `declare` flag of its own and no value, because the
1154
+ * block is the ambient context. A `global` block is ambient wherever it stands.
1155
+ */
1156
+ export function isPolicyInDeclaredModule(node: PolicyExpression): boolean {
1157
+ const parent = node.parent
1158
+ if (parent === undefined || parent === null) return false
1159
+ return (
1160
+ (parent.type === 'TSModuleDeclaration' &&
1161
+ (parent.declare === true || parent.kind === 'global')) ||
1162
+ isPolicyInDeclaredModule(parent)
1163
+ )
1164
+ }
1165
+
1166
+ /** Reports a let declaration that prefer-const leaves: one reassigned or declared without a value. */
1167
+ export function reportLet(context: PolicyContext, node: PolicyExpression): void {
1168
+ if (
1169
+ node.kind !== 'let' ||
1170
+ node.declare === true ||
1171
+ isPolicyAmbient(context.filename) ||
1172
+ isPolicyInDeclaredModule(node)
1173
+ ) {
1174
+ return
1175
+ }
1176
+ const head = node.parent?.type === 'ForInStatement' || node.parent?.type === 'ForOfStatement'
1177
+ const bare =
1178
+ !head &&
1179
+ (node.declarations ?? []).some(
1180
+ (declarator) => declarator.init === undefined || declarator.init === null,
1181
+ )
1182
+ if (bare || context.sourceCode.getDeclaredVariables(node).some(isPolicyReassigned)) {
1183
+ context.report({ node, messageId: 'let' })
1184
+ }
1185
+ }
1186
+
1026
1187
  /** Reports a centralized declaration that carries no export. */
1027
1188
  export function reportHidden(context: PolicyContext, node: PolicyExpression): void {
1028
1189
  if (isPolicyAmbient(context.filename)) return
@@ -1193,6 +1354,64 @@ export function reportEndingImport(context: PolicyContext, node: PolicyExpressio
1193
1354
  if (importsPolicyTerminator(node)) context.report({ node, messageId: 'terminator' })
1194
1355
  }
1195
1356
 
1357
+ /** Reports whether a path is a skill script, the one file shape the script rule reads. */
1358
+ export function isPolicyScript(filename: string, cwd: string): boolean {
1359
+ return POLICY_SCRIPT_PATTERN.test(pathToPolicyRelative(filename, cwd))
1360
+ }
1361
+
1362
+ /** Reports whether a statement is the one entry guard a skill script runs its main under. */
1363
+ export function isPolicyGuard(statement: PolicyExpression, sourceCode: PolicySourceCode): boolean {
1364
+ const test = statement.test
1365
+ if (statement.type !== 'IfStatement' || test === undefined) return false
1366
+ if (statement.alternate !== undefined && statement.alternate !== null) return false
1367
+ const text = sourceCode.text.slice(test.range[0], test.range[1]).replace(/\s+/gu, ' ')
1368
+ return text === POLICY_ENTRY_GUARD
1369
+ }
1370
+
1371
+ /** Reports a skill script's top-level statement that is hidden, undocumented, or runs at import. */
1372
+ export function reportScript(context: PolicyContext, node: PolicyExpression): void {
1373
+ if (isPolicyAmbient(context.filename) || !isPolicyScript(context.filename, context.cwd)) return
1374
+ const sourceCode = context.sourceCode
1375
+ const comments = sourceCode.getAllComments()
1376
+ const statements = programToPolicyStatements(node)
1377
+ const guard = statements.findIndex((statement) => isPolicyGuard(statement, sourceCode))
1378
+ for (const [index, statement] of statements.entries()) {
1379
+ if (statement.type === 'ImportDeclaration' || index === guard) continue
1380
+ const declaration = statement.declaration
1381
+ if (
1382
+ statement.type === 'ExportNamedDeclaration' &&
1383
+ declaration !== undefined &&
1384
+ declaration !== null
1385
+ ) {
1386
+ if (statementToPolicyDoc(statement, comments, sourceCode) === undefined) {
1387
+ context.report({ node: statement, messageId: 'doc' })
1388
+ }
1389
+ continue
1390
+ }
1391
+ context.report({
1392
+ node: statement,
1393
+ messageId: POLICY_DECLARATION_TYPES.includes(statement.type) ? 'hidden' : 'entry',
1394
+ })
1395
+ }
1396
+ }
1397
+
1398
+ /** Reports whether a property signature sits in an interface or in a type alias's object type. */
1399
+ export function isPolicyOwned(node: PolicyExpression): boolean {
1400
+ const parent = node.parent
1401
+ if (parent === undefined || parent === null) return false
1402
+ if (parent.type === 'TSInterfaceDeclaration' || parent.type === 'TSTypeAliasDeclaration') {
1403
+ return true
1404
+ }
1405
+ if (POLICY_INLINE_TYPES.includes(parent.type)) return false
1406
+ return isPolicyOwned(parent)
1407
+ }
1408
+
1409
+ /** Reports a property signature of an interface or type alias that is not readonly. */
1410
+ export function reportMutable(context: PolicyContext, node: PolicyExpression): void {
1411
+ if (isPolicyAmbient(context.filename) || node.readonly === true) return
1412
+ if (isPolicyOwned(node)) context.report({ node, messageId: 'mutable' })
1413
+ }
1414
+
1196
1415
  /** Bans function declarations and assignments inside another function body. */
1197
1416
  export const NESTED_RULE: PolicyRuleInterface = {
1198
1417
  meta: {
@@ -1262,6 +1481,25 @@ export const PRIVACY_RULE: PolicyRuleInterface = {
1262
1481
  },
1263
1482
  }
1264
1483
 
1484
+ /** Bans a let declaration that prefer-const does not already report. */
1485
+ export const LET_RULE: PolicyRuleInterface = {
1486
+ meta: {
1487
+ type: 'problem',
1488
+ docs: {
1489
+ description:
1490
+ 'Disallow a let declaration that is reassigned or declared without a value; prefer-const reports the rest.',
1491
+ },
1492
+ messages: {
1493
+ let: 'Declare this binding with const and derive its value: an expression, a helper that returns it, or a for…of over entries() in place of a counter.',
1494
+ },
1495
+ },
1496
+ create(context) {
1497
+ return {
1498
+ VariableDeclaration: (node) => reportLet(context, node),
1499
+ }
1500
+ },
1501
+ }
1502
+
1265
1503
  /** Bans a centralized declaration that no export reaches. */
1266
1504
  export const HIDDEN_RULE: PolicyRuleInterface = {
1267
1505
  meta: {
@@ -1546,6 +1784,49 @@ export const VOICE_RULE: PolicyRuleInterface = {
1546
1784
  },
1547
1785
  }
1548
1786
 
1787
+ /** Bans a skill script top-level statement that is hidden, undocumented, or runs at import. */
1788
+ export const SCRIPT_RULE: PolicyRuleInterface = {
1789
+ meta: {
1790
+ type: 'problem',
1791
+ docs: {
1792
+ description:
1793
+ 'Disallow an unexported declaration, an undocumented export, and a statement beyond the one guarded entry in a skill script.',
1794
+ },
1795
+ messages: {
1796
+ hidden:
1797
+ 'Export this declaration; a skill script exports every declaration so its proof can test each one.',
1798
+ doc: 'Write a one-sentence TSDoc summary directly above this export.',
1799
+ entry:
1800
+ 'Move this statement into a function; a skill script holds imports, exported declarations, and the one process.argv[1] guard that runs main, and an import runs nothing.',
1801
+ },
1802
+ },
1803
+ create(context) {
1804
+ return {
1805
+ Program: (node) => reportScript(context, node),
1806
+ }
1807
+ },
1808
+ }
1809
+
1810
+ /** Bans a property signature in an interface or type alias that is not readonly. */
1811
+ export const MUTABLE_RULE: PolicyRuleInterface = {
1812
+ meta: {
1813
+ type: 'problem',
1814
+ docs: {
1815
+ description:
1816
+ 'Disallow a property signature without readonly in an interface or in the object type of a type alias.',
1817
+ },
1818
+ messages: {
1819
+ mutable:
1820
+ 'Declare this property readonly; a contract exposes its members and no holder rewrites them.',
1821
+ },
1822
+ },
1823
+ create(context) {
1824
+ return {
1825
+ TSPropertySignature: (node) => reportMutable(context, node),
1826
+ }
1827
+ },
1828
+ }
1829
+
1549
1830
  /** Bans a comment carrying a term the substitution table bans unconditionally. */
1550
1831
  export const TERM_RULE: PolicyRuleInterface = {
1551
1832
  meta: {
@@ -1571,6 +1852,7 @@ export default {
1571
1852
  rules: {
1572
1853
  'no-mocking': MOCKING_RULE,
1573
1854
  'no-keyword-privacy': PRIVACY_RULE,
1855
+ 'no-let': LET_RULE,
1574
1856
  'no-nested-functions': NESTED_RULE,
1575
1857
  'no-hidden-declaration': HIDDEN_RULE,
1576
1858
  'no-misplaced-type': TYPE_RULE,
@@ -1586,5 +1868,7 @@ export default {
1586
1868
  'no-host-line-endings': ENDING_RULE,
1587
1869
  'no-malformed-summary': VOICE_RULE,
1588
1870
  'no-banned-term': TERM_RULE,
1871
+ 'no-malformed-script': SCRIPT_RULE,
1872
+ 'no-mutable-property': MUTABLE_RULE,
1589
1873
  },
1590
1874
  }
@@ -50,14 +50,20 @@
50
50
  "no-unused-private-class-members": "error",
51
51
  "no-var": "error",
52
52
  "prefer-const": "error",
53
+ "no-param-reassign": "error",
54
+ "no-ex-assign": "error",
53
55
  "no-debugger": "error",
54
56
  "no-underscore-dangle": "warn",
55
57
 
56
58
  "vitest/no-alias-methods": "error",
59
+ "vitest/no-identical-title": "error",
60
+ "vitest/no-focused-tests": "error",
61
+ "vitest/no-disabled-tests": "error",
57
62
  "vitest/warn-todo": "off",
58
63
 
59
64
  "policy/no-mocking": "error",
60
65
  "policy/no-keyword-privacy": "error",
66
+ "policy/no-let": "error",
61
67
  "policy/no-malformed-summary": "error",
62
68
  "policy/no-banned-term": "error"
63
69
  },
@@ -86,13 +92,27 @@
86
92
  "files": [".agents/skills/*/scripts/*.ts"],
87
93
  "rules": {
88
94
  "policy/no-nested-functions": "error",
89
- "policy/no-host-line-endings": "error"
95
+ "policy/no-host-line-endings": "error",
96
+ "policy/no-malformed-script": "error",
97
+ "no-restricted-imports": [
98
+ "error",
99
+ {
100
+ "patterns": [
101
+ {
102
+ "regex": "^(?:[^n]|n[^o]|no[^d]|nod[^e]|node[^:]|n$|no$|nod$|node$|$)",
103
+ "message": "a skill script is self-contained: import node: modules only and declare every helper in the script"
104
+ }
105
+ ]
106
+ }
107
+ ]
90
108
  }
91
109
  },
92
110
  {
93
111
  "files": [
94
112
  "src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}",
95
- "app/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}"
113
+ "app/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}",
114
+ "tests/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}",
115
+ "configs/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}"
96
116
  ],
97
117
  "rules": {
98
118
  "policy/no-nested-functions": "error"
@@ -110,7 +130,8 @@
110
130
  "policy/no-misnamed-parser": "error",
111
131
  "policy/no-misnamed-factory": "error",
112
132
  "policy/no-misnamed-plugin": "error",
113
- "policy/no-malformed-domain": "error"
133
+ "policy/no-malformed-domain": "error",
134
+ "policy/no-mutable-property": "error"
114
135
  }
115
136
  },
116
137
  {
@@ -772,6 +793,119 @@
772
793
  }
773
794
  ]
774
795
  }
796
+ },
797
+ {
798
+ "files": ["tests/setup*.ts"],
799
+ "rules": {
800
+ "no-restricted-imports": [
801
+ "error",
802
+ {
803
+ "paths": [
804
+ {
805
+ "name": "vitest",
806
+ "importNames": ["describe", "it", "test", "expect"],
807
+ "message": "a setup module owns everything an assertion needs and nothing an assertion is: register and assert in a test file"
808
+ }
809
+ ],
810
+ "patterns": [
811
+ {
812
+ "regex": "^typescript(?:[/?#]|$)",
813
+ "message": "the in-process compiler API is not a surface the fleet uses"
814
+ }
815
+ ]
816
+ }
817
+ ]
818
+ }
819
+ },
820
+ {
821
+ "files": ["tests/setup*.test.ts"],
822
+ "rules": {
823
+ "no-restricted-imports": [
824
+ "error",
825
+ {
826
+ "patterns": [
827
+ {
828
+ "regex": "^typescript(?:[/?#]|$)",
829
+ "message": "the in-process compiler API is not a surface the fleet uses"
830
+ }
831
+ ]
832
+ }
833
+ ]
834
+ }
835
+ },
836
+ {
837
+ "files": ["tests/setup.ts"],
838
+ "rules": {
839
+ "no-restricted-imports": [
840
+ "error",
841
+ {
842
+ "paths": [
843
+ {
844
+ "name": "vitest",
845
+ "importNames": ["describe", "it", "test", "expect"],
846
+ "message": "a setup module owns everything an assertion needs and nothing an assertion is: register and assert in a test file"
847
+ }
848
+ ],
849
+ "patterns": [
850
+ {
851
+ "regex": "^typescript(?:[/?#]|$)",
852
+ "message": "the in-process compiler API is not a surface the fleet uses"
853
+ },
854
+ {
855
+ "regex": "^node:",
856
+ "message": "tests/setup.ts must stay host-independent: import no node: module"
857
+ },
858
+ {
859
+ "regex": "^(?:vue(?:[/?#]|$)|@vue/)",
860
+ "message": "tests/setup.ts must stay host-independent: import no Vue module"
861
+ }
862
+ ]
863
+ }
864
+ ]
865
+ }
866
+ },
867
+ {
868
+ "files": ["tests/setupBrowser.ts", "tests/setupStyles.ts"],
869
+ "rules": {
870
+ "no-restricted-imports": [
871
+ "error",
872
+ {
873
+ "paths": [
874
+ {
875
+ "name": "vitest",
876
+ "importNames": ["describe", "it", "test", "expect"],
877
+ "message": "a setup module owns everything an assertion needs and nothing an assertion is: register and assert in a test file"
878
+ }
879
+ ],
880
+ "patterns": [
881
+ {
882
+ "regex": "^typescript(?:[/?#]|$)",
883
+ "message": "the in-process compiler API is not a surface the fleet uses"
884
+ },
885
+ {
886
+ "regex": "^node:",
887
+ "message": "the browser and styles setup modules hold no Node: import no node: module"
888
+ }
889
+ ]
890
+ }
891
+ ]
892
+ }
893
+ },
894
+ {
895
+ "files": ["configs/policy.ts"],
896
+ "rules": {
897
+ "no-restricted-imports": [
898
+ "error",
899
+ {
900
+ "patterns": [
901
+ {
902
+ "regex": ".",
903
+ "message": "configs/policy.ts imports nothing, so it resolves in every workspace"
904
+ }
905
+ ]
906
+ }
907
+ ]
908
+ }
775
909
  }
776
910
  ]
777
911
  }