mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
fix(skills): render runtime discovery warnings as notices
This commit is contained in:
1 parent
be5c98c744
commit
82c27867b3
12 files changed
+145
-3
No files matched your search
@@ -179,6 +179,10 @@ When the listing changes:
|
||||
this project; add `--global` to apply everywhere.
|
||||
- `reasonix doctor` reports skill health warnings, such as a missing
|
||||
description or a required capability that is not available.
|
||||
- The terminal UI and Studio show discovery diagnostics as renderer-managed
|
||||
notices, including new diagnostics discovered while completing a slash command.
|
||||
Identical diagnostics are shown once per skill store; changed diagnostics remain
|
||||
visible. Noninteractive commands keep their stderr diagnostics.
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
|
||||
@@ -16,7 +16,7 @@ func (b *builder) liveSkillSource() func() *skill.Store {
|
||||
ProjectRoot: b.root, ReasonixHomeDir: b.roots.Home(), CustomPaths: cfg.SkillCustomPaths(),
|
||||
PluginPaths: cfg.PluginPackageSkillOwners(), PluginAgentPaths: cfg.PluginPackageAgentOwners(),
|
||||
ExcludedPaths: cfg.SkillExcludedPaths(), DisabledNames: func() []string { return disabledSkillNames(cfg, b.root) },
|
||||
MaxDepth: cfg.SkillMaxDepth(), Stderr: io.Discard, SuppressWarnings: true,
|
||||
MaxDepth: cfg.SkillMaxDepth(), Stderr: io.Discard, SuppressWarnings: cfg.Skills.SuppressWarnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,9 @@ type Options struct {
|
||||
// Interactive terminals must pass a private writer (or io.Discard) so
|
||||
// background output cannot corrupt the TUI's raw mode.
|
||||
Stderr io.Writer
|
||||
// DiagnosticSink receives skill discovery warnings as renderer-managed notices.
|
||||
// Nil preserves the noninteractive stderr destination.
|
||||
DiagnosticSink event.Sink
|
||||
// OnPhase receives completed assembly timings synchronously, including on a later failure.
|
||||
OnPhase func(Phase)
|
||||
// Project root for config, skills, memory, commands, hooks, and tool
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package boot
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"reasonix/internal/contract/event"
|
||||
)
|
||||
|
||||
type skillDiagnosticWriter struct{ sink event.Sink }
|
||||
|
||||
func (w skillDiagnosticWriter) Write(p []byte) (int, error) {
|
||||
report(w.sink, event.Event{Level: event.LevelWarn, Text: strings.TrimRight(string(p), "\r\n")})
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package boot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/ext/skill"
|
||||
)
|
||||
|
||||
func TestSkillDiagnosticsReachRendererAcrossRediscovery(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
home := t.TempDir()
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
var raw bytes.Buffer
|
||||
var notices []event.Event
|
||||
opts := Options{Stderr: &raw, DiagnosticSink: event.FuncSink(func(e event.Event) { notices = append(notices, e) })}
|
||||
assembly := buildSkillAssembly(opts, &config.Config{}, root, true, "")
|
||||
notices = nil
|
||||
raw.Reset()
|
||||
dir := filepath.Join(root, ".reasonix", "skills", "new-warning")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(dir, "SKILL.md")
|
||||
write := func(name string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte("---\nname: "+name+"\n---\nA runtime skill.\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write("new-warning")
|
||||
assembly.store.SlashList()
|
||||
if raw.Len() != 0 {
|
||||
t.Fatalf("runtime diagnostic overwrote terminal stderr: %s", raw.String())
|
||||
}
|
||||
if len(notices) != 1 || notices[0].Kind != event.Notice || notices[0].Level != event.LevelWarn || !strings.Contains(notices[0].Text, "new-warning") {
|
||||
t.Fatalf("renderer notices = %+v", notices)
|
||||
}
|
||||
assembly.store.SlashList()
|
||||
if len(notices) != 1 {
|
||||
t.Fatalf("identical diagnostic repeated: %+v", notices)
|
||||
}
|
||||
write("changed-warning")
|
||||
time.Sleep(2100 * time.Millisecond)
|
||||
assembly.store.SlashList()
|
||||
if len(notices) != 2 || !strings.Contains(notices[1].Text, "changed-warning") {
|
||||
t.Fatalf("changed diagnostic lost: %+v", notices)
|
||||
}
|
||||
assembly.store.ConfigureDiscoverySource(func() *skill.Store {
|
||||
return skill.New(skill.Options{ProjectRoot: root, ReasonixHomeDir: home, Stderr: io.Discard})
|
||||
})
|
||||
assembly.store.SlashList()
|
||||
if len(notices) != 2 {
|
||||
t.Fatalf("live source repeated diagnostic: %+v", notices)
|
||||
}
|
||||
write("live-warning")
|
||||
assembly.store.SlashList()
|
||||
if len(notices) != 3 || !strings.Contains(notices[2].Text, "live-warning") {
|
||||
t.Fatalf("live diagnostic lost: %+v", notices)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillDiagnosticDestinationsAtStartup(t *testing.T) {
|
||||
for _, interactive := range []bool{false, true} {
|
||||
for _, suppressed := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("renderer=%t/suppressed=%t", interactive, suppressed), func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("REASONIX_HOME", t.TempDir())
|
||||
dir := filepath.Join(root, ".reasonix", "skills", "startup-warning")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("---\nname: startup-warning\n---\nBody.\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var raw bytes.Buffer
|
||||
var notices []event.Event
|
||||
opts := Options{Stderr: &raw}
|
||||
if interactive {
|
||||
opts.DiagnosticSink = event.FuncSink(func(e event.Event) { notices = append(notices, e) })
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
cfg.Skills.SuppressWarnings = suppressed
|
||||
assembly := buildSkillAssembly(opts, cfg, root, true, "")
|
||||
assembly.store.SlashList()
|
||||
if interactive && raw.Len() != 0 {
|
||||
t.Fatalf("raw stderr: %s", raw.String())
|
||||
}
|
||||
if suppressed {
|
||||
if len(notices) != 0 || raw.Len() != 0 {
|
||||
t.Fatalf("suppressed diagnostic emitted: %+v %q", notices, raw.String())
|
||||
}
|
||||
} else if interactive {
|
||||
if len(notices) != 1 {
|
||||
t.Fatalf("startup notices = %+v", notices)
|
||||
}
|
||||
} else if !strings.Contains(raw.String(), "startup-warning") {
|
||||
t.Fatalf("missing noninteractive stderr: %q", raw.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ func buildSkillAssembly(opts Options, cfg *config.Config, root string, implicit
|
||||
reuse := opts.ReuseAssembly != nil && shouldReuseDiscovery(opts.PreviousPlan) &&
|
||||
opts.ReuseAssembly.ImplicitSkillInvocation == implicit
|
||||
stderr := opts.Stderr
|
||||
if reuse {
|
||||
stderr = io.Discard
|
||||
if opts.DiagnosticSink != nil {
|
||||
stderr = skillDiagnosticWriter{sink: opts.DiagnosticSink}
|
||||
}
|
||||
a.store = skill.New(skill.Options{
|
||||
ProjectRoot: root, ReasonixHomeDir: home, CustomPaths: cfg.SkillCustomPaths(), PluginPaths: cfg.PluginPackageSkillOwners(),
|
||||
|
||||
@@ -18,6 +18,7 @@ func (s *Store) discover(scan func(discoveryRoot) []Skill) []Skill {
|
||||
return nil
|
||||
}
|
||||
current.admits = s.admits
|
||||
current.stderr, current.said = s.stderr, s.said
|
||||
return current.discover(current.discoverRoot)
|
||||
}
|
||||
var out []Skill
|
||||
|
||||
@@ -32,6 +32,7 @@ type cliBuildOverrides struct {
|
||||
GoalTurnsUnreachable bool
|
||||
HoldOwedChecks bool
|
||||
Stderr io.Writer
|
||||
DiagnosticSink event.Sink
|
||||
OnSessionRecovered func(control.SessionRecoveryInfo) error
|
||||
Ablation ablation.Set
|
||||
// NewConversation marks an interactive start that is not resuming anything, so the user's default mode applies.
|
||||
@@ -82,6 +83,7 @@ func cliProfileBuildOptions(modelName string, maxStepsOverride int, requireKey b
|
||||
StatsSource: surface.CLI,
|
||||
FeedbackSurface: feedback.SurfaceTUI,
|
||||
Stderr: overrides.Stderr,
|
||||
DiagnosticSink: overrides.DiagnosticSink,
|
||||
OnSessionRecovered: overrides.OnSessionRecovered,
|
||||
Ablation: overrides.Ablation,
|
||||
NewConversation: overrides.NewConversation,
|
||||
|
||||
@@ -91,6 +91,7 @@ func runTUI(args []string, version string) int {
|
||||
Version: version, WorkspaceRoot: workspaceRoot, OnSessionRecovered: cliSessionRecoveredHandler(leases),
|
||||
Effort: f.effortOverride(), PermissionAllow: allowed, AdditionalDirs: f.addDirs,
|
||||
NewConversation: resumePath == "",
|
||||
DiagnosticSink: bc,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
|
||||
|
||||
@@ -354,6 +354,7 @@ func (h *Hub) openBound(ctx context.Context, req OpenRequest, inherited *agentco
|
||||
Sink: paneSink,
|
||||
NativeSurfaces: h.opts.NativeSurfaces,
|
||||
Stderr: os.Stderr,
|
||||
DiagnosticSink: paneSink,
|
||||
StatsSource: h.surface(),
|
||||
FeedbackSurface: feedbackSurface(h.surface()),
|
||||
AccountsURL: accountsBaseURL(),
|
||||
|
||||
@@ -237,6 +237,7 @@ func (s *Server) statsSurface() surface.Surface { return s.surface.Or(surface.Se
|
||||
// switch would quietly serve another project's conversations.
|
||||
func (s *Server) rebuildOptions(cur control.SessionAPI, ref string) boot.Options {
|
||||
opts := boot.Options{Model: ref, Sink: s.rebuildSink(), NativeSurfaces: true, Stderr: os.Stderr, StatsSource: s.statsSurface(), FeedbackSurface: feedbackSurface(s.statsSurface()), AccountsURL: accountsBaseURL(), ProviderResolver: s.resolver, CleanupPendingReconciler: BackgroundCleanupReconciler}
|
||||
opts.DiagnosticSink = s.rebuildSink()
|
||||
if s.lastBuild != nil {
|
||||
opts.ComputerDriver = s.lastBuild.ComputerDriver
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
## 修复
|
||||
|
||||
- TUI and Studio skill diagnostics use renderer-managed notices, including first/new warnings during slash completion (#13655).
|
||||
- Identical-warning deduplication from #13656 and noninteractive stderr output remain intact.
|
||||
Reference in new issue
Block a user