Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
454c807
docs(skills): status-page card — confirm gate + rollback reporting fo…
ysyneu Jul 31, 2026
eace839
Merge pull request #129 from flashcatcloud/docs/status-page-card-muta…
ysyneu Jul 31, 2026
19937ad
docs(skills): back up critical config before mutating — undo-path dis…
ysyneu Aug 4, 2026
91e5b58
Fix cligen schema walker to recurse through nested array items
ysyneu Aug 6, 2026
96eecd0
Merge pull request #135 from flashcatcloud/feat/skilldoc-nested-filte…
ysyneu Aug 6, 2026
8dfdac3
feat(skilldoc): subset fences — split one command group across cards
ysyneu Aug 6, 2026
10f9df5
docs(skill): split channel/incident cards; add shared filters reference
ysyneu Aug 6, 2026
0ba6179
docs(skill),fix(skilldoc): review fixes — integration_id equivalence;…
ysyneu Aug 6, 2026
3b2baa4
refactor(skilldoc): simplify-pass — segment-bounded prefixes, single …
ysyneu Aug 6, 2026
824acfb
Merge pull request #136 from flashcatcloud/feat/skill-cards-split
ysyneu Aug 6, 2026
c778b0a
fix(cli): state the 100-row server cap in alert/alert-event --limit f…
ysyneu Aug 6, 2026
0389c66
feat(cli): announce the default compact projection on stderr
ysyneu Aug 6, 2026
9b1bebf
docs(skills): carve alerts out of the selectable-fields note
ysyneu Aug 6, 2026
99dc3dc
fix(cli): state the 100-row cap on insight incidents --limit too
ysyneu Aug 6, 2026
445bd12
test(cli): assert toon default projection against stdout only; alert …
ysyneu Aug 6, 2026
5316c25
Merge pull request #138 from flashcatcloud/fix/limit-help-caps
ysyneu Aug 6, 2026
65a9731
Merge pull request #139 from flashcatcloud/fix/list-projection-notice
ysyneu Aug 6, 2026
8cd3090
Merge pull request #134 from flashcatcloud/docs/skill-backup-before-m…
ysyneu Aug 7, 2026
1deaf6d
fix(cli): never truncate incident detail --fields values
ysyneu Aug 11, 2026
0ab45b2
Merge pull request #141 from flashcatcloud/fix/detail-projection-neve…
ysyneu Aug 11, 2026
3d2e3ca
Merge feat/ai-sre into main — unify the two lineages
ysyneu Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/cli/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func newAlertListCmd() *cobra.Command {
cmd.Flags().BoolVar(&muted, "muted", false, "Show ever-muted only")
cmd.Flags().StringVar(&since, "since", "24h", "Start time")
cmd.Flags().StringVar(&until, "until", "now", "End time")
cmd.Flags().IntVar(&limit, "limit", 20, "Max results")
cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)")
cmd.Flags().IntVar(&page, "page", 1, "Page number")
cmd.Flags().StringVar(&fields, "fields", "", "Comma-separated fields to project in json/toon output (e.g. alert_id,title,alert_severity,created_at); ignored in table mode. Use to avoid dumping the full nested record.")

Expand Down
4 changes: 3 additions & 1 deletion internal/cli/alert_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func newAlertEventListCmd() *cobra.Command {
fieldNames := []string{"event_id", "alert_id", "event_severity", "event_status", "event_time", "title"}
if fields != "" {
fieldNames = parseStringSlice(fields)
} else {
noteDefaultProjection(cmd.ErrOrStderr(), fieldNames)
}
proj, err := projectFields(result.Items, fieldNames)
if err != nil {
Expand All @@ -113,7 +115,7 @@ func newAlertEventListCmd() *cobra.Command {
cmd.Flags().StringVar(&integrationType, "integration-type", "", "Comma-separated integration types (plugin keys, e.g. AliCloud,Prometheus) — not integration IDs; use --integration for that")
cmd.Flags().StringVar(&since, "since", "1h", "Start time")
cmd.Flags().StringVar(&until, "until", "now", "End time")
cmd.Flags().IntVar(&limit, "limit", 20, "Max results")
cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)")
cmd.Flags().IntVar(&page, "page", 1, "Page number")
cmd.Flags().StringVar(&fields, "fields", "", "Comma-separated fields to project in json/toon output (e.g. event_id,alert_id,event_severity,event_status,event_time,title); ignored in table mode. Defaults to these compact event fields. Long strings are truncated as needed to keep structured output below 16 KiB.")

Expand Down
22 changes: 22 additions & 0 deletions internal/cli/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,28 @@ func execCommand(args ...string) (string, error) {
return buf.String(), err
}

// execCommandSplit is execCommand with stdout and stderr captured separately,
// for tests that assert machine-readable stdout stays pure while advisory
// notices (e.g. the default-projection note) land on stderr.
func execCommandSplit(args ...string) (stdout, stderr string, err error) {
resetCommandFlags(rootCmd)

outBuf := new(bytes.Buffer)
errBuf := new(bytes.Buffer)
rootCmd.SetOut(outBuf)
rootCmd.SetErr(errBuf)
rootCmd.SetArgs(args)

err = rootCmd.Execute()

rootCmd.SetArgs(nil)
rootCmd.SetOut(nil)
rootCmd.SetErr(nil)
resetCommandFlags(rootCmd)

return outBuf.String(), errBuf.String(), err
}

func resetCommandFlags(cmd *cobra.Command) {
if cmd == nil {
return
Expand Down
11 changes: 11 additions & 0 deletions internal/cli/fieldproject.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"fmt"
"io"
"reflect"
"sort"
"strings"
Expand Down Expand Up @@ -69,6 +70,16 @@ func projectFields(items any, fields []string) ([]map[string]any, error) {
return out, nil
}

// noteDefaultProjection announces on stderr that structured rows were reduced
// to the command's compact default projection. Without it, a reader piping
// stdout to jq sees an unselected key (labels, description, …) as null on
// every row and can conclude the server never returns it, when it is one
// --fields away. stderr keeps stdout byte-identical for jq/toon pipelines.
func noteDefaultProjection(w io.Writer, fields []string) {
_, _ = fmt.Fprintf(w, "note: rows projected to default compact fields (%s); other response fields are available via --fields\n",
strings.Join(fields, ","))
}

// boundProjectedOutput keeps the new agent-oriented projections below their
// command budget without changing the selected keys. List rows (many small
// records) are shortened fairly when they overflow the budget, with
Expand Down
30 changes: 22 additions & 8 deletions internal/cli/fieldproject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,29 +213,37 @@ func TestIncidentListStructuredDefaultUsesCompactProjection(t *testing.T) {
stub := newGFStub(t)
stub.data = map[string]any{"items": []any{incidentRow()}, "total": 1}

out, err := execCommand("incident", "list", "--output-format", "json")
out, stderrText, err := execCommandSplit("incident", "list", "--output-format", "json")
if err != nil {
t.Fatalf("execCommand: %v", err)
t.Fatalf("execCommandSplit: %v", err)
}

assertProjectedJSONFields(t, out, []string{"incident_id", "title", "incident_severity", "progress", "start_time", "channel_id"})
if !strings.Contains(stderrText, "note: rows projected to default compact fields") {
t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText)
}
})

t.Run("toon default", func(t *testing.T) {
saveAndResetGlobals(t)
stub := newGFStub(t)
stub.data = map[string]any{"items": []any{incidentRow()}, "total": 1}

out, err := execCommand("incident", "list", "--output-format", "toon")
out, stderrText, err := execCommandSplit("incident", "list", "--output-format", "toon")
if err != nil {
t.Fatalf("execCommand: %v", err)
t.Fatalf("execCommandSplit: %v", err)
}

// Positive keys must come from stdout alone: the stderr note embeds the
// same field names, so a merged capture would satisfy this vacuously.
for _, key := range []string{"incident_id", "title", "incident_severity", "progress", "start_time", "channel_id"} {
if !strings.Contains(out, key) {
t.Errorf("default toon output missing compact key %q, got:\n%s", key, out)
}
}
if !strings.Contains(stderrText, "note: rows projected to default compact fields") {
t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText)
}
for _, key := range []string{"responders", "labels", "description"} {
if strings.Contains(out, key) {
t.Errorf("default toon output should not contain full-record key %q, got:\n%s", key, out)
Expand Down Expand Up @@ -489,13 +497,16 @@ func TestIncidentSimilarStructuredProjection(t *testing.T) {
}
stub.data = map[string]any{"items": items, "total": len(items)}

out, err := execCommand("incident", "similar", "inc-1", "--limit", "20", "--output-format", "json")
out, stderrText, err := execCommandSplit("incident", "similar", "inc-1", "--limit", "20", "--output-format", "json")
if err != nil {
t.Fatalf("execCommand: %v", err)
t.Fatalf("execCommandSplit: %v", err)
}
if len(out) >= 16*1024 {
t.Fatalf("compact similar output is %d bytes, want <16 KiB", len(out))
}
if !strings.Contains(stderrText, "note: rows projected to default compact fields") {
t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText)
}

var rows []map[string]json.RawMessage
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &rows); err != nil {
Expand Down Expand Up @@ -614,13 +625,16 @@ func TestAlertEventListStructuredProjection(t *testing.T) {
}
stub.data = map[string]any{"items": items, "total": len(items)}

out, err := execCommand("alert-event", "list", "--limit", "30", "--output-format", "json")
out, stderrText, err := execCommandSplit("alert-event", "list", "--limit", "30", "--output-format", "json")
if err != nil {
t.Fatalf("execCommand: %v", err)
t.Fatalf("execCommandSplit: %v", err)
}
if len(out) >= 16*1024 {
t.Fatalf("compact alert-event output is %d bytes, want <16 KiB", len(out))
}
if !strings.Contains(stderrText, "note: rows projected to default compact fields") {
t.Errorf("default projection should announce itself on stderr, got:\n%s", stderrText)
}
var rows []map[string]json.RawMessage
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &rows); err != nil {
t.Fatalf("parse compact alert-event json: %v\n%s", err, out)
Expand Down
4 changes: 4 additions & 0 deletions internal/cli/incident.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ func newIncidentListCmd() *cobra.Command {
if len(selectedFields) == 0 {
return fmt.Errorf("--fields must name at least one field")
}
} else {
noteDefaultProjection(cmd.ErrOrStderr(), selectedFields)
}
proj, err := projectFields(result.Items, selectedFields)
if err != nil {
Expand Down Expand Up @@ -605,6 +607,8 @@ func newIncidentSimilarCmd() *cobra.Command {
fieldNames := []string{"incident_id", "title", "incident_severity", "progress", "start_time", "close_time", "ack_time", "alert_cnt", "root_cause", "score"}
if fields != "" {
fieldNames = parseStringSlice(fields)
} else {
noteDefaultProjection(cmd.ErrOrStderr(), fieldNames)
}
proj, err := projectFields(result.Items, fieldNames)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/insight.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func newInsightIncidentsCmd() *cobra.Command {

cmd.Flags().StringVar(&since, "since", "7d", "Start time")
cmd.Flags().StringVar(&until, "until", "now", "End time")
cmd.Flags().IntVar(&limit, "limit", 20, "Max results")
cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 100)")
cmd.Flags().IntVar(&page, "page", 1, "Page number")

return cmd
Expand Down
27 changes: 21 additions & 6 deletions internal/cli/zz_generated_alerts.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading