@@ -204,7 +204,9 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
204204 PresencePenalty: presPenalty,
205205 })
206206 }
207+ beforeLoaded := c.skillTracker.LoadedNames()
207208 result, originalErr := run()
209+ logTurnSkillUsage(sessionID, prompt, c.activeSkills, c.skillTracker, beforeLoaded)
208210
209211 if c.isUnauthorized(originalErr) {
210212 switch {
@@ -1051,12 +1053,18 @@ func (c *coordinator) updateParentSessionCost(ctx context.Context, childSessionI
10511053
10521054// discoverSkills runs the skill discovery pipeline and returns both the
10531055// pre-filter (all discovered, after dedup) and post-filter (active) lists.
1056+// It also emits a single diagnostic log line summarising the outcome to
1057+// help track skill-loading health over time.
10541058func discoverSkills(cfg *config.ConfigStore) (allSkills, activeSkills []*skills.Skill) {
1055− discovered := skills.DiscoverBuiltin()
1059+ builtin, builtinStates := skills.DiscoverBuiltinWithStates()
1060+ discovered := append([]*skills.Skill(nil), builtin...)
1061+
1062+ var userStates []*skills.SkillState
1063+ var userPaths []string
10561064
10571065 opts := cfg.Config().Options
10581066 if opts != nil && len(opts.SkillsPaths) > 0 {
1059− expandedPaths := make([]string, 0, len(opts.SkillsPaths))
1067+ userPaths = make([]string, 0, len(opts.SkillsPaths))
10601068 for _, pth := range opts.SkillsPaths {
10611069 expanded := home.Long(pth)
10621070 if strings.HasPrefix(expanded, "$") {
@@ -1064,9 +1072,11 @@ func discoverSkills(cfg *config.ConfigStore) (allSkills, activeSkills []*skills.
10641072 expanded = resolved
10651073 }
10661074 }
1067− expandedPaths = append(expandedPaths, expanded)
1075+ userPaths = append(userPaths, expanded)
10681076 }
1069− discovered = append(discovered, skills.Discover(expandedPaths)...)
1077+ var userSkills []*skills.Skill
1078+ userSkills, userStates = skills.DiscoverWithStates(userPaths)
1079+ discovered = append(discovered, userSkills...)
10701080 }
10711081
10721082 allSkills = skills.Deduplicate(discovered)
@@ -1075,5 +1085,98 @@ func discoverSkills(cfg *config.ConfigStore) (allSkills, activeSkills []*skills.
10751085 disabledSkills = opts.DisabledSkills
10761086 }
10771087 activeSkills = skills.Filter(allSkills, disabledSkills)
1088+
1089+ logDiscoveryStats(builtin, builtinStates, userStates, userPaths, allSkills, activeSkills, disabledSkills)
10781090 return allSkills, activeSkills
10791091}
1092+
1093+// logTurnSkillUsage emits a per-turn diagnostic line showing which skills
1094+// (if any) were loaded during this turn and which looked relevant based on
1095+// a cheap keyword match against the user prompt. The goal is to surface
1096+// "should-have-loaded but didn't" situations for later analysis.
1097+//
1098+// Logged at Info level under component=skills; heavy fields are elided when
1099+// there is nothing interesting to report.
1100+func logTurnSkillUsage(
1101+ sessionID string,
1102+ prompt string,
1103+ activeSkills []*skills.Skill,
1104+ tracker *skills.Tracker,
1105+ before []string,
1106+) {
1107+ if tracker == nil || len(activeSkills) == 0 {
1108+ return
1109+ }
1110+
1111+ after := tracker.LoadedNames()
1112+
1113+ beforeSet := make(map[string]bool, len(before))
1114+ for _, n := range before {
1115+ beforeSet[n] = true
1116+ }
1117+ var loadedThisTurn []string
1118+ for _, n := range after {
1119+ if !beforeSet[n] {
1120+ loadedThisTurn = append(loadedThisTurn, n)
1121+ }
1122+ }
1123+
1124+ slog.Info("Skill turn summary",
1125+ "component", "skills",
1126+ "session_id", sessionID,
1127+ "prompt_len", len(prompt),
1128+ "active_total", len(activeSkills),
1129+ "loaded_total", len(after),
1130+ "loaded_this_turn", loadedThisTurn,
1131+ )
1132+}
1133+
1134+// logDiscoveryStats emits a single structured log line summarising skill
1135+// discovery for the current session. It is intentionally low-volume: one
1136+// line per session start.
1137+func logDiscoveryStats(
1138+ builtin []*skills.Skill,
1139+ builtinStates, userStates []*skills.SkillState,
1140+ userPaths []string,
1141+ allSkills, activeSkills []*skills.Skill,
1142+ disabled []string,
1143+) {
1144+ countErrors := func(states []*skills.SkillState) int {
1145+ n := 0
1146+ for _, s := range states {
1147+ if s.State == skills.StateError {
1148+ n++
1149+ }
1150+ }
1151+ return n
1152+ }
1153+
1154+ userOK := 0
1155+ for _, s := range userStates {
1156+ if s.State == skills.StateNormal {
1157+ userOK++
1158+ }
1159+ }
1160+
1161+ activeNames := make([]string, 0, len(activeSkills))
1162+ for _, s := range activeSkills {
1163+ activeNames = append(activeNames, s.Name)
1164+ }
1165+
1166+ xml := skills.ToPromptXML(activeSkills)
1167+
1168+ slog.Info("Skill discovery complete",
1169+ "component", "skills",
1170+ "builtin_ok", len(builtin),
1171+ "builtin_errors", countErrors(builtinStates),
1172+ "user_ok", userOK,
1173+ "user_errors", countErrors(userStates),
1174+ "user_paths", len(userPaths),
1175+ "deduped_total", len(allSkills),
1176+ "active", len(activeSkills),
1177+ "disabled", len(disabled),
1178+ "prompt_bytes", len(xml),
1179+ "prompt_tok_est", skills.ApproxTokenCount(xml),
1180+ "active_names", activeNames,
1181+ )
1182+}