Skip to content
Merged
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,11 @@ dimensions and larger corpora are where the graph earns its keep. With
knowledge into every turn; a project session can index its whole folder and
keep it fresh as files change.

**Skills.** Markdown files with YAML front matter in `~/.antares/skills`. The
agent writes its own after solving something non-obvious; the catalogue (names
and descriptions only) goes in the prompt, and full bodies are fetched on demand
so the context stays small.
**Skills.** Markdown procedures in configured Antares directories and twelve
conventional user/project locations, discovered and refreshed automatically.
Imported content is read-only; enable/disable preferences live in Antares config.
Project chats use their own catalog. Names and descriptions go in the prompt; full bodies are fetched on
demand. See [docs/skills.md](docs/skills.md) for paths, precedence, and refresh timing.

**Scheduling.** A five-field cron parser plus `@daily`/`@every 90m` shorthands.
Jobs are natural-language prompts that run unattended and can deliver their
Expand Down
96 changes: 78 additions & 18 deletions cmd/antares/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,20 @@ func cmdTUI() error {
// runtimeServices bundles everything a running server needs, so a config reload
// can rebuild the pieces that depend on configuration.
type runtimeServices struct {
mu sync.Mutex
cfg *config.Config
db store.Store
shell *tools.ShellManager
agent *agent.Agent
skills *skills.Manager
cron *cron.Runner
gateway *gateway.Manager
mcp *mcp.Manager
social *socialbrowser.Manager
mu sync.Mutex
cfg *config.Config
db store.Store
shell *tools.ShellManager
agent *agent.Agent
skills *skills.Manager
cron *cron.Runner
gateway *gateway.Manager
mcp *mcp.Manager
social *socialbrowser.Manager
skillsHome string
skillsProjectDir string
skillsCancel context.CancelFunc
skillsDone chan struct{}
}

func bootstrap(ctx context.Context) (*runtimeServices, error) {
Expand All @@ -215,6 +219,10 @@ func bootstrap(ctx context.Context) (*runtimeServices, error) {
if err != nil {
return nil, err
}
cfg, err = migrateSkillState(cfg)
if err != nil {
return nil, err
}
if err := logx.Setup(cfg.Logging.Level, cfg.Logging.File, cfg.Logging.JSON); err != nil {
return nil, fmt.Errorf("setting up logging: %w", err)
}
Expand Down Expand Up @@ -274,9 +282,20 @@ func bootstrap(ctx context.Context) (*runtimeServices, error) {
slog.Info("unpacked the security skill library", "count", n)
}

skillDirs := append(append([]string{}, cfg.Skills.Dirs...), "~/.antares/security-skills")
skillMgr := skills.NewManager(expandAll(skillDirs))
skillMgr.SetPackDirs([]string{packDir})
skillsHome, err := os.UserHomeDir()
if err != nil || strings.TrimSpace(skillsHome) == "" {
slog.Warn("automatic user skills unavailable", "error", err)
skillsHome = ""
}
skillsProjectDir, err := os.Getwd()
if err != nil {
slog.Warn("automatic project skills unavailable", "error", err)
skillsProjectDir = ""
}
skillMgr := skills.NewManager(skills.Options{
Dirs: expandAll(cfg.Skills.Dirs), PackDirs: []string{packDir},
UserHome: skillsHome, ProjectDir: skillsProjectDir,
})
if err := skillMgr.Reload(); err != nil {
slog.Warn("some skills failed to load", "error", err)
}
Expand All @@ -300,6 +319,7 @@ func bootstrap(ctx context.Context) (*runtimeServices, error) {
ag.SetRoles(roleReg)

rt := &runtimeServices{cfg: cfg, db: db, shell: shell, agent: ag, skills: skillMgr}
rt.skillsHome, rt.skillsProjectDir = skillsHome, skillsProjectDir
rt.social = socialbrowser.New()
ag.SetSocialBrowser(rt.social)

Expand Down Expand Up @@ -328,6 +348,7 @@ func bootstrap(ctx context.Context) (*runtimeServices, error) {
}
}

rt.startSkillRefresh(ctx)
return rt, nil
}

Expand Down Expand Up @@ -697,6 +718,10 @@ func (rt *runtimeServices) reload() error {
if err != nil {
return err
}
cfg, err = migrateSkillState(cfg)
if err != nil {
return err
}
cfg, _ = config.Effective(rt.cfg, cfg)
previous := rt.cfg
if err := cfg.Server.ValidateListen(); err != nil {
Expand All @@ -719,13 +744,12 @@ func (rt *runtimeServices) reload() error {
rt.agent.SetRAG(ragProvider)

packDir := config.Path("security-skills")
skillDirs := append(append([]string{}, cfg.Skills.Dirs...), "~/.antares/security-skills")
rt.skills = skills.NewManager(expandAll(skillDirs))
rt.skills.SetPackDirs([]string{packDir})
if err := rt.skills.Reload(); err != nil {
if err := rt.skills.Reconfigure(skills.Options{
Dirs: expandAll(cfg.Skills.Dirs), PackDirs: []string{packDir},
UserHome: rt.skillsHome, ProjectDir: rt.skillsProjectDir,
}); err != nil {
slog.Warn("some skills failed to load", "error", err)
}
rt.agent.SetSkills(rt.skills)

if cfg.Plugins.Enabled {
pluginMgr := plugin.NewManager(expandAll(cfg.Plugins.Dirs))
Expand Down Expand Up @@ -756,7 +780,43 @@ func (rt *runtimeServices) reload() error {
return nil
}

// startSkillRefresh owns the one catalog worker for this runtime, including when
// skills are loaded for the dashboard but disabled for agent prompts and tools.
func (rt *runtimeServices) startSkillRefresh(ctx context.Context) {
if rt == nil {
return
}
rt.mu.Lock()
defer rt.mu.Unlock()
if rt.skills == nil || rt.skillsDone != nil {
return
}
ctx, rt.skillsCancel = context.WithCancel(ctx)
done := make(chan struct{})
rt.skillsDone = done
mgr := rt.skills
go func() {
defer close(done)
mgr.Watch(ctx, 5*time.Second)
}()
}

func (rt *runtimeServices) stopSkillRefresh() {
if rt == nil {
return
}
rt.mu.Lock()
cancel, done := rt.skillsCancel, rt.skillsDone
rt.mu.Unlock()
if cancel == nil {
return
}
cancel()
<-done
}

func (rt *runtimeServices) close() {
rt.stopSkillRefresh()
if rt.mcp != nil {
rt.mcp.Close()
}
Expand Down
24 changes: 24 additions & 0 deletions cmd/antares/skills.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"fmt"

"github.com/enowdev/antares/internal/config"
"github.com/enowdev/antares/internal/skills"
)

// migrateSkillState imports only the selected configured sources, once per profile.
func migrateSkillState(cfg *config.Config) (*config.Config, error) {
if cfg.Skills.FrontmatterMigrated {
return cfg, nil
}
manager := skills.NewManager(skills.Options{Dirs: expandAll(cfg.Skills.Dirs)})
if err := manager.Reload(); err != nil {
return nil, fmt.Errorf("migrate skill preferences: %w", err)
}
fresh, err := config.MigrateSkillState(manager.LegacyDisabled())
if err != nil {
return nil, fmt.Errorf("migrate skill preferences: %w", err)
}
return fresh, nil
}
Loading
Loading