From 7eabb27272cd9eb24ec1043d58735e4ecddb760e Mon Sep 17 00:00:00 2001 From: "zhangheng.023" Date: Fri, 26 Jun 2026 12:30:54 +0800 Subject: [PATCH] fix: include hint in update JSON errors and explain skill-name charset --- cmd/update/update.go | 16 ++++++++++------ cmd/update/update_test.go | 4 ++++ internal/skillscheck/sync.go | 2 +- internal/skillscheck/sync_test.go | 1 + 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cmd/update/update.go b/cmd/update/update.go index 598d65dc..691f873a 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -190,14 +190,18 @@ func updateRun(opts *UpdateOptions) error { // --- Output helpers --- // reportError emits the failure on the requested surface: JSON mode prints the -// {ok:false, error:{type, message}} envelope to stdout and signals the typed -// error's exit code bare; human mode returns the typed error for the -// dispatcher to render. +// {ok:false, error:{type, message, hint?}} envelope to stdout and signals the +// typed error's exit code bare; human mode returns the typed error for the +// dispatcher to render. The hint is included only when the typed error carries +// one, so AI-agent/script consumers reading JSON get the same actionable +// guidance humans see on stderr. func reportError(opts *UpdateOptions, io *cmdutil.IOStreams, errType string, typedErr errs.TypedError) error { if opts.JSON { - output.PrintJson(io.Out, map[string]interface{}{ - "ok": false, "error": map[string]interface{}{"type": errType, "message": typedErr.ProblemDetail().Message}, - }) + errObj := map[string]interface{}{"type": errType, "message": typedErr.ProblemDetail().Message} + if hint := typedErr.ProblemDetail().Hint; hint != "" { + errObj["hint"] = hint + } + output.PrintJson(io.Out, map[string]interface{}{"ok": false, "error": errObj}) return output.ErrBare(output.ExitCodeOf(typedErr)) } return typedErr diff --git a/cmd/update/update_test.go b/cmd/update/update_test.go index 6a82e22c..80798919 100644 --- a/cmd/update/update_test.go +++ b/cmd/update/update_test.go @@ -1681,6 +1681,10 @@ func TestUpdate_InvalidSkillsFlag_JSONExit2(t *testing.T) { if !strings.Contains(stdout.String(), `"type": "validation"`) { t.Fatalf("JSON output missing validation type: %s", stdout.String()) } + // spec ยง3.8: JSON validation errors must carry the actionable hint too. + if !strings.Contains(stdout.String(), `"hint"`) { + t.Fatalf("JSON output missing hint field: %s", stdout.String()) + } } func TestUpdate_UnknownSkillResult_Exit2(t *testing.T) { diff --git a/internal/skillscheck/sync.go b/internal/skillscheck/sync.go index 87b72530..281b680b 100644 --- a/internal/skillscheck/sync.go +++ b/internal/skillscheck/sync.go @@ -72,7 +72,7 @@ func ParseSuiteSelection(rawNames []string) (*SuiteSelection, error) { } } if len(invalid) > 0 { - return nil, fmt.Errorf("invalid skill name(s): %s", strings.Join(invalid, ", ")) + return nil, fmt.Errorf("invalid skill name(s): %s (skill names use only letters, digits, and _ : -)", strings.Join(invalid, ", ")) } sort.Strings(cleaned) return &SuiteSelection{Skills: cleaned}, nil diff --git a/internal/skillscheck/sync_test.go b/internal/skillscheck/sync_test.go index 8abbcc3b..b7f11058 100644 --- a/internal/skillscheck/sync_test.go +++ b/internal/skillscheck/sync_test.go @@ -869,6 +869,7 @@ func TestParseSuiteSelection(t *testing.T) { {name: "all mixed", input: []string{"all", "lark-im"}, wantErr: "cannot be combined"}, {name: "empty", input: []string{"", " "}, wantErr: "at least one"}, {name: "invalid name", input: []string{"bad name"}, wantErr: "invalid skill name"}, + {name: "invalid name explains charset", input: []string{"bad name"}, wantErr: "letters, digits, and _ : -"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {