formatSkillsForPrompt function
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('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>');
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();
}