formatSkillsForPrompt function

String formatSkillsForPrompt(
  1. List<Skill> skills, {
  2. bool forModel = true,
  3. Iterable<String> touchedPaths = const [],
  4. String? cwd,
  5. DateTime? scannedAt,
  6. Set<String> midSessionNames = const {},
  7. bool liveRediscovery = true,
})

Renders the progressive-disclosure block for the system prompt: metadata only — the agent loads a skill's file with the read tool when the task matches its description. Empty when there are no skills.

forModel applies Claude's invocation flags: disable-model-invocation skills are excluded, and path-gated skills (paths: / applyTo:) enter only when touchedPaths matches one of their globs.

Freshness disclosure (gh-1440): hosts that track discovery freshness pass scannedAt (the wall-clock stamp of the last discovery scan — NOT the current time, so unchanged state renders byte-identical sections) plus midSessionNames, the lowercase names discovered after the boot scan; those entries carry an added mid-session (<source>) flag so the model does not read the index's mere presence as endorsement. With liveRediscovery off the section ends in an explicit staleness footer instead — a documented, visible downgrade to boot-snapshot semantics. Omitting scannedAt (the default) renders the legacy shape exactly.

Implementation

String formatSkillsForPrompt(
  List<Skill> skills, {
  bool forModel = true,
  Iterable<String> touchedPaths = const [],
  String? cwd,
  DateTime? scannedAt,
  Set<String> midSessionNames = const {},
  bool liveRediscovery = true,
}) {
  final listed = forModel
      ? skills
            .where(
              (s) =>
                  s.modelInvocable &&
                  s.matchesTouchedPaths(touchedPaths, cwd: cwd),
            )
            .toList()
      : skills;
  if (listed.isEmpty) return '';
  String escape(String text) => text
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;');
  final buffer = StringBuffer()
    ..writeln(
      'The following skills provide specialized instructions for specific '
      'tasks.',
    )
    ..writeln(
      "Use the read tool to load a skill's file when the task matches its "
      'description.',
    )
    ..writeln(
      "When a skill file references a relative path, resolve it against the "
      'skill directory (parent of SKILL.md) and use that absolute path in '
      'tool commands.',
    )
    ..writeln()
    ..writeln('<available_skills>');
  for (final skill in listed) {
    buffer
      ..writeln('  <skill>')
      ..writeln('    <name>${escape(skill.name)}</name>')
      ..writeln('    <description>${escape(skill.description)}</description>')
      ..writeln('    <location>${escape(skill.filePath)}</location>');
    if (scannedAt != null &&
        midSessionNames.contains(skill.name.toLowerCase())) {
      buffer.writeln('    <status>added mid-session (${skill.source.name})</status>');
    }
    buffer.writeln('  </skill>');
  }
  buffer.write('</available_skills>');
  if (scannedAt != null) {
    final stamp = scannedAt.toUtc().toIso8601String();
    buffer
      ..writeln()
      ..writeln(
        liveRediscovery
            ? 'skills index scanned at $stamp'
            : 'skills index scanned at $stamp — '
                'newer files are NOT reflected; /skills reload to refresh',
      );
  }
  return buffer.toString();
}