feat(skills): session-start and turn-end hooks and protected paths declared in SKILL.md
A skill's SKILL.md frontmatter may declare session-start (output into <skill_session_context>, at session start, /new, resume and after compaction), turn-end (after every run, in the background; output or a failure reaches the next run as a hidden notice, repeats suppressed until a clean run) and protected-paths (write/edit refused inside them). Hooks run from the skill directory with VOLCANIC_WORKSPACE, VOLCANIC_SKILL_DIR and the chat's TZ; waits are bounded by each hook's timeout and failures never break a run. /skills shows declared hooks and broken fields.
This commit is contained in:
@@ -225,8 +225,8 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every skill directory (SKILL.md, INSTRUCTIONS.md or jobs/, in any combination), with what it holds; a broken job
|
||||
/// file is listed with its problem (scheduler on).
|
||||
/// Every skill directory (SKILL.md, INSTRUCTIONS.md or jobs/, in any combination), with what it holds (hooks and
|
||||
/// protected paths included); a broken hook field or job file is listed with its problem (job files: scheduler on).
|
||||
/// </summary>
|
||||
private async Task<string> SkillsAsync(ChatContext chat, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -247,11 +247,14 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon
|
||||
var jobsText = jobCount == 1 ? "1 job" : $"{jobCount.ToString(CultureInfo.InvariantCulture)} jobs";
|
||||
var hasInstructions = instructions.TryGetValue(dir, out var i);
|
||||
var instructionsText = i is { Truncated: true } ? "INSTRUCTIONS.md, truncated in the prompt" : "INSTRUCTIONS.md";
|
||||
var hooks = SkillHookFiles.Read(cwd, dir);
|
||||
var hooksText = hooks?.Summary() ?? "";
|
||||
if (skills.TryGetValue(dir, out var skill))
|
||||
{
|
||||
var extras = new List<string>();
|
||||
if (hasInstructions) extras.Add(instructionsText);
|
||||
if (jobCount > 0) extras.Add(jobsText);
|
||||
if (hooksText.Length > 0) extras.Add(hooksText);
|
||||
lines.Add($"• **{skill.Name}** – {skill.Description}" + (extras.Count > 0 ? $" (+ {string.Join(", ", extras)})" : ""));
|
||||
}
|
||||
else
|
||||
@@ -259,9 +262,12 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon
|
||||
var parts = new List<string>();
|
||||
if (hasInstructions) parts.Add(instructionsText);
|
||||
if (jobCount > 0) parts.Add(jobsText);
|
||||
if (hooksText.Length > 0) parts.Add(hooksText);
|
||||
if (parts.Count == 0) parts.Add("SKILL.md without a description (not listed to me)");
|
||||
lines.Add($"• **{name}** – {string.Join(", ", parts)}");
|
||||
}
|
||||
foreach (var error in hooks?.Errors ?? [])
|
||||
lines.Add($" ⚠️ {Path.GetRelativePath(cwd, hooks!.SkillFile)}: {error}");
|
||||
foreach (var broken in jobs.Where(j => j.Status is SkillJobStatus.Invalid or SkillJobStatus.OverLimit && j.Id.StartsWith(name + "/", StringComparison.Ordinal)))
|
||||
lines.Add($" ⚠️ {broken.File}: {broken.Problem}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user