package tools
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"harness/internal/skills"
)
// SkillArgs is the skill input. The name is constrained to the discovered
// slugs by an enum patched into the schema, so a hallucinated name is a schema
// violation the model sees at once rather than a lookup failure it has to
// reason about.
type SkillArgs struct {
Name string `json:"name" jsonschema:"description=Which skill to load"`
Args string `json:"args,omitempty" jsonschema:"description=Arguments for the skill; whatever followed the skill name"`
}
// NewSkill builds the skill tool over the skills discovered at startup. It
// returns nil when there are none, so the caller registers nothing and the
// prompt is byte-identical to a harness without the feature.
func NewSkill(found []skills.Skill) Tool {
if len(found) == 0 {
return nil
}
byName := make(map[string]skills.Skill, len(found))
names := make([]string, 0, len(found))
var listing strings.Builder
for _, s := range found {
byName[s.Name] = s
names = append(names, s.Name)
fmt.Fprintf(&listing, "\n %s: %s", s.Name, s.Description)
}
sort.Strings(names)
desc := strings.TrimSpace(`
Load a skill: a set of instructions for one kind of work. Invoke a skill when
its description matches what you are about to do — before starting, not after.
The instructions come back as the tool result; follow them.
Available skills:`) + listing.String()
t := New("skill", desc, func(_ context.Context, in SkillArgs) (string, error) {
s, ok := byName[in.Name]
if !ok {
return "", fmt.Errorf("no skill named %q; available: %s", in.Name, strings.Join(names, ", "))
}
var b strings.Builder
fmt.Fprintf(&b, "Skill: %s\nDirectory: %s\n", s.Name, s.Dir)
b.WriteString("Relative paths below resolve against that directory.\n\n")
b.WriteString(s.Body)
if a := strings.TrimSpace(in.Args); a != "" {
b.WriteString("\n\nARGUMENTS: " + a)
}
return b.String(), nil
})
return withEnum(t, "name", names)
}
// withEnum patches an enum onto one string property of a reflected schema.
// invopop/jsonschema derives the schema from struct tags, which cannot carry a
// list only known at startup.
func withEnum(t Tool, property string, values []string) Tool {
var m map[string]any
if err := json.Unmarshal(t.InputSchema(), &m); err != nil {
return t
}
props, ok := m["properties"].(map[string]any)
if !ok {
return t
}
p, ok := props[property].(map[string]any)
if !ok {
return t
}
p["enum"] = values
patched, err := json.Marshal(m)
if err != nil {
return t
}
return patchedSchema{Tool: t, schema: patched}
}
// patchedSchema wraps a Tool to override only its input schema.
type patchedSchema struct {
Tool
schema json.RawMessage
}
// InputSchema returns the schema with the patched enum, overriding the
// embedded Tool's reflected one.
func (p patchedSchema) InputSchema() json.RawMessage { return p.schema }