@orkestrel/scaffold 0.0.77 → 0.0.78

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 +204 -0
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +102 -0
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +95 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +207 -0
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +108 -0
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +114 -0
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +108 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +156 -0
  9. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +196 -0
  10. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +206 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +93 -0
  12. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +458 -0
  13. package/dist/agents/skills/orkestrel-publish/scripts/window.js +188 -0
  14. package/dist/agents/skills/orkestrel-scout/scripts/map.js +300 -0
  15. package/dist/agents/templates/brief.md +55 -0
  16. package/dist/bin/main.js +4 -2
  17. package/dist/bin/main.js.map +1 -1
  18. package/dist/host/AGENTS.md +77 -135
  19. package/dist/host/agents/orchestration.md +147 -998
  20. package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +2 -2
  21. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
  22. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +6 -13
  23. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/agents/openai.yaml +1 -1
  24. package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/fleet.md +5 -7
  25. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +11 -22
  26. package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/agents/openai.yaml +1 -1
  27. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +8 -16
  28. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
  29. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +13 -13
  30. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +61 -0
  31. package/dist/host/agents/skills/orkestrel-dispatch/agents/openai.yaml +4 -0
  32. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +25 -0
  33. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +32 -0
  34. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +259 -0
  35. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +110 -0
  36. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +115 -0
  37. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +239 -0
  38. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +124 -0
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +123 -0
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +129 -0
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +157 -0
  42. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +42 -193
  43. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +38 -108
  44. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +35 -134
  45. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +10 -14
  46. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/agents/openai.yaml +1 -1
  47. package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/hardening.md +3 -4
  48. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +228 -0
  49. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +15 -23
  50. package/dist/host/agents/skills/orkestrel-journey/agents/openai.yaml +4 -0
  51. package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/captures.md +1 -1
  52. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/SKILL.md +25 -33
  53. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/agents/openai.yaml +1 -1
  54. package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/references/capture-harness.md +3 -3
  55. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +33 -20
  56. package/dist/host/agents/skills/orkestrel-publish/references/release.md +39 -0
  57. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +22 -21
  58. package/dist/host/agents/skills/orkestrel-publish/references/window.md +27 -14
  59. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +220 -0
  60. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +114 -0
  61. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +629 -0
  62. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +242 -0
  63. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +28 -0
  64. package/dist/host/agents/skills/orkestrel-scout/agents/openai.yaml +4 -0
  65. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +352 -0
  66. package/dist/host/agents/templates/brief.md +21 -142
  67. package/dist/host/agents/transports/claude-cli.md +21 -0
  68. package/dist/host/agents/transports/codex.md +38 -159
  69. package/dist/host/agents/transports/cursor.md +16 -65
  70. package/dist/host/claude/AGENTS.md +38 -0
  71. package/dist/host/claude/agents/analyst.md +14 -53
  72. package/dist/host/claude/agents/astra.md +26 -0
  73. package/dist/host/claude/agents/builder.md +14 -30
  74. package/dist/host/claude/agents/checker.md +13 -57
  75. package/dist/host/claude/agents/distiller.md +11 -26
  76. package/dist/host/claude/agents/grok.md +12 -35
  77. package/dist/host/claude/agents/opus.md +14 -30
  78. package/dist/host/claude/agents/planner.md +10 -44
  79. package/dist/host/claude/agents/researcher.md +11 -30
  80. package/dist/host/claude/agents/reviewer.md +11 -95
  81. package/dist/host/claude/agents/scout.md +9 -23
  82. package/dist/host/claude/agents/verifier.md +15 -33
  83. package/dist/host/claude/rules/documentation.md +8 -2
  84. package/dist/host/claude/rules/portability.md +7 -1
  85. package/dist/host/claude/rules/quality.md +36 -96
  86. package/dist/host/claude/rules/styles.md +3 -0
  87. package/dist/host/claude/rules/tests.md +6 -3
  88. package/dist/host/claude/rules/workspace.md +19 -15
  89. package/dist/host/claude/rules/writing.md +57 -108
  90. package/dist/host/claude/settings.json +5 -3
  91. package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
  92. package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
  93. package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
  94. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
  95. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
  96. package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
  97. package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
  98. package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
  99. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
  100. package/dist/host/codex/agents/analyst.toml +14 -31
  101. package/dist/host/codex/agents/astra.toml +25 -0
  102. package/dist/host/codex/agents/builder.toml +13 -20
  103. package/dist/host/codex/agents/checker.toml +13 -27
  104. package/dist/host/codex/agents/distiller.toml +9 -22
  105. package/dist/host/codex/agents/grok.toml +11 -30
  106. package/dist/host/codex/agents/opus.toml +14 -22
  107. package/dist/host/codex/agents/orkestrel.toml +1 -1
  108. package/dist/host/codex/agents/planner.toml +11 -28
  109. package/dist/host/codex/agents/researcher.toml +10 -22
  110. package/dist/host/codex/agents/reviewer.toml +11 -27
  111. package/dist/host/codex/agents/scout.toml +11 -17
  112. package/dist/host/codex/agents/verifier.toml +16 -12
  113. package/dist/host/codex/config.toml +18 -21
  114. package/dist/host/cursor/mcp.json +0 -4
  115. package/dist/host/cursor/rules/orchestration.mdc +12 -20
  116. package/dist/host/dotfiles/mcp.json +0 -4
  117. package/dist/host/dotfiles/oxlintrc.json +7 -0
  118. package/dist/host/guides/probe.md +9 -9
  119. package/dist/host/guides/scaffold.md +117 -71
  120. package/dist/host/guides/test.md +1 -1
  121. package/dist/host/manifest.json +321 -184
  122. package/dist/host/scripts/codex.sh +0 -0
  123. package/dist/host/scripts/cursor.sh +0 -0
  124. package/dist/host/scripts/deps.sh +0 -0
  125. package/dist/host/scripts/ollama.sh +0 -0
  126. package/dist/host/tests/config.test.ts +68 -46
  127. package/dist/host/tests/policy.test.ts +1 -5
  128. package/dist/host/tests/setupPolicy.ts +179 -4
  129. package/dist/src/core/index.cjs +255 -84
  130. package/dist/src/core/index.cjs.map +1 -1
  131. package/dist/src/core/index.d.cts +94 -29
  132. package/dist/src/core/index.d.ts +94 -29
  133. package/dist/src/core/index.js +253 -85
  134. package/dist/src/core/index.js.map +1 -1
  135. package/dist/src/server/index.cjs +55 -9
  136. package/dist/src/server/index.cjs.map +1 -1
  137. package/dist/src/server/index.d.cts +29 -4
  138. package/dist/src/server/index.d.ts +29 -4
  139. package/dist/src/server/index.js +56 -11
  140. package/dist/src/server/index.js.map +1 -1
  141. package/package.json +15 -11
  142. package/dist/host/CLAUDE.md +0 -61
  143. package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
  144. package/dist/host/agents/transports/claude.md +0 -49
  145. package/dist/host/claude/agents/application.md +0 -36
  146. package/dist/host/claude/agents/sol.md +0 -61
  147. package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
  148. package/dist/host/codex/agents/application.toml +0 -25
  149. package/dist/host/codex/agents/sol.toml +0 -19
  150. /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
  151. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
  152. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
  153. /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
  154. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
  155. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
  156. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
  157. /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
File without changes
File without changes
File without changes
File without changes
@@ -186,6 +186,14 @@ describe('root configuration', () => {
186
186
  setup: ['./tests/setup.ts', './tests/setupServer.ts'],
187
187
  })
188
188
  }
189
+ // The `configs/agents/tsconfig.skills.json` wrapper selects the `skills` blueprint fact, and the
190
+ // project it registers runs the mirrored proofs under `tests/agents/`.
191
+ if (existsSync(resolve(root, 'configs/agents/tsconfig.skills.json'))) {
192
+ expected.set('skills', {
193
+ include: 'tests/agents/**/*.test.ts',
194
+ setup: ['./tests/setup.ts'],
195
+ })
196
+ }
189
197
  for (const label of [
190
198
  'policy',
191
199
  'config',
@@ -220,8 +228,8 @@ describe('root configuration', () => {
220
228
  })
221
229
  }
222
230
  expected.set('probe', {
223
- benchmark: ['tmp/probe/**/*.test.ts', 'tests/**/*.test.ts'],
224
- include: 'tmp/probe/**/*.test.ts',
231
+ benchmark: ['tmp/probes/**/*.test.ts', 'tests/**/*.test.ts'],
232
+ include: 'tmp/probes/**/*.test.ts',
225
233
  parallel: false,
226
234
  pool: 'threads',
227
235
  setup: ['./tests/setup.ts'],
@@ -370,63 +378,77 @@ describe('root configuration', () => {
370
378
  }).toThrow(/strictly equal/u)
371
379
  })
372
380
 
373
- it('emits every project as a factory so the release mode reaches its proof', () => {
381
+ it('returns the invocation mode and no other invocation field from every registered project factory', () => {
374
382
  const projects = configuration.test?.projects
375
383
  if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
376
384
  if (projects.length === 0) throw new Error('The root configuration registers no project')
377
- // Measured: with `--mode release` on the command line, `import.meta.env.MODE` reads
378
- // `release` inside a project Vitest calls and `test` inside an inline project
379
- // configuration. `prepublishOnly` runs the distribution proof with `--mode release`, and
380
- // that proof fails rather than skips only when it reads `release`, so converting these
381
- // entries to inline configurations turns the publish gate into a skip while every suite
382
- // stays green. The control is that conversion applied to one entry.
383
- const inline = {
384
- test: {
385
- name: { label: 'inline' },
386
- include: ['tests/inline.test.ts'],
387
- setupFiles: ['./tests/setup.ts'],
388
- },
389
- }
390
- const callable = projects.concat(inline).filter((entry) => typeof entry === 'function')
391
- for (const entry of projects) expect(callable).toContain(entry)
392
- expect(callable).not.toContain(inline)
393
- })
394
-
395
- it('keeps Vitest invocation fields out of project configurations', () => {
396
- const projects = configuration.test?.projects
397
- if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
398
- const factories = projects.filter((row) => typeof row === 'function')
399
- if (factories.length === 0)
400
- throw new Error('The root configuration registers no project factory')
401
- const sentinel = {
402
- command: 'sentinel-command',
385
+ // Vitest calls each registered factory with its invocation record, whose `mode` is the
386
+ // `--mode` value, and runs a project that declares no `mode` in its own run mode, `test`.
387
+ // `prepublishOnly` runs the distribution proof with `--mode release`, and that proof fails
388
+ // rather than skips only when it reads `release`, so every project returns the record's
389
+ // `mode` and none of the record's other fields. The sentinel mode is a value no factory
390
+ // declares, so only a forwarded mode reads it. The controls are the ways a row misses
391
+ // that: a factory that ignores the record, one that spreads it whole, and an inline
392
+ // entry, which Vitest never calls.
393
+ const invocation = {
394
+ command: 'serve',
403
395
  isPreview: true,
404
396
  isSsrBuild: true,
405
397
  mode: 'sentinel-mode',
406
398
  sentinel: true,
407
399
  }
408
- for (const factory of factories) {
409
- const project: unknown = Reflect.apply(factory, undefined, [sentinel])
400
+ const test = { name: { label: 'control' }, include: ['tests/control.test.ts'] }
401
+ const ignoring = Object.defineProperty(() => ({ test }), 'name', { value: 'ignoring' })
402
+ const spreading = Object.defineProperty((record: object) => ({ ...record, test }), 'name', {
403
+ value: 'spreading',
404
+ })
405
+ const entries: readonly unknown[] = [...projects, ignoring, spreading, { test }]
406
+ const readings = entries.map((entry) => {
407
+ if (typeof entry !== 'function') {
408
+ return { name: undefined, callable: false, mode: undefined, leaked: [] }
409
+ }
410
+ const project: unknown = Reflect.apply(entry, undefined, [invocation])
410
411
  if (typeof project !== 'object' || project === null) {
411
- throw new Error('A project factory returned no configuration')
412
+ throw new Error(`The project factory ${entry.name} returned no configuration`)
412
413
  }
413
- for (const field of Object.keys(sentinel)) {
414
- expect(Object.getOwnPropertyDescriptor(project, field)?.value).toBeUndefined()
414
+ return {
415
+ name: entry.name,
416
+ callable: true,
417
+ mode: Object.getOwnPropertyDescriptor(project, 'mode')?.value,
418
+ leaked: Object.keys(invocation).filter(
419
+ (field) => field !== 'mode' && Object.hasOwn(project, field),
420
+ ),
415
421
  }
422
+ })
423
+ const forwarded = { callable: true, mode: 'sentinel-mode', leaked: [] }
424
+ expect(readings.slice(0, projects.length)).toStrictEqual(
425
+ projects.map((entry) => ({
426
+ name: typeof entry === 'function' ? entry.name : undefined,
427
+ ...forwarded,
428
+ })),
429
+ )
430
+ expect(readings.slice(projects.length)).toStrictEqual([
431
+ { name: 'ignoring', callable: true, mode: undefined, leaked: [] },
432
+ {
433
+ name: 'spreading',
434
+ callable: true,
435
+ mode: 'sentinel-mode',
436
+ leaked: ['command', 'isPreview', 'isSsrBuild', 'sentinel'],
437
+ },
438
+ { name: undefined, callable: false, mode: undefined, leaked: [] },
439
+ ])
440
+ for (const reading of readings.slice(projects.length)) {
441
+ expect({ ...reading, name: undefined }).not.toStrictEqual({ name: undefined, ...forwarded })
416
442
  }
417
443
 
418
- const control = Object.defineProperty(() => ({ ...sentinel }), 'name', { value: 'control' })
419
- expect(() => {
420
- for (const factory of factories.concat(control)) {
421
- const project: unknown = Reflect.apply(factory, undefined, [sentinel])
422
- if (typeof project !== 'object' || project === null) {
423
- throw new Error('A project factory returned no configuration')
424
- }
425
- for (const field of Object.keys(sentinel)) {
426
- expect(Object.getOwnPropertyDescriptor(project, field)?.value).toBeUndefined()
427
- }
428
- }
429
- }).toThrow(/expected/u)
444
+ // A record that carries no string `mode` is refused rather than forwarded as a project
445
+ // with no mode, which would run in `test` again.
446
+ for (const entry of projects) {
447
+ if (typeof entry !== 'function') continue
448
+ expect(() => Reflect.apply(entry, undefined, [{ ...invocation, mode: undefined }])).toThrow(
449
+ 'The project invocation carries no string mode',
450
+ )
451
+ }
430
452
  })
431
453
 
432
454
  it('requires and validates every selected target wrapper', async () => {
@@ -762,7 +762,7 @@ describe('repository policy', () => {
762
762
  // pointer pair and no `.agents/` directory, no rule map, and no skill bridges.
763
763
  // This vendored suite runs there, and every inspector it routes through has to
764
764
  // stay silent on that shape.
765
- it('accepts a target holding the pointer pair and no canon tree', () => {
765
+ it('accepts a target holding the pointer and no canon tree', () => {
766
766
  const scratch = createPolicyScratch({ prefix: 'orkestrel-policy-pointer-' })
767
767
  try {
768
768
  writePolicySurfaceHost(scratch)
@@ -770,10 +770,6 @@ describe('repository policy', () => {
770
770
  'AGENTS.md',
771
771
  '# AGENTS.md\n\nRead `node_modules/@orkestrel/scaffold/dist/host/AGENTS.md` for the canon.\n',
772
772
  )
773
- scratch.write(
774
- 'CLAUDE.md',
775
- '# Claude Code bridge\n\nRead the `AGENTS.md` file beside this one first.\n',
776
- )
777
773
  scratch.write('.claude/settings.json', '{\n\t"permissions": {\n\t\t"allow": []\n\t}\n}\n')
778
774
  scratch.write(
779
775
  '.claude/agents/orkestrel.md',
@@ -220,6 +220,9 @@ export const SKILL_FAMILY_ROOT = '.agents/skills'
220
220
  /** Names the directory whose immediate child directories form the Claude skill bridge family. */
221
221
  export const SKILL_BRIDGE_ROOT = '.claude/skills'
222
222
 
223
+ /** Names the directory whose tree mirrors `.agents/skills/<skill>/scripts/*.ts` as `*.test.ts` proofs. */
224
+ export const SKILL_PROOF_ROOT = 'tests/agents/skills'
225
+
223
226
  /** Holds the minimal valid skill text for physical family controls. */
224
227
  export const SKILL_POLICY_TEXT =
225
228
  '---\nname: sample\ndescription: Use this skill for a policy fixture.\n---\n\n# Skill\n'
@@ -831,6 +834,65 @@ export function readSkillReferences(root: string, name: string): readonly string
831
834
  .sort()
832
835
  }
833
836
 
837
+ /**
838
+ * Lists the TypeScript scripts a canonical skill directory holds.
839
+ *
840
+ * @param root - The workspace root to inspect.
841
+ * @param name - The skill directory name.
842
+ * @returns Each `scripts/*.ts` path relative to the skill directory, sorted.
843
+ */
844
+ export function readSkillScripts(root: string, name: string): readonly string[] {
845
+ const directory = resolvePolicyDirectory(root, `${SKILL_FAMILY_ROOT}/${name}/scripts`)
846
+ if (directory === undefined) return []
847
+ return readdirSync(directory, { withFileTypes: true })
848
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.ts'))
849
+ .map((entry) => `scripts/${entry.name}`)
850
+ .sort()
851
+ }
852
+
853
+ /**
854
+ * Extracts every `scripts/*.ts` path a skill document names as its own.
855
+ *
856
+ * @remarks
857
+ * A path written under another skill's directory, such as
858
+ * `.agents/skills/orkestrel-dispatch/scripts/cite.ts` inside a different skill's document, names
859
+ * that skill's script and is skipped here; the owning skill's own sweep requires it.
860
+ *
861
+ * @param content - The raw SKILL.md text.
862
+ * @param name - The directory name of the skill whose document `content` is.
863
+ * @returns The distinct script paths, relative to the skill directory, in first-mention order.
864
+ */
865
+ export function extractSkillScripts(content: string, name: string): readonly string[] {
866
+ const found: string[] = []
867
+ for (const match of content.matchAll(
868
+ /(?:\.agents\/skills\/([A-Za-z0-9-]+)\/)?(scripts\/[A-Za-z0-9][A-Za-z0-9._-]*\.ts)/gu,
869
+ )) {
870
+ const owner = match[1]
871
+ const script = match[2]
872
+ if (script === undefined || (owner !== undefined && owner !== name)) continue
873
+ if (!found.includes(script)) found.push(script)
874
+ }
875
+ return found
876
+ }
877
+
878
+ /**
879
+ * Extracts every script path a skill document names under another skill's directory.
880
+ *
881
+ * @param content - The raw SKILL.md text.
882
+ * @param name - The directory name of the skill whose document `content` is.
883
+ * @returns The distinct `.agents/skills/<other>/scripts/*.ts` paths, in first-mention order.
884
+ */
885
+ export function extractForeignSkillScripts(content: string, name: string): readonly string[] {
886
+ const found: string[] = []
887
+ for (const match of content.matchAll(
888
+ /\.agents\/skills\/([A-Za-z0-9-]+)\/scripts\/[A-Za-z0-9][A-Za-z0-9._-]*\.ts/gu,
889
+ )) {
890
+ if (match[1] === name || found.includes(match[0])) continue
891
+ found.push(match[0])
892
+ }
893
+ return found
894
+ }
895
+
834
896
  /**
835
897
  * Creates metadata in the canonical skill interface shape.
836
898
  *
@@ -1493,6 +1555,46 @@ export function inspectSkill(
1493
1555
  )
1494
1556
  }
1495
1557
  }
1558
+ const scripts = content === undefined ? [] : extractSkillScripts(content, name)
1559
+ for (const script of scripts) {
1560
+ if (!isPolicyFile(root, `${base}/${script}`)) {
1561
+ violations.push(
1562
+ createPolicyViolation(
1563
+ 'skill',
1564
+ `${base}/${script}`,
1565
+ `SKILL.md script resolves to an exact-case regular file: ${script}`,
1566
+ ),
1567
+ )
1568
+ }
1569
+ }
1570
+ for (const script of readSkillScripts(root, name)) {
1571
+ if (!scripts.includes(script)) {
1572
+ violations.push(
1573
+ createPolicyViolation(
1574
+ 'skill',
1575
+ `${base}/${script}`,
1576
+ `scripts TypeScript file is named by SKILL.md: ${script}`,
1577
+ ),
1578
+ )
1579
+ }
1580
+ const proof = `${SKILL_PROOF_ROOT}/${name}/${script.replace(/\.ts$/u, '.test.ts')}`
1581
+ if (!isPolicyFile(root, proof)) {
1582
+ violations.push(
1583
+ createPolicyViolation('skill', proof, `skill script has a mirrored proof: ${proof}`),
1584
+ )
1585
+ }
1586
+ }
1587
+ for (const script of content === undefined ? [] : extractForeignSkillScripts(content, name)) {
1588
+ if (!isPolicyFile(root, script)) {
1589
+ violations.push(
1590
+ createPolicyViolation(
1591
+ 'skill',
1592
+ script,
1593
+ `SKILL.md names another skill's script that resolves to an exact-case regular file: ${script}`,
1594
+ ),
1595
+ )
1596
+ }
1597
+ }
1496
1598
  const references = resolvePolicyDirectory(root, `${base}/references`)
1497
1599
  if (references !== undefined) {
1498
1600
  for (const entry of readdirSync(references, { withFileTypes: true })) {
@@ -1514,6 +1616,7 @@ export function inspectSkill(
1514
1616
  if (
1515
1617
  path === 'agents' ||
1516
1618
  path === 'references' ||
1619
+ path === 'scripts' ||
1517
1620
  path.startsWith('references/') ||
1518
1621
  (!hasSkill && path.toLowerCase() === 'skill.md') ||
1519
1622
  (!hasMetadata && path.toLowerCase() === 'agents/openai.yaml')
@@ -1524,7 +1627,7 @@ export function inspectSkill(
1524
1627
  createPolicyViolation(
1525
1628
  'skill',
1526
1629
  `${base}/${path}`,
1527
- 'skill directory contains only agents/ and references/ directories',
1630
+ 'skill directory contains only agents/, references/, and scripts/ directories',
1528
1631
  ),
1529
1632
  )
1530
1633
  continue
@@ -1545,6 +1648,7 @@ export function inspectSkill(
1545
1648
  path === 'SKILL.md' ||
1546
1649
  path === 'agents/openai.yaml' ||
1547
1650
  /^references\/[^/]+\.md$/u.test(path) ||
1651
+ /^scripts\/[^/]+\.ts$/u.test(path) ||
1548
1652
  (!hasSkill &&
1549
1653
  (path.toLowerCase() === 'skill.md' || path.toLowerCase().startsWith('skill.md/'))) ||
1550
1654
  (!hasMetadata && path.toLowerCase() === 'agents/openai.yaml') ||
@@ -1556,7 +1660,7 @@ export function inspectSkill(
1556
1660
  createPolicyViolation(
1557
1661
  'skill',
1558
1662
  `${base}/${path}`,
1559
- 'skill directory contains only SKILL.md, agents/openai.yaml, and references/*.md',
1663
+ 'skill directory contains only SKILL.md, agents/openai.yaml, references/*.md, and scripts/*.ts',
1560
1664
  ),
1561
1665
  )
1562
1666
  }
@@ -2574,6 +2678,76 @@ export const POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2574
2678
 
2575
2679
  /** Lists the physical in-family controls for every skill-family assertion class. */
2576
2680
  export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2681
+ {
2682
+ label: 'rejects a named script of another skill that does not exist',
2683
+ membership: 'script paths qualified with another skill directory in canonical skill documents',
2684
+ rule: 'skill',
2685
+ files: [
2686
+ {
2687
+ path: '.agents/skills/sample/SKILL.md',
2688
+ content: SKILL_POLICY_TEXT + '\nRun `node .agents/skills/other/scripts/gone.ts` first.\n',
2689
+ },
2690
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
2691
+ ],
2692
+ violations: [
2693
+ {
2694
+ rule: 'skill',
2695
+ path: '.agents/skills/other/scripts/gone.ts',
2696
+ message:
2697
+ "SKILL.md names another skill's script that resolves to an exact-case regular file: .agents/skills/other/scripts/gone.ts",
2698
+ },
2699
+ ],
2700
+ },
2701
+ {
2702
+ label: 'accepts a named script of another skill that exists',
2703
+ membership: 'script paths qualified with another skill directory in canonical skill documents',
2704
+ rule: 'skill',
2705
+ files: [
2706
+ {
2707
+ path: '.agents/skills/sample/SKILL.md',
2708
+ content:
2709
+ SKILL_POLICY_TEXT + '\nRun `node .agents/skills/other/scripts/present.ts` first.\n',
2710
+ },
2711
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
2712
+ {
2713
+ path: '.agents/skills/other/SKILL.md',
2714
+ content:
2715
+ SKILL_POLICY_TEXT.replace('name: sample', 'name: other') +
2716
+ '\nRun `scripts/present.ts`.\n',
2717
+ },
2718
+ { path: '.agents/skills/other/agents/openai.yaml', content: createSkillMetadata('other') },
2719
+ {
2720
+ path: '.agents/skills/other/scripts/present.ts',
2721
+ content: '// Usage: node .agents/skills/other/scripts/present.ts\n',
2722
+ },
2723
+ { path: 'tests/agents/skills/other/scripts/present.test.ts', content: '' },
2724
+ ],
2725
+ violations: [],
2726
+ },
2727
+ {
2728
+ label: 'rejects a skill script with no mirrored proof',
2729
+ membership: 'skill scripts against tests/agents/skills/<skill>/scripts/*.test.ts',
2730
+ rule: 'skill',
2731
+ files: [
2732
+ {
2733
+ path: '.agents/skills/sample/SKILL.md',
2734
+ content: SKILL_POLICY_TEXT + '\nRun `scripts/lone.ts`.\n',
2735
+ },
2736
+ { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
2737
+ {
2738
+ path: '.agents/skills/sample/scripts/lone.ts',
2739
+ content: '// Usage: node .agents/skills/sample/scripts/lone.ts\n',
2740
+ },
2741
+ ],
2742
+ violations: [
2743
+ {
2744
+ rule: 'skill',
2745
+ path: 'tests/agents/skills/sample/scripts/lone.test.ts',
2746
+ message:
2747
+ 'skill script has a mirrored proof: tests/agents/skills/sample/scripts/lone.test.ts',
2748
+ },
2749
+ ],
2750
+ },
2577
2751
  {
2578
2752
  label: 'rejects an unexported value binding in a skill fence',
2579
2753
  membership: 'named value imports in canonical skill fences',
@@ -2911,7 +3085,8 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2911
3085
  label: 'rejects a non-contract file in a skill directory',
2912
3086
  membership: 'regular files at any depth inside a discovered skill directory',
2913
3087
  rule: 'skill',
2914
- message: 'skill directory contains only SKILL.md, agents/openai.yaml, and references/*.md',
3088
+ message:
3089
+ 'skill directory contains only SKILL.md, agents/openai.yaml, references/*.md, and scripts/*.ts',
2915
3090
  files: [
2916
3091
  { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
2917
3092
  { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },
@@ -2922,7 +3097,7 @@ export const SKILL_POLICY_CONTROLS: readonly PolicyControl[] = Object.freeze([
2922
3097
  label: 'rejects an empty non-contract directory in a skill directory',
2923
3098
  membership: 'directories at any depth inside a discovered skill directory',
2924
3099
  rule: 'skill',
2925
- message: 'skill directory contains only agents/ and references/ directories',
3100
+ message: 'skill directory contains only agents/, references/, and scripts/ directories',
2926
3101
  files: [
2927
3102
  { path: '.agents/skills/sample/SKILL.md', content: SKILL_POLICY_TEXT },
2928
3103
  { path: '.agents/skills/sample/agents/openai.yaml', content: createSkillMetadata('sample') },