From da0e4f5b747c97d2e326cd916a1390bf8dce4272 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 27 Jul 2026 03:36:07 -0700 Subject: [PATCH 1/7] feat(skilldoc): auto-inject response-shape summary into command cards Every generated card fence now carries a one-line response-shape summary per verb (top-level object vs. bare array vs. {items:[...]} page wrapper, plus the field names at that level), extracted straight from the "Response fields (...):" block cligen already writes into each command's Long. That block was previously only reachable via `--help`; an agent that reads a card alone had no way to learn the real shape or field names and would guess one, getting a silent null back on a miss instead of an error (e.g. guessing `is_enabled` on automation rules, which returned null for every rule/channel because the real field is `enabled`). responseShapeLine in internal/skilldoc/generate.go recognizes cligen's three canonical header phrasings and reads field names from the correct indent depth for each (row fields nested one level under the sole "items" key for the page-wrapper shape; top-level fields directly otherwise). It does not invent a fourth shape or re-derive anything cligen didn't already say. Regenerated every card via `skilldoc gen` except incident.md, which another branch is actively editing (--comment -> --comment-file) and must not be touched here. --- internal/cmd/skilldoc/main_test.go | 96 ++++++++++++++++ internal/skilldoc/generate.go | 100 +++++++++++++++- internal/skilldoc/response_shape_test.go | 139 +++++++++++++++++++++++ 3 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 internal/skilldoc/response_shape_test.go diff --git a/internal/cmd/skilldoc/main_test.go b/internal/cmd/skilldoc/main_test.go index 1722aed..23dcf5b 100644 --- a/internal/cmd/skilldoc/main_test.go +++ b/internal/cmd/skilldoc/main_test.go @@ -158,6 +158,102 @@ func TestRunGenAll_FillsEveryCardAndSkipsCardless(t *testing.T) { } } +// sectionFor returns the slice of a generated fence from "### " up to +// the next "### " heading (or end of string). Local copy of the helper +// skilldoc's own tests use — kept private to each package since exporting it +// would blur GenerateFence's real API surface just for test convenience. +func sectionFor(fence, verb string) string { + start := strings.Index(fence, "### "+verb) + if start < 0 { + return "" + } + rest := fence[start+len("### "+verb):] + if next := strings.Index(rest, "\n### "); next >= 0 { + return fence[start : start+len("### "+verb)+next] + } + return fence[start:] +} + +// TestGenerateFence_ScheduleList_ResponseShapeMatchesRealLong is the +// ground-truth cross-check the response-shape feature exists for. Before this +// change, skills/flashduty/reference/schedule.md carried zero envelope +// guidance for any of its verbs (unlike incident.md/change.md/automation.md/ +// enrichment.md/monit.md, the only 5 hand-written cards that happened to +// note their envelope shape). `schedule list` in particular is real, +// commonly-invoked, and — per the actual live CLI tree, not a fixture — +// documents an `{items: [...]}` page wrapper, NOT a bare top-level array +// (that phrasing belongs to the deprecated `oncall schedule list` twin, which +// calls the same SDK method but is a different command path with no card). +// +// The expected shape/fields below are derived by independently re-scanning +// that real Long text with throwaway logic — not by calling skilldoc's own +// extractor and not by pasting a literal expected string — so this fails if +// the generator's extraction ever silently drifts from what cligen actually +// wrote for this command. +func TestGenerateFence_ScheduleList_ResponseShapeMatchesRealLong(t *testing.T) { + d := dump() + + var long string + found := false + for _, c := range d.Commands { + if c.Path == "schedule list" { + long, found = c.Long, true + break + } + } + if !found { + t.Fatal("schedule list not found in the real CLI dump — has it been renamed?") + } + + lines := strings.Split(long, "\n") + headerLine := -1 + for i, l := range lines { + if strings.HasPrefix(l, "Response fields (") { + headerLine = i + break + } + } + if headerLine < 0 { + t.Fatal("schedule list's real Long carries no Response fields block — has cligen's output changed?") + } + if !strings.Contains(lines[headerLine], "nested under items[]") { + t.Fatalf("expected schedule list to be an items[]-wrapped page response; real header was:\n%s", lines[headerLine]) + } + var wantFields []string + for _, l := range lines[headerLine+1:] { + if strings.TrimSpace(l) == "" { + break + } + if strings.HasPrefix(l, " - ") { // one level under the sole top-level "items" row + name := strings.TrimPrefix(l, " - ") + if sp := strings.IndexAny(name, " ("); sp >= 0 { + name = name[:sp] + } + wantFields = append(wantFields, name) + } + } + if len(wantFields) == 0 { + t.Fatal("independent scan of the real Long found no row fields under items — test logic is broken") + } + + fresh := skilldoc.GenerateFence(d, "schedule") + listSection := sectionFor(fresh, "list") + if listSection == "" { + t.Fatal("generated schedule fence has no `list` section") + } + if !strings.Contains(listSection, "page wrapper") || !strings.Contains(listSection, "jq '.items[]'") { + t.Errorf("schedule list card section must document the items[] page wrapper, got:\n%s", listSection) + } + if strings.Contains(listSection, "TOP-LEVEL array") { + t.Errorf("schedule list is NOT a top-level array — must not carry that phrasing:\n%s", listSection) + } + for _, f := range wantFields { + if !strings.Contains(listSection, f+" (") { + t.Errorf("schedule list card section missing real row field %q (from live Long):\n%s", f, listSection) + } + } +} + func TestRunGen_FillsFence(t *testing.T) { dir := t.TempDir() d := fixtureDump() diff --git a/internal/skilldoc/generate.go b/internal/skilldoc/generate.go index dfeb6a6..0d75678 100644 --- a/internal/skilldoc/generate.go +++ b/internal/skilldoc/generate.go @@ -17,9 +17,14 @@ const ( // GenerateFence renders the factual fenced block for one command group: a // section per leaf verb with its short description and a flag table (name, // type, required, usage + enum), plus a body-only (--data) note when the -// command has nested JSON-only fields. Required-ness and enums are sourced from -// the authoritative "Request fields:" text in each command's Long; the flag -// list falls back to the dump's Flags when no such block exists (read-only +// command has nested JSON-only fields, plus a one-line response-shape summary +// (top-level object vs. bare array vs. `{items: [...]}` page wrapper, and the +// field names at that level) when the command documents one. Required-ness +// and enums are sourced from the authoritative "Request fields:" text in each +// command's Long; the response shape is likewise sourced from that same +// Long's "Response fields (...):" block (cligen's own ground truth — see +// responseShapeLine), not re-derived or hand-curated. The flag list falls +// back to the dump's Flags when no Request-fields block exists (read-only // verbs). Output is deterministic. func GenerateFence(d Dump, group string) string { cmds := groupCommands(d, group) @@ -84,6 +89,9 @@ func writeCommand(b *strings.Builder, c Command) { if len(fields.bodyOnly) > 0 { fmt.Fprintf(b, "- body-only (`--data`): %s\n", strings.Join(fields.bodyOnly, "; ")) } + if shape := responseShapeLine(c.Long); shape != "" { + b.WriteString(shape) + } } // positionalsOf returns the placeholder tokens after the leaf verb in a Use @@ -295,3 +303,89 @@ func cleanUsage(tail string) string { s = strings.TrimPrefix(s, "—") return strings.TrimSpace(s) } + +// --- Long "Response fields:" parser ----------------------------------------- +// +// cligen classifies every documented response into exactly one of three +// envelope shapes and says so verbatim in the "Response fields (...):" header +// it writes into Long — this parser only ever recognizes those three; it does +// not infer a shape of its own. That header (and the field list under it) is +// authoritative today but surfaces only via `--help`, which an agent that +// reads just the card fence never invokes. responseShapeLine folds a +// one-line summary of it into the fence so every generated verb — not only +// the handful some earlier hand-written card happened to cover — tells the +// agent up front whether `--json` is a bare array, a single object, or a +// `{items: [...]}` page wrapper, and exactly which field names exist at that +// level. Guessing a field name silently returns null instead of an error, so +// this is the difference between an agent noticing its own mistake and not. + +// responseHeaderRe matches the header line, capturing the parenthetical shape +// description cligen wrote (verbatim, no leading indent — it starts a new +// paragraph in Long). +var responseHeaderRe = regexp.MustCompile(`^Response fields \((.*)\):$`) + +// responseFieldRe matches one Response-fields bullet row at any indent depth, +// e.g. " - account_id (integer) (required) — ..." or, one level deeper, +// " - person_ids (array) ...". Capture groups: indent, name, type. +var responseFieldRe = regexp.MustCompile(`^( *)- ([a-zA-Z0-9_]+) \(([^)]*)\)`) + +// responseShapeLine renders the one-line response-shape summary for a +// command's Long, or "" when Long documents no Response fields block (mutation +// verbs with an empty body, and a few hand-written commands that predate +// cligen). The three shapes cligen's header can name: +// +// - top-level object: the block's own fields are the response. +// - top-level array: `--json` is a bare array of these row objects — pipe +// `jq '.[]'`, never `.items[]`. +// - `{items: [...]}` page wrapper: the block's sole top-level field is +// `items`; the row fields are nested one level (2 spaces) deeper under it. +// +// Field names (with their documented type) are read from whichever indent +// depth holds the actual row/object fields for the detected shape, so the +// summary always names the fields an agent would pipe `jq` at — not the +// wrapper key. +func responseShapeLine(long string) string { + lines := strings.Split(long, "\n") + headerIdx, header := -1, "" + for i, line := range lines { + if m := responseHeaderRe.FindStringSubmatch(line); m != nil { + headerIdx, header = i, m[1] + break + } + } + if headerIdx < 0 { + return "" + } + + wrapped := strings.Contains(header, "nested under items[]") + fieldIndent := " " + if wrapped { + fieldIndent = " " // one level under the sole top-level "items" row + } + + var fields []string + for _, line := range lines[headerIdx+1:] { + if strings.TrimSpace(line) == "" { + break + } + m := responseFieldRe.FindStringSubmatch(line) + if m == nil || m[1] != fieldIndent { + continue + } + fields = append(fields, m[2]+" ("+m[3]+")") + } + if len(fields) == 0 { + return "" + } + + var shape string + switch { + case wrapped: + shape = "`{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`)" + case strings.Contains(header, "TOP-LEVEL array"): + shape = "TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`)" + default: + shape = "single object (`data` unwrapped to the top level)" + } + return fmt.Sprintf("- response: %s — fields: %s\n", shape, strings.Join(fields, "; ")) +} diff --git a/internal/skilldoc/response_shape_test.go b/internal/skilldoc/response_shape_test.go new file mode 100644 index 0000000..92c79fe --- /dev/null +++ b/internal/skilldoc/response_shape_test.go @@ -0,0 +1,139 @@ +package skilldoc + +import ( + "strings" + "testing" +) + +// The three Long fixtures below reproduce cligen's exact header phrasing for +// each of the three envelope shapes it can document (verified against the +// real generated output in internal/cli/zz_generated_*.go and +// zz_generated_response_help.go — see internal/skilldoc/generate.go's +// responseShapeLine doc comment). Only the header wording is load-bearing; +// the field content below it is a synthetic fixture, same convention as +// generatorDump's Request-fields block. + +const objectShapeLong = `Get schedule info. + +Request fields: + --schedule-id int (required) — Schedule ID. + +Response fields ('data' envelope is unwrapped — these fields are at the top level): + - schedule_id (integer) (required) — Schedule ID. + - name (string) — Schedule display name. + - cur_oncall (object) — Current on-call group, or null when nobody is on-call. + - group_name (string) — Group display name. +` + +const topLevelArrayShapeLong = `List agents. + +Response fields (this command's ` + "`--json`" + ` is a TOP-LEVEL array of these row objects — pipe ` + "`jq '.[]'`" + `, NOT ` + "`.items[]`" + `): + - agent_id (string) (required) — Unique agent ID. + - status (string) — Agent status. [enabled, disabled] +` + +const itemsWrappedShapeLong = `List schedules. + +Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): + - items (array) (required) — Schedules on this page. + - schedule_id (integer) (required) — Schedule ID. + - name (string) — Schedule display name. + - cur_oncall (object) — Current on-call group, or null when nobody is on-call. + - group_name (string) — Group display name. +` + +const noResponseBlockLong = `Delete schedules. + +Request fields: + --schedule-id int (required) — Schedule ID. +` + +func TestResponseShapeLine_Object(t *testing.T) { + got := responseShapeLine(objectShapeLong) + if !strings.Contains(got, "single object") { + t.Errorf("object shape not detected:\n%s", got) + } + if strings.Contains(got, "TOP-LEVEL array") || strings.Contains(got, "items") { + t.Errorf("object shape must not mention array/items wrapper phrasing:\n%s", got) + } + // Only the TOP-LEVEL fields (2-space indent) are named — not the nested + // cur_oncall.group_name one level deeper. + for _, want := range []string{"schedule_id (integer)", "name (string)", "cur_oncall (object)"} { + if !strings.Contains(got, want) { + t.Errorf("missing top-level field %q:\n%s", want, got) + } + } + if strings.Contains(got, "group_name") { + t.Errorf("nested field group_name must NOT appear at the object's own level:\n%s", got) + } +} + +func TestResponseShapeLine_TopLevelArray(t *testing.T) { + got := responseShapeLine(topLevelArrayShapeLong) + if !strings.Contains(got, "TOP-LEVEL array") { + t.Errorf("array shape not detected:\n%s", got) + } + if !strings.Contains(got, "jq '.[]'") || !strings.Contains(got, "NOT `.items[]`") { + t.Errorf("array shape must warn against `.items[]`:\n%s", got) + } + for _, want := range []string{"agent_id (string)", "status (string)"} { + if !strings.Contains(got, want) { + t.Errorf("missing row field %q:\n%s", want, got) + } + } +} + +func TestResponseShapeLine_ItemsWrapped(t *testing.T) { + got := responseShapeLine(itemsWrappedShapeLong) + if !strings.Contains(got, "items: [...]") || !strings.Contains(got, "page wrapper") { + t.Errorf("items[] wrapper shape not detected:\n%s", got) + } + if !strings.Contains(got, "jq '.items[]'") { + t.Errorf("items[] shape must point at `.items[]`, not top-level `.[]`:\n%s", got) + } + // The row fields are the ones nested UNDER items (4-space indent), not the + // bare "items" wrapper key itself. + for _, want := range []string{"schedule_id (integer)", "name (string)", "cur_oncall (object)"} { + if !strings.Contains(got, want) { + t.Errorf("missing row field %q:\n%s", want, got) + } + } + if strings.Contains(got, "- fields: items") || strings.HasPrefix(strings.TrimSpace(strings.SplitN(got, "fields:", 2)[1]), "items (") { + t.Errorf("wrapper key `items` itself must not be listed as a row field:\n%s", got) + } + if strings.Contains(got, "group_name") { + t.Errorf("field nested two levels deep (cur_oncall.group_name) must NOT appear:\n%s", got) + } +} + +func TestResponseShapeLine_NoBlockIsEmpty(t *testing.T) { + if got := responseShapeLine(noResponseBlockLong); got != "" { + t.Errorf("command with no Response fields block must yield no response line, got:\n%s", got) + } +} + +// TestGenerateFence_InjectsResponseShapePerVerb is the fence-level integration +// check: a group with one verb of each shape must surface a "- response: " +// line in that verb's own section, and a verb with no documented response +// must surface none (not a blank placeholder line). +func TestGenerateFence_InjectsResponseShapePerVerb(t *testing.T) { + d := Dump{Commands: []Command{ + {Path: "widget info", Group: "widget", Short: "Get widget", Use: "info", Long: objectShapeLong}, + {Path: "widget list", Group: "widget", Short: "List widgets", Use: "list", Long: itemsWrappedShapeLong}, + {Path: "widget delete", Group: "widget", Short: "Delete widget", Use: "delete", Long: noResponseBlockLong}, + }} + out := GenerateFence(d, "widget") + + infoSec := sectionFor(out, "info") + if !strings.Contains(infoSec, "- response: single object") { + t.Errorf("info section missing object response line:\n%s", infoSec) + } + listSec := sectionFor(out, "list") + if !strings.Contains(listSec, "- response:") || !strings.Contains(listSec, "page wrapper") { + t.Errorf("list section missing items[] response line:\n%s", listSec) + } + deleteSec := sectionFor(out, "delete") + if strings.Contains(deleteSec, "- response:") { + t.Errorf("delete section must not fabricate a response line when Long documents none:\n%s", deleteSec) + } +} From f53c05fe2cc40b6b6b48c767113dcdb144cc243e Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 27 Jul 2026 03:36:19 -0700 Subject: [PATCH 2/7] docs(skilldoc): regenerate card fences with response-shape summaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical `skilldoc gen` regeneration only — picks up the response-shape line every verb's fence now carries. No hand-written content changed. incident.md is deliberately excluded (owned by another in-flight branch). --- skills/flashduty/reference/alert.md | 10 +++++++++ skills/flashduty/reference/automation.md | 6 +++++ skills/flashduty/reference/calendar.md | 5 +++++ skills/flashduty/reference/change.md | 1 + skills/flashduty/reference/channel.md | 14 ++++++++++++ skills/flashduty/reference/enrichment.md | 10 +++++++++ skills/flashduty/reference/field.md | 3 +++ skills/flashduty/reference/member.md | 3 +++ skills/flashduty/reference/monit-agent.md | 2 ++ skills/flashduty/reference/monit-query.md | 2 ++ skills/flashduty/reference/monit.md | 27 +++++++++++++++++++++++ skills/flashduty/reference/role.md | 5 +++++ skills/flashduty/reference/route.md | 2 ++ skills/flashduty/reference/rum.md | 12 ++++++++++ skills/flashduty/reference/safari.md | 20 +++++++++++++++++ skills/flashduty/reference/sourcemap.md | 2 ++ skills/flashduty/reference/status-page.md | 14 ++++++++++++ skills/flashduty/reference/team.md | 5 +++++ skills/flashduty/reference/template.md | 6 +++++ 19 files changed, 149 insertions(+) diff --git a/skills/flashduty/reference/alert.md b/skills/flashduty/reference/alert.md index 5d0f3de..db6afe1 100644 --- a/skills/flashduty/reference/alert.md +++ b/skills/flashduty/reference/alert.md @@ -60,9 +60,11 @@ List events for an alert - `--limit` int64 — Page size. Defaults to 20 and cannot exceed 100. (0-100) - `--page` int64 — Page number starting at 1. Used when 'search_after_ctx' is omitted. (min 0) - `--search-after-ctx` string — Cursor returned by the previous page. When supplied, cursor pagination is used instead of page-number pagination. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); alert_id (string); alert_key (string); channel_id (integer); created_at (integer); data_source_id (integer); deleted_at (integer); description (string); event_id (string); event_severity (string); event_status (string); event_time (integer); images (array); integration_id (integer); integration_type (string); labels (object); title (string); title_rule (string); updated_at (integer) ### events List alert events +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); alert_id (string); alert_key (string); channel_id (integer); created_at (integer); data_source_id (integer); deleted_at (integer); description (string); event_id (string); event_severity (string); event_status (string); event_time (integer); images (array); integration_id (integer); integration_type (string); labels (object); title (string); title_rule (string); updated_at (integer) ### feed List alert activity feed @@ -72,13 +74,16 @@ List alert activity feed - `--page` int64 — Page number, starting at 1. - `--search-after-ctx` string - `--types` stringSlice — Filter by feed types. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); created_at (integer); creator_id (integer); detail (object); ref_id (string); type (string); updated_at (integer) ### get Get alert detail +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); alert_id (string); alert_key (string); alert_severity (string); alert_status (string); channel_id (integer); channel_name (string); channel_status (string); created_at (integer); data_source_id (integer); data_source_name (string); data_source_ref_id (string); data_source_type (string); description (string); end_time (integer); event_cnt (integer); events (array); ever_muted (boolean); images (array); incident (object); integration_id (integer); integration_name (string); integration_ref_id (string); integration_type (string); labels (object); last_time (integer); responder_email (string); responder_name (string); start_time (integer); title (string); title_rule (string); updated_at (integer) ### info Get alert detail - `` (positional, required) string — Alert ID (ObjectID hex string). +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); alert_id (string); alert_key (string); alert_severity (string); alert_status (string); channel_id (integer); channel_name (string); channel_status (string); created_at (integer); data_source_id (integer); data_source_name (string); data_source_ref_id (string); data_source_type (string); description (string); end_time (integer); event_cnt (integer); events (array); ever_muted (boolean); images (array); incident (object); integration_id (integer); integration_name (string); integration_ref_id (string); integration_type (string); labels (object); last_time (integer); responder_email (string); responder_name (string); start_time (integer); title (string); title_rule (string); updated_at (integer) ### list List alerts @@ -92,10 +97,12 @@ List alerts - `--severity` string - `--since` string - `--until` string +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); alert_id (string); alert_key (string); alert_severity (string); alert_status (string); channel_id (integer); channel_name (string); channel_status (string); created_at (integer); data_source_id (integer); data_source_name (string); data_source_ref_id (string); data_source_type (string); description (string); end_time (integer); event_cnt (integer); events (array); ever_muted (boolean); images (array); incident (object); integration_id (integer); integration_name (string); integration_ref_id (string); integration_type (string); labels (object); last_time (integer); responder_email (string); responder_name (string); start_time (integer); title (string); title_rule (string); updated_at (integer) ### list-by-ids [...] List alerts by IDs - `` (positional, required) stringSlice — List of alert IDs (ObjectID hex strings). +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); alert_id (string); alert_key (string); alert_severity (string); alert_status (string); channel_id (integer); channel_name (string); channel_status (string); created_at (integer); data_source_id (integer); data_source_name (string); data_source_ref_id (string); data_source_type (string); description (string); end_time (integer); event_cnt (integer); events (array); ever_muted (boolean); images (array); incident (object); integration_id (integer); integration_name (string); integration_ref_id (string); integration_type (string); labels (object); last_time (integer); responder_email (string); responder_name (string); start_time (integer); title (string); title_rule (string); updated_at (integer) ### merge [...] Merge alerts into an incident @@ -108,10 +115,12 @@ Merge alerts into an incident ### pipeline-info Get alert pipeline - `` (positional, required) int64 — Integration ID. +- response: single object (`data` unwrapped to the top level) — fields: created_at (integer); creator_id (integer); integration_id (integer); rules (array); status (string); updated_at (integer); updated_by (integer) ### pipeline-list [...] List alert pipelines - `` (positional, required) intSlice — Integration IDs. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: created_at (integer); creator_id (integer); integration_id (integer); rules (array); status (string); updated_at (integer); updated_by (integer) ### pipeline-upsert Create or update alert pipeline @@ -122,6 +131,7 @@ Create or update alert pipeline View alert timeline - `--limit` int - `--page` int +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); created_at (integer); creator_id (integer); detail (object); ref_id (string); type (string); updated_at (integer) diff --git a/skills/flashduty/reference/automation.md b/skills/flashduty/reference/automation.md index f0a5f4f..b576c81 100644 --- a/skills/flashduty/reference/automation.md +++ b/skills/flashduty/reference/automation.md @@ -119,6 +119,7 @@ Create an Automation - `--schedule-enabled` bool - `--team-id` int64 - `--weekday` string +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); can_edit (boolean); created_at (integer); cron_expr (string); enabled (boolean); environment_id (string); environment_kind (string); http_post_token (string); http_post_trigger_enabled (boolean); http_post_trigger_id (string); http_post_trigger_url (string); name (string); oncall_incident_channel_ids (array); oncall_incident_severities (array); oncall_incident_trigger_enabled (boolean); oncall_incident_trigger_id (string); owner_id (integer); prompt (string); rule_id (string); run_scope (string); schedule_next_fire_at_ms (integer); schedule_trigger_enabled (boolean); schedule_trigger_id (string); team_id (integer); timezone (string); updated_at (integer) ### delete Delete an Automation @@ -131,6 +132,7 @@ Fire an Automation HTTP POST trigger ### get Get an Automation +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); can_edit (boolean); created_at (integer); cron_expr (string); enabled (boolean); environment_id (string); environment_kind (string); http_post_token (string); http_post_trigger_enabled (boolean); http_post_trigger_id (string); http_post_trigger_url (string); name (string); oncall_incident_channel_ids (array); oncall_incident_severities (array); oncall_incident_trigger_enabled (boolean); oncall_incident_trigger_id (string); owner_id (integer); prompt (string); rule_id (string); run_scope (string); schedule_next_fire_at_ms (integer); schedule_trigger_enabled (boolean); schedule_trigger_id (string); team_id (integer); timezone (string); updated_at (integer) ### list List visible Automations @@ -140,6 +142,7 @@ List visible Automations - `--page` int - `--scope` string - `--team-ids` int64Slice +- response: single object (`data` unwrapped to the top level) — fields: rules (array); total (integer) ### runs List Automation runs @@ -149,10 +152,12 @@ List Automation runs - `--status` string - `--trigger-kind` string - `--until` string +- response: single object (`data` unwrapped to the top level) — fields: runs (array); total (integer) ### templates List Automation templates - `--locale` string +- response: single object (`data` unwrapped to the top level) — fields: templates (array) ### update Update an Automation @@ -172,6 +177,7 @@ Update an Automation - `--rotate-http-post-token` bool - `--schedule` string - `--weekday` string +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); can_edit (boolean); created_at (integer); cron_expr (string); enabled (boolean); environment_id (string); environment_kind (string); http_post_token (string); http_post_trigger_enabled (boolean); http_post_trigger_id (string); http_post_trigger_url (string); name (string); oncall_incident_channel_ids (array); oncall_incident_severities (array); oncall_incident_trigger_enabled (boolean); oncall_incident_trigger_id (string); owner_id (integer); prompt (string); rule_id (string); run_scope (string); schedule_next_fire_at_ms (integer); schedule_trigger_enabled (boolean); schedule_trigger_id (string); team_id (integer); timezone (string); updated_at (integer) diff --git a/skills/flashduty/reference/calendar.md b/skills/flashduty/reference/calendar.md index 1492516..c411d78 100644 --- a/skills/flashduty/reference/calendar.md +++ b/skills/flashduty/reference/calendar.md @@ -66,6 +66,7 @@ Create calendar - `--team-id` int64 — Owning team ID. 0 means no team. - `--timezone` string — IANA timezone. Defaults to Asia/Shanghai when empty. - `--workdays` intSlice — Workday numbers (0 = Sunday, 6 = Saturday). +- response: single object (`data` unwrapped to the top level) — fields: cal_id (string); cal_name (string) ### delete Delete calendar @@ -82,6 +83,7 @@ List calendar events - `--day` int64 — Day (1-31). 0 means no day filter. (0-31) - `--month` int64 — Month (1-12). 0 means no month filter. (0-12) - `--year` int64 — Year. Defaults to the current year when omitted. (min 2023) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); cal_id (string); created_at (integer); creator_id (integer); description (string); end_at (string); event_id (string); is_off (boolean); start_at (string); summary (string); updated_at (integer) ### event-upsert Upsert calendar event @@ -92,15 +94,18 @@ Upsert calendar event - `--is-off` bool (required) — Whether the event marks a non-working day. true = day off, false = working day override. - `--start-at` string (required) — Event start date in YYYY-MM-DD. - `--summary` string (required) — Event summary. (1-39 chars) +- response: single object (`data` unwrapped to the top level) — fields: cal_id (string); event_id (string); summary (string) ### info Get calendar info - `` (positional, required) string — Calendar ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); cal_id (string); cal_name (string); created_at (integer); creator_id (integer); description (string); extra_cal_ids (array); kind (string); status (string); team_id (integer); timezone (string); updated_at (integer); updated_by (integer); workdays (array) ### list List calendars - `--kind` string — Calendar kind filter. Defaults to personal when empty. · enum: region.official.holiday | personal - `--no-locale` bool — Disable locale filtering when listing public-holiday calendars. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); cal_id (string); cal_name (string); created_at (integer); creator_id (integer); description (string); extra_cal_ids (array); kind (string); status (string); team_id (integer); timezone (string); updated_at (integer); updated_by (integer); workdays (array) ### update Update calendar diff --git a/skills/flashduty/reference/change.md b/skills/flashduty/reference/change.md index 0051909..67c7d31 100644 --- a/skills/flashduty/reference/change.md +++ b/skills/flashduty/reference/change.md @@ -33,6 +33,7 @@ List changes - `--query` string - `--since` string - `--until` string +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); change_id (string); change_key (string); change_status (string); channel_id (integer); channel_name (string); channel_status (string); description (string); end_time (integer); events (array); integration_id (integer); integration_name (string); labels (object); last_time (integer); link (string); start_time (integer); title (string) diff --git a/skills/flashduty/reference/channel.md b/skills/flashduty/reference/channel.md index 138e0f2..785f9d9 100644 --- a/skills/flashduty/reference/channel.md +++ b/skills/flashduty/reference/channel.md @@ -74,6 +74,7 @@ Create channel - `--plugin-ids` intSlice — IDs of plugins (integrations) subscribed to this channel. - `--team-id` int64 (required) — Owning team ID. - body-only (`--data`): escalate_rule (object); flapping (object); group (object) +- response: single object (`data` unwrapped to the top level) — fields: channel_id (integer); channel_name (string); external_report_token (string) ### delete Delete channel @@ -96,6 +97,7 @@ Create escalation rule - `--rule-name` string (required) — Rule name, 1 to 39 characters. (1-39 chars) - `--template-id` string (required) — Notification template ID (MongoDB ObjectID). - body-only (`--data`): filters (array); layers (array) (required); time_filters (array) +- response: single object (`data` unwrapped to the top level) — fields: rule_id (string); rule_name (string) ### escalate-rule-delete Delete escalation rule @@ -116,10 +118,12 @@ Enable escalation rule Get escalation rule detail - `--channel-id` int64 (required) — Channel the rule belongs to. - `--rule-id` string (required) — Rule ID (MongoDB ObjectID). +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); aggr_window (integer); channel_id (integer); channel_name (string); created_at (integer); deleted_at (integer); description (string); filters (object); layers (array); priority (integer); rule_id (string); rule_name (string); status (string); template_id (string); time_filters (array); updated_at (integer); updated_by (integer) ### escalate-rule-list List escalation rules - `` (positional, required) int64 — Channel to list rules for. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); aggr_window (integer); channel_id (integer); channel_name (string); created_at (integer); deleted_at (integer); description (string); filters (object); layers (array); priority (integer); rule_id (string); rule_name (string); status (string); template_id (string); time_filters (array); updated_at (integer); updated_by (integer) ### escalate-rule-update Update escalation rule @@ -135,10 +139,12 @@ Update escalation rule ### info Get channel detail - `` (positional, required) int64 — Channel ID to fetch. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); active_incident_highest_severity (string); auto_resolve_mode (string); auto_resolve_timeout (integer); channel_id (integer); channel_name (string); created_at (integer); creator_id (integer); creator_name (string); deleted_at (integer); description (string); disable_auto_close (boolean); disable_outlier_detection (boolean); external_report_token (string); flapping (object); group (object); is_external_report_enabled (boolean); is_private (boolean); is_starred (boolean); last_incident_at (integer); managing_team_ids (array); progress_to_incident_cnts (object); status (string); team_id (integer); team_name (string); updated_at (integer) ### infos [...] Batch get channels - `` (positional, required) intSlice — Channel IDs to look up. Up to 1000. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: channel_id (integer); channel_name (string); status (string) ### inhibit-rule-create Create inhibit rule @@ -149,6 +155,7 @@ Create inhibit rule - `--priority` int64 — Evaluation priority. Lower runs first. - `--rule-name` string (required) — Rule name, 1 to 39 characters. (1-39 chars) - body-only (`--data`): source_filters (array); target_filters (array) +- response: single object (`data` unwrapped to the top level) — fields: rule_id (string); rule_name (string) ### inhibit-rule-delete Delete inhibit rule @@ -168,6 +175,7 @@ Enable inhibit rule ### inhibit-rule-list List inhibit rules - `` (positional, required) int64 — Channel to list rules for. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); channel_id (integer); created_at (integer); deleted_at (integer); description (string); equals (array); is_directly_discard (boolean); priority (integer); rule_id (string); rule_name (string); source_filters (object); status (string); target_filters (object); updated_at (integer); updated_by (integer) ### inhibit-rule-update Update inhibit rule @@ -184,6 +192,7 @@ Update inhibit rule List channels - `--name` string - `--team-ids` int64Slice +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); active_incident_highest_severity (string); auto_resolve_mode (string); auto_resolve_timeout (integer); channel_id (integer); channel_name (string); created_at (integer); creator_id (integer); creator_name (string); deleted_at (integer); description (string); disable_auto_close (boolean); disable_outlier_detection (boolean); external_report_token (string); flapping (object); group (object); is_external_report_enabled (boolean); is_private (boolean); is_starred (boolean); last_incident_at (integer); managing_team_ids (array); progress_to_incident_cnts (object); status (string); team_id (integer); team_name (string); updated_at (integer) ### silence-rule-create Create silence rule @@ -195,6 +204,7 @@ Create silence rule - `--priority` int64 — Evaluation priority. Lower runs first. - `--rule-name` string (required) — Rule name, 1 to 39 characters. (1-39 chars) - body-only (`--data`): filters (array); time_filter (object); time_filters (array) +- response: single object (`data` unwrapped to the top level) — fields: rule_id (string); rule_name (string) ### silence-rule-delete Delete silence rule @@ -214,6 +224,7 @@ Enable silence rule ### silence-rule-list List silence rules - `` (positional, required) int64 — Channel to list rules for. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); channel_id (integer); created_at (integer); deleted_at (integer); description (string); filters (object); from_incident_id (string); is_auto_delete (boolean); is_directly_discard (boolean); is_effective (boolean); priority (integer); rule_id (string); rule_name (string); status (string); time_filter (object); time_filters (array); updated_at (integer); updated_by (integer) ### silence-rule-update Update silence rule @@ -233,6 +244,7 @@ Create drop rule - `--priority` int64 — Evaluation priority. Lower runs first. - `--rule-name` string (required) — Rule name, 1 to 39 characters. (1-39 chars) - body-only (`--data`): filters (array) +- response: single object (`data` unwrapped to the top level) — fields: rule_id (string); rule_name (string) ### unsubscribe-rule-delete Delete drop rule @@ -252,6 +264,7 @@ Enable drop rule ### unsubscribe-rule-list List drop rules - `` (positional, required) int64 — Channel to list rules for. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); channel_id (integer); created_at (integer); deleted_at (integer); description (string); filters (object); priority (integer); rule_id (string); rule_name (string); status (string); updated_at (integer); updated_by (integer) ### unsubscribe-rule-update Update drop rule @@ -276,6 +289,7 @@ Update channel - `--managing-team-ids` intSlice — Additional teams that can manage the channel. Up to 3 entries. - `--team-id` int64 — New owning team ID. - body-only (`--data`): flapping (object); group (object) +- response: single object (`data` unwrapped to the top level) — fields: external_report_token (string) diff --git a/skills/flashduty/reference/enrichment.md b/skills/flashduty/reference/enrichment.md index 6688bcf..210ff3f 100644 --- a/skills/flashduty/reference/enrichment.md +++ b/skills/flashduty/reference/enrichment.md @@ -75,10 +75,12 @@ fduty enrichment info --output-format toon ### info Get enrichment rules - `` (positional, required) int64 — Integration ID to query enrichment rules for. Must be greater than 0. (min 1) +- response: single object (`data` unwrapped to the top level) — fields: created_at (integer); creator_id (integer); integration_id (integer); rules (array); status (string); updated_at (integer); updated_by (integer) ### list [...] List enrichment rules - `` (positional, required) intSlice — List of integration IDs to query. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: created_at (integer); creator_id (integer); integration_id (integer); rules (array); status (string); updated_at (integer); updated_by (integer) ### mapping-api-create Create mapping API @@ -90,6 +92,7 @@ Create mapping API - `--timeout` int64 — Request timeout in seconds (1–3). Default 2. - `--url` string (required) — HTTP/HTTPS endpoint URL (max 500 chars). (≤500 chars) - body-only (`--data`): headers (object) +- response: single object (`data` unwrapped to the top level) — fields: api_id (string); api_name (string) ### mapping-api-delete Delete mapping API @@ -98,9 +101,11 @@ Delete mapping API ### mapping-api-info Get mapping API detail - `` (positional, required) string — Mapping API ID (MongoDB ObjectID hex). +- response: single object (`data` unwrapped to the top level) — fields: api_id (string); api_name (string); created_at (integer); creator_id (integer); description (string); headers (object); insecure_skip_verify (boolean); retry_count (integer); status (string); team_id (integer); timeout (integer); updated_at (integer); updated_by (integer); url (string) ### mapping-api-list List mapping APIs +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: api_id (string); api_name (string); created_at (integer); creator_id (integer); description (string); headers (object); insecure_skip_verify (boolean); retry_count (integer); status (string); team_id (integer); timeout (integer); updated_at (integer); updated_by (integer); url (string) ### mapping-api-update Update mapping API @@ -132,6 +137,7 @@ List mapping data - `` (positional, required) string — Mapping schema ID (MongoDB ObjectID hex). - `--search-after-ctx` string — Opaque cursor token for cursor-based pagination. - body-only (`--data`): query (object) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: created_at (integer); fields (object); key (string); updated_at (integer) ### mapping-data-truncate Truncate mapping data @@ -146,6 +152,7 @@ Upload mapping data via CSV Upsert mapping data rows - `` (positional, required) string — Mapping schema ID (MongoDB ObjectID hex). - body-only (`--data`): docs (array) (required) +- response: single object (`data` unwrapped to the top level) — fields: keys (array) ### mapping-schema-create Create mapping schema @@ -154,6 +161,7 @@ Create mapping schema - `--schema-name` string (required) — Unique schema name (max 39 chars). (≤39 chars) - `--source-labels` stringSlice (required) — Lookup key label names (1–3). Must not overlap with 'result_labels'. - `--team-id` int64 — Owning team ID. '0' means no team. +- response: single object (`data` unwrapped to the top level) — fields: schema_id (string); schema_name (string) ### mapping-schema-delete Delete mapping schema @@ -162,9 +170,11 @@ Delete mapping schema ### mapping-schema-info Get mapping schema detail - `` (positional, required) string — Mapping schema ID (MongoDB ObjectID hex). +- response: single object (`data` unwrapped to the top level) — fields: created_at (integer); creator_id (integer); description (string); result_labels (array); schema_id (string); schema_name (string); source_labels (array); status (string); team_id (integer); updated_at (integer); updated_by (integer) ### mapping-schema-list List mapping schemas +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: created_at (integer); creator_id (integer); description (string); result_labels (array); schema_id (string); schema_name (string); source_labels (array); status (string); team_id (integer); updated_at (integer); updated_by (integer) ### mapping-schema-update Update mapping schema diff --git a/skills/flashduty/reference/field.md b/skills/flashduty/reference/field.md index 506f05f..e42b638 100644 --- a/skills/flashduty/reference/field.md +++ b/skills/flashduty/reference/field.md @@ -50,6 +50,7 @@ Create field - `--options` stringSlice — Required and non-empty for 'single_select'/'multi_select' (unique strings, each 1–200 chars). Must be omitted or empty for 'checkbox'/'text'. - `--value-type` string (required) — Stored value type. 'checkbox' requires 'bool'; 'single_select'/'multi_select'/'text' require 'string'. Immutable after creation. · enum: string | bool | float - body-only (`--data`): default_value (any) +- response: single object (`data` unwrapped to the top level) — fields: field_id (string); field_name (string) ### delete Delete field @@ -58,10 +59,12 @@ Delete field ### info Get field detail - `` (positional, required) string — Field ID — 24-character hex ObjectID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); created_at (integer); creator_id (integer); default_value (any); deleted_at (integer); description (string); display_name (string); field_id (string); field_name (string); field_type (string); options (any); status (string); updated_at (integer); updated_by (integer); value_type (string) ### list List custom fields - `--name` string +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); created_at (integer); creator_id (integer); default_value (any); deleted_at (integer); description (string); display_name (string); field_id (string); field_name (string); field_type (string); options (any); status (string); updated_at (integer); updated_by (integer); value_type (string) ### update Update field diff --git a/skills/flashduty/reference/member.md b/skills/flashduty/reference/member.md index a13347a..26b590c 100644 --- a/skills/flashduty/reference/member.md +++ b/skills/flashduty/reference/member.md @@ -65,6 +65,7 @@ Delete member ### info Get current member info +- response: single object (`data` unwrapped to the top level) — fields: account_avatar (string); account_email (string); account_id (integer); account_locale (string); account_name (string); account_role_ids (array); account_time_zone (string); avatar (string); country_code (string); domain (string); email (string); email_verified (boolean); is_external (boolean); locale (string); member_id (integer); member_name (string); phone (string); phone_verified (boolean); status (string); time_zone (string) ### info-reset Reset member info @@ -81,6 +82,7 @@ Reset member info Invite members - `--from` string — Invite source context - body-only (`--data`): members (array) (required) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: member_id (integer); member_name (string) ### list List members @@ -91,6 +93,7 @@ List members - `--query` string — Search keyword - `--role-id` int64 — Filter by role ID - `--search-after-ctx` string +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); account_role_ids (array); avatar (string); country_code (string); created_at (integer); email (string); email_verified (boolean); is_external (boolean); locale (string); member_id (integer); member_name (string); phone (string); phone_verified (boolean); ref_id (string); status (string); time_zone (string); updated_at (integer) ### role-grant [...] Grant role to member diff --git a/skills/flashduty/reference/monit-agent.md b/skills/flashduty/reference/monit-agent.md index 0d385b0..65f185d 100644 --- a/skills/flashduty/reference/monit-agent.md +++ b/skills/flashduty/reference/monit-agent.md @@ -29,11 +29,13 @@ fduty monit-agent invoke --target-locator \ List the diagnostic tools the agent exposes for a target - `--target-kind` string - `--target-locator` string +- response: single object (`data` unwrapped to the top level) — fields: error (object); target (object); tools (array) ### invoke Run up to 8 monit-agent tools concurrently on a target - `--target-kind` string - `--target-locator` string +- response: single object (`data` unwrapped to the top level) — fields: error (object); results (array); target (object) diff --git a/skills/flashduty/reference/monit-query.md b/skills/flashduty/reference/monit-query.md index 9495346..3a36e8b 100644 --- a/skills/flashduty/reference/monit-query.md +++ b/skills/flashduty/reference/monit-query.md @@ -38,6 +38,7 @@ Pre-clustered RCA findings (log_patterns or metric_trends) - `--time-end` string - `--time-start` string - `--timeout-seconds` int +- response: single object (`data` unwrapped to the top level) — fields: data_handling (object); ds_name (string); ds_type (string); operation (string); query (string); results (array); schema_version (string); window (object) ### rows Raw datasource passthrough (returns values/rows as the datasource itself would) @@ -45,6 +46,7 @@ Raw datasource passthrough (returns values/rows as the datasource itself would) - `--ds-name` string - `--ds-type` string - `--expr` string +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: fields (object); values (object) diff --git a/skills/flashduty/reference/monit.md b/skills/flashduty/reference/monit.md index c2d5f55..d2d0442 100644 --- a/skills/flashduty/reference/monit.md +++ b/skills/flashduty/reference/monit.md @@ -97,6 +97,7 @@ Create datasource - `--note` string — Optional description. - `--type-ident` string (required) — Datasource type identifier. Allowed: 'prometheus', 'loki', 'mysql', 'oracle', 'postgres', 'clickhouse', 'elasticsearch', 'sls', 'victorialogs'. - body-only (`--data`): payload (object) (required) +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); address (string); edge_cluster_name (string); enabled (boolean); id (integer); name (string); note (string); payload (object); type_ident (string); updated_at (integer) ### datasource-delete Delete datasource @@ -105,10 +106,12 @@ Delete datasource ### datasource-info Get datasource detail - `--id` int64 (required) — Resource ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); address (string); edge_cluster_name (string); enabled (boolean); id (integer); name (string); note (string); payload (object); type_ident (string); updated_at (integer) ### datasource-list List datasources - `--type` string — Filter by datasource type identifier. Omit to return all types. Allowed values: 'prometheus', 'loki', 'mysql', 'oracle', 'postgres', 'clickhouse', 'elasticsearch', 'sls', 'victorialogs'. +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); address (string); edge_cluster_name (string); enabled (boolean); id (integer); name (string); note (string); payload (object); type_ident (string); updated_at (integer) ### datasource-sls-logstores List SLS logstores @@ -133,6 +136,7 @@ Update datasource - `--note` string — Optional description. - `--type-ident` string (required) — Datasource type identifier. Allowed: 'prometheus', 'loki', 'mysql', 'oracle', 'postgres', 'clickhouse', 'elasticsearch', 'sls', 'victorialogs'. - body-only (`--data`): payload (object) (required) +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); address (string); edge_cluster_name (string); enabled (boolean); id (integer); name (string); note (string); payload (object); type_ident (string); updated_at (integer) ### preview-sync Preview datasource query @@ -149,6 +153,7 @@ Diagnose data source - `--ds-type` string (required) — Data source type. 'log_patterns' supports 'loki' and 'victorialogs'; 'metric_trends' supports 'prometheus'. - `--operation` string — Diagnostic operation. When omitted, inferred from 'ds_type' (loki / victorialogs → 'log_patterns', prometheus → 'metric_trends'). Other sources must specify explicitly. · enum: log_patterns | metric_trends - body-only (`--data`): input (object) (required); methods (array); options (object); time_range (object) +- response: single object (`data` unwrapped to the top level) — fields: data_handling (object); ds_name (string); ds_type (string); operation (string); query (string); results (array); schema_version (string); window (object) ### query-rows Query data source rows @@ -158,14 +163,17 @@ Query data source rows - `--ds-type` string (required) — Data source type; must match a configured data source under the tenant. Examples: 'prometheus', 'loki', 'victorialogs', 'sls', 'elasticsearch', 'mysql', 'postgres', 'oracle', 'clickhouse'. - `--expr` string (required) — Query expression. Syntax depends on 'ds_type' and is interpreted by the corresponding monit-edge client (PromQL for Prometheus, LogQL for Loki, SQL for SQL sources, etc.). - body-only (`--data`): args (object) +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: fields (object); values (object) ### rule-audit-detail Get rule audit snapshot - `--id` int64 (required) — Audit record ID — the 'id' of an audit row returned by 'POST /monit/rule/audits', NOT the rule ID. Passing a rule ID returns HTTP 400. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); action (string); alert_rule_id (integer); content (string); created_at (integer); creator_id (integer); creator_name (string); id (integer) ### rule-audits List rule change history - `--id` int64 (required) — Rule ID. +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); action (string); alert_rule_id (integer); content (string); created_at (integer); creator_id (integer); creator_name (string); id (integer) ### rule-counter-channel Get rule counts by channel @@ -175,9 +183,11 @@ Get rule counts by folder node ### rule-counter-status Get rule status counters for top-level folders +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: folder_id (integer); folder_name (string); rule_total (integer); triggered_rule_count (integer) ### rule-counter-total Get rule counter time series +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); clock (integer); id (integer); num (integer) ### rule-create Create alert rule @@ -204,6 +214,7 @@ Create alert rule - `--updater-id` int64 - `--updater-name` string - body-only (`--data`): annotations (object); enabled_times (array); labels (object); rule_configs (object) +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); annotations (object); channel_ids (array); created_at (integer); creator_id (integer); creator_name (string); cron_pattern (string); debug_log_enabled (boolean); delay_seconds (integer); description (string); description_type (string); ds_ids (array); ds_list (array); ds_type (string); enabled (boolean); enabled_times (array); folder_id (integer); id (integer); labels (object); name (string); repeat_interval (integer); repeat_total (integer); rule_configs (object); updated_at (integer); updater_id (integer); updater_name (string) ### rule-delete Delete alert rule @@ -215,30 +226,37 @@ Batch delete alert rules ### rule-dstypes List available datasource types +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); id (integer); ident (string); name (string); weight (integer) ### rule-export Export alert rules - `--ids` intSlice (required) — Rule IDs. +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: annotations (object); cron_pattern (string); debug_log_enabled (boolean); delay_seconds (integer); description (string); description_type (string); ds_ids (array); ds_list (array); ds_type (string); enabled (boolean); enabled_times (array); labels (object); name (string); repeat_interval (integer); repeat_total (integer); rule_configs (object) ### rule-import Import alert rules +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: message (string); name (string) ### rule-info Get alert rule detail - `--id` int64 (required) — Rule ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); annotations (object); channel_ids (array); created_at (integer); creator_id (integer); creator_name (string); cron_pattern (string); debug_log_enabled (boolean); delay_seconds (integer); description (string); description_type (string); ds_ids (array); ds_list (array); ds_type (string); enabled (boolean); enabled_times (array); folder_id (integer); id (integer); labels (object); name (string); repeat_interval (integer); repeat_total (integer); rule_configs (object); updated_at (integer); updater_id (integer); updater_name (string) ### rule-list-basic List alert rules - `--folder-id` int64 — Folder ID. 0 to list all accessible rules. +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); created_at (integer); creator_id (integer); creator_name (string); cron_pattern (string); debug_log_enabled (boolean); delay_seconds (integer); ds_type (string); enabled (boolean); folder_id (integer); id (integer); labels (object); name (string); triggered (boolean); updated_at (integer); updater_id (integer); updater_name (string) ### rule-move Move alert rules to folder - `--dest-folder-id` int64 (required) — Destination folder ID. - `--ids` intSlice (required) — Rule IDs to move. +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: message (string); name (string) ### rule-status Get rule trigger status under folder - `--folder-id` int64 — Folder ID. 0 for all. +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: folder_id (integer); folder_name (string); rule_total (integer); triggered_rule_count (integer) ### rule-update Update alert rule @@ -265,6 +283,7 @@ Update alert rule - `--updater-id` int64 - `--updater-name` string - body-only (`--data`): annotations (object); enabled_times (array); labels (object); rule_configs (object) +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); annotations (object); channel_ids (array); created_at (integer); creator_id (integer); creator_name (string); cron_pattern (string); debug_log_enabled (boolean); delay_seconds (integer); description (string); description_type (string); ds_ids (array); ds_list (array); ds_type (string); enabled (boolean); enabled_times (array); folder_id (integer); id (integer); labels (object); name (string); repeat_interval (integer); repeat_total (integer); rule_configs (object); updated_at (integer); updater_id (integer); updater_name (string) ### rule-update-fields Batch update rule fields @@ -282,6 +301,7 @@ Batch update rule fields - `--repeat-interval` int64 - `--repeat-total` int64 - body-only (`--data`): annotations (object); enabled_times (array); labels (object) +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: message (string); name (string) ### store-ruleset-create Create ruleset @@ -289,6 +309,7 @@ Create ruleset - `--open-flag` int64 — Sharing scope. '0' = private (creator only), '1' = account-shared, '2' = public. Defaults to '0' if omitted. - `--payload` string (required) — JSON string containing the alert rule definitions. - `--type-ident` string (required) — Datasource type identifier this ruleset applies to, e.g. 'prometheus'. +- response: single object (`data` unwrapped to the top level) — fields: created_at (integer); creator_account_id (integer); creator_id (integer); creator_name (string); id (integer); note (string); open_flag (integer); payload (string); type_ident (string); updated_at (integer) ### store-ruleset-delete Delete ruleset @@ -297,10 +318,12 @@ Delete ruleset ### store-ruleset-info Get ruleset detail - `--id` int64 (required) — Resource ID. +- response: single object (`data` unwrapped to the top level) — fields: created_at (integer); creator_account_id (integer); creator_id (integer); creator_name (string); id (integer); note (string); open_flag (integer); payload (string); type_ident (string); updated_at (integer) ### store-ruleset-list List rulesets - `--type-ident` string (required) — Datasource type identifier to filter by, e.g. 'prometheus'. +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: created_at (integer); creator_account_id (integer); creator_id (integer); creator_name (string); id (integer); note (string); open_flag (integer); payload (string); type_ident (string); updated_at (integer) ### store-ruleset-update Update ruleset @@ -308,6 +331,7 @@ Update ruleset - `--note` string (required) — New description. - `--open-flag` int64 — New sharing scope. '0' = private, '1' = account-shared, '2' = public. - `--payload` string (required) — New JSON string of alert rule definitions. +- response: single object (`data` unwrapped to the top level) — fields: created_at (integer); creator_account_id (integer); creator_id (integer); creator_name (string); id (integer); note (string); open_flag (integer); payload (string); type_ident (string); updated_at (integer) ### targets List monitored targets @@ -315,6 +339,7 @@ List monitored targets - `--cursor` string — Opaque pagination cursor from the previous response's 'next_cursor'. Omit / pass empty string for the first page. Reset whenever 'keyword', 'limit', or tenant changes. - `--keyword` string — Prefix match against 'target_locator'. ASCII only, no whitespace, no '|', max 256 bytes. Substring search is not supported. - `--limit` int64 — Page size. Default 50, max 200. (max 200) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: agent_version (string); cluster_name (string); edge_ipport (string); target_kind (string); target_locator (string); updated_at (integer) ### tools-catalog List target tool catalog @@ -322,6 +347,7 @@ List target tool catalog - `--include-output-shape` bool — When true, each tool entry includes its 'output_shape' JSON Schema. Defaults to false to keep responses small for LLM consumption. - `--target-kind` string — Optional target kind. When omitted webapi auto-infers across currently known kinds. Built-in kinds: 'host', 'mysql'. Required on retry when the previous call returned 'ambiguous_target_kind'. - `--target-locator` string (required) — Target identifier (host name, MySQL address, …). Max 256 bytes; no whitespace, control characters, or '|'. +- response: single object (`data` unwrapped to the top level) — fields: error (object); target (object); tools (array) ### tools-invoke Invoke target tools @@ -329,6 +355,7 @@ Invoke target tools - `--target-kind` string — Optional target kind; auto-inferred when omitted. - `--target-locator` string (required) — Target identifier. Same validation rules as '/monit/tools/catalog'. - body-only (`--data`): tools (array) (required) +- response: single object (`data` unwrapped to the top level) — fields: error (object); results (array); target (object) diff --git a/skills/flashduty/reference/role.md b/skills/flashduty/reference/role.md index 48f57f9..041c1a4 100644 --- a/skills/flashduty/reference/role.md +++ b/skills/flashduty/reference/role.md @@ -75,11 +75,13 @@ Enable a role ### info Get role detail - `` (positional, required) int64 — Role ID. +- response: single object (`data` unwrapped to the top level) — fields: created_at (integer); description (string); editable (boolean); permission_ids (array); role_id (integer); role_name (string); status (string); updated_at (integer) ### list List roles - `--asc` bool — Ascending sort order. - `--orderby` string — Sort field. · enum: created_at | updated_at +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: created_at (integer); description (string); editable (boolean); permission_ids (array); role_id (integer); role_name (string); status (string); updated_at (integer) ### member-grant [...] Grant role to members @@ -94,11 +96,13 @@ Revoke role from members ### permission-factor-list List permission factors - `--factor-types` stringSlice — Filter by factor type. · enum: api | button | visit | menu | url +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: factor_name (string); factor_type (string) ### permission-list List permissions - `--role-ids` intSlice — Filter to permissions granted to these roles. - `--with-all` bool — If true, return all permissions with is_granted set to indicate which are granted. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: class (string); description (string); id (integer); is_granted (boolean); permission_name (string); permission_type (string); scope (string); status (string) ### upsert Create or update a role @@ -106,6 +110,7 @@ Create or update a role - `--permission-ids` intSlice — Permission IDs to grant. Replaces the existing set. - `--role-id` int64 — Role ID. Omit or set to 0 to create. - `--role-name` string (required) — Role display name. 1–39 characters. (1-39 chars) +- response: single object (`data` unwrapped to the top level) — fields: role_id (integer); role_name (string) diff --git a/skills/flashduty/reference/route.md b/skills/flashduty/reference/route.md index 6cab941..4a127f3 100644 --- a/skills/flashduty/reference/route.md +++ b/skills/flashduty/reference/route.md @@ -52,10 +52,12 @@ fduty route list --output-format toon ### info Get routing rule detail - `` (positional, required) int64 — Integration ID. Must be greater than 0. +- response: single object (`data` unwrapped to the top level) — fields: cases (array); created_at (integer); creator_id (integer); default (object); deleted_at (integer); integration_id (integer); sections (array); status (string); updated_at (integer); updated_by (integer); version (integer) ### list [...] List routing rules - `` (positional, required) intSlice — Integration IDs to fetch routing rules for. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: cases (array); created_at (integer); creator_id (integer); default (object); deleted_at (integer); integration_id (integer); sections (array); status (string); updated_at (integer); updated_by (integer); version (integer) ### upsert Upsert routing rule diff --git a/skills/flashduty/reference/rum.md b/skills/flashduty/reference/rum.md index 0027054..80292ce 100644 --- a/skills/flashduty/reference/rum.md +++ b/skills/flashduty/reference/rum.md @@ -63,6 +63,7 @@ Create application - `` (positional, required) int64 — Owning team ID. - `--type` string (required) — Application type. · enum: browser | ios | android | react-native | flutter | kotlin-multiplatform | roku | unity - body-only (`--data`): alerting (object); links (object); tracing (object) +- response: single object (`data` unwrapped to the top level) — fields: application_id (string); application_name (string); client_token (string) ### application-delete Delete application @@ -71,10 +72,12 @@ Delete application ### application-info Get application detail - `` (positional, required) string — RUM application ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); alerting (object); application_id (string); application_name (string); client_token (string); created_at (integer); created_by (integer); is_private (boolean); links (object); no_geo (boolean); no_ip (boolean); status (string); team_id (integer); tracing (object); type (string); updated_at (integer); updated_by (integer) ### application-infos [...] Batch get applications - `` (positional, required) stringSlice — Up to 200 application IDs. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); alerting (object); application_id (string); application_name (string); client_token (string); created_at (integer); created_by (integer); is_private (boolean); links (object); no_geo (boolean); no_ip (boolean); status (string); team_id (integer); tracing (object); type (string); updated_at (integer); updated_by (integer) ### application-list List applications @@ -86,6 +89,7 @@ List applications - `--query` string — Search query to filter by application name. - `--search-after-ctx` string - `--team-id` int64 — Filter by team ID. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); alerting (object); application_id (string); application_name (string); client_token (string); created_at (integer); created_by (integer); is_private (boolean); links (object); no_geo (boolean); no_ip (boolean); status (string); team_id (integer); tracing (object); type (string); updated_at (integer); updated_by (integer) ### application-update Update application @@ -102,6 +106,7 @@ Update application Test application webhook - `` (positional, required) string — RUM application ID. - `--webhook-url` string (required) — Webhook URL to receive the sample alert event. +- response: single object (`data` unwrapped to the top level) — fields: message (string); ok (boolean); status_code (integer) ### data-query Query RUM data @@ -119,20 +124,24 @@ Count facet value distribution - `--sql` string — SQL WHERE clause (no SELECT) for additional filtering. - `--start-time` int64 (required) — Start of the time range, Unix epoch milliseconds. - body-only (`--data`): facet_value (any) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: count (integer); facet_value (any) ### facet-list List RUM facet fields - `--is-facet` bool — When true, return only facet-enabled fields. When false or omitted, return all fields. - `--scopes` stringSlice — Filter by RUM data scopes. Valid values: 'session', 'view', 'action', 'error', 'resource', 'long_task', 'vital', 'issue', 'sourcemap'. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); description (string); edit_able (boolean); enum_values (array); field_key (string); field_name (string); group (string); is_facet (boolean); queryable (boolean); scopes (array); show_type (string); status (string); unit_family (string); unit_name (string); value_type (string) ### field-list List RUM fields - `--is-facet` bool — When true, return only facet-enabled fields. When false or omitted, return all fields. - `--scopes` stringSlice — Filter by RUM data scopes. Valid values: 'session', 'view', 'action', 'error', 'resource', 'long_task', 'vital', 'issue', 'sourcemap'. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); description (string); edit_able (boolean); enum_values (array); field_key (string); field_name (string); group (string); is_facet (boolean); queryable (boolean); scopes (array); show_type (string); status (string); unit_family (string); unit_name (string); value_type (string) ### issue-info Get issue detail - `` (positional, required) string — Issue ID. +- response: single object (`data` unwrapped to the top level) — fields: age (integer); application_id (string); application_name (string); created_at (integer); error (object); error_count (integer); first_seen (object); is_crash (boolean); issue_id (string); last_seen (object); regression (object); resolved_at (integer); resolved_by (integer); service (string); session_count (integer); severity (string); status (string); suspected_cause (object); team_id (integer); updated_at (integer); versions (array) ### issue-list List issues @@ -151,6 +160,7 @@ List issues - `--statuses` stringSlice — Filter by statuses. · enum: for_review | reviewed | ignored | resolved - `--suspected-causes` stringSlice — Filter by suspected causes. - `--team-ids` intSlice — Filter by team IDs. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: age (integer); application_id (string); application_name (string); created_at (integer); error (object); error_count (integer); first_seen (object); is_crash (boolean); issue_id (string); last_seen (object); regression (object); resolved_at (integer); resolved_by (integer); service (string); session_count (integer); severity (string); status (string); suspected_cause (object); team_id (integer); updated_at (integer); versions (array) ### issue-update Update issue @@ -162,6 +172,7 @@ Update issue Get session replay metadata - `` (positional, required) string — RUM session ID. - `--ts` int64 — Unix timestamp in milliseconds of the session start time. Optional; disambiguates when a session ID has been reused across different time windows. +- response: single object (`data` unwrapped to the top level) — fields: application (object); device (object); foreground_periods (array); session (object); views (array) ### session-replay-segments List session replay segments @@ -171,6 +182,7 @@ List session replay segments - `--ts` int64 — Unix timestamp in milliseconds. When set (and 'search_after_ctx' is empty), seeks to the most recent full-snapshot segment at or before this time instead of starting from the beginning. - `--url-mode` bool — When 'true', return presigned download URLs as a JSON envelope instead of streaming segment bytes. Defaults to 'false'. - `--view-id` string — Restrict results to segments belonging to this view. Omit to page through the entire session. +- response: single object (`data` unwrapped to the top level) — fields: items (array); search_after_ctx (string) diff --git a/skills/flashduty/reference/safari.md b/skills/flashduty/reference/safari.md index 5802813..331a2e3 100644 --- a/skills/flashduty/reference/safari.md +++ b/skills/flashduty/reference/safari.md @@ -58,6 +58,7 @@ Create A2A agent - `--streaming` bool — Whether the remote agent supports streaming. - `--team-id` int64 — Team scope: 0 = account-wide; >0 = team. Creating at account scope requires the owner/admin role; creating into a team requires actual membership in that team. - body-only (`--data`): auth_config (object) +- response: single object (`data` unwrapped to the top level) — fields: agent_id (string) ### a2a-agent-delete Delete A2A agent @@ -74,6 +75,7 @@ Enable A2A agent ### a2a-agent-get Get A2A agent detail - `` (positional, required) string — Target agent ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); agent_card_name (string); agent_card_skills (array); agent_id (string); agent_name (string); allow_insecure_oauth_http (boolean); allow_insecure_tls_skip_verify (boolean); auth_config (object); auth_mode (string); auth_type (string); can_edit (boolean); card_resolve_timeout (integer); card_url (string); created_at (integer); created_by (integer); environment_id (string); environment_kind (string); instructions (string); oauth_metadata (string); secret_schema (string); status (string); streaming (boolean); task_timeout (integer); team_id (integer); updated_at (integer) ### a2a-agent-list List A2A agents @@ -83,6 +85,7 @@ List A2A agents - `--query` string — Case-insensitive substring search across agent name, instructions, card URL, agent ID, and the resolved card name. (≤128 chars) - `--scope` string — Visibility scope: 'all' (account-scope plus the caller's visible teams), 'account' (account-scope only), or 'team' (team-scoped rows across the caller's visible teams). · enum: all | account | team - `--team-ids` intSlice — Filter to these team IDs; empty = the caller's visible set. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); agent_card_name (string); agent_card_skills (array); agent_id (string); agent_name (string); allow_insecure_oauth_http (boolean); allow_insecure_tls_skip_verify (boolean); auth_config (object); auth_mode (string); auth_type (string); can_edit (boolean); card_resolve_timeout (integer); card_url (string); created_at (integer); created_by (integer); environment_id (string); environment_kind (string); instructions (string); oauth_metadata (string); secret_schema (string); status (string); streaming (boolean); task_timeout (integer); team_id (integer); updated_at (integer) ### a2a-agent-update Update A2A agent @@ -117,6 +120,7 @@ Create Automation rule - `--schedule-trigger-enabled` bool — Whether the schedule trigger is enabled. Defaults to true when omitted; HTTP-POST-only rules should send false. - `--team-id` int64 — Scope team ID. 0 or omitted means a personal rule; >0 means a team in the account. Immutable after creation. (min 0) - `--timezone` string — IANA timezone 'cron_expr' is evaluated in, e.g. 'Asia/Shanghai'. Must be a timezone name loadable by the server; an invalid value is rejected. Defaults to the caller's member timezone, then the account timezone, then UTC when omitted. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); can_edit (boolean); created_at (integer); cron_expr (string); enabled (boolean); environment_id (string); environment_kind (string); http_post_token (string); http_post_trigger_enabled (boolean); http_post_trigger_id (string); http_post_trigger_url (string); name (string); oncall_incident_channel_ids (array); oncall_incident_severities (array); oncall_incident_trigger_enabled (boolean); oncall_incident_trigger_id (string); owner_id (integer); prompt (string); rule_id (string); run_scope (string); schedule_next_fire_at_ms (integer); schedule_trigger_enabled (boolean); schedule_trigger_id (string); team_id (integer); timezone (string); updated_at (integer) ### automation-rule-delete Delete Automation rule @@ -125,6 +129,7 @@ Delete Automation rule ### automation-rule-get Get Automation rule - `` (positional, required) string — Rule ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); can_edit (boolean); created_at (integer); cron_expr (string); enabled (boolean); environment_id (string); environment_kind (string); http_post_token (string); http_post_trigger_enabled (boolean); http_post_trigger_id (string); http_post_trigger_url (string); name (string); oncall_incident_channel_ids (array); oncall_incident_severities (array); oncall_incident_trigger_enabled (boolean); oncall_incident_trigger_id (string); owner_id (integer); prompt (string); rule_id (string); run_scope (string); schedule_next_fire_at_ms (integer); schedule_trigger_enabled (boolean); schedule_trigger_id (string); team_id (integer); timezone (string); updated_at (integer) ### automation-rule-list List Automation rules @@ -136,10 +141,12 @@ List Automation rules - `--scope` string — Scope filter: 'all' (own personal + accessible team rules), 'personal', or 'team'; default 'all'. · enum: all | personal | team - `--search-after-ctx` string - `--team-ids` intSlice — Filter to these team IDs; this narrows results and does not expand access. +- response: single object (`data` unwrapped to the top level) — fields: rules (array); total (integer) ### automation-rule-run Run Automation rule - `` (positional, required) string — Rule ID. +- response: single object (`data` unwrapped to the top level) — fields: preflight (object); rule_id (string); run (object); trigger_kind (string) ### automation-rule-update Update Automation rule @@ -157,6 +164,7 @@ Update Automation rule - `` (positional, required) string — Target rule ID. - `--schedule-trigger-enabled` bool — Whether the schedule trigger is enabled. - `--team-id` int64 — Only the current value is accepted; personal/team scope is immutable after creation. (min 0) +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); can_edit (boolean); created_at (integer); cron_expr (string); enabled (boolean); environment_id (string); environment_kind (string); http_post_token (string); http_post_trigger_enabled (boolean); http_post_trigger_id (string); http_post_trigger_url (string); name (string); oncall_incident_channel_ids (array); oncall_incident_severities (array); oncall_incident_trigger_enabled (boolean); oncall_incident_trigger_id (string); owner_id (integer); prompt (string); rule_id (string); run_scope (string); schedule_next_fire_at_ms (integer); schedule_trigger_enabled (boolean); schedule_trigger_id (string); team_id (integer); timezone (string); updated_at (integer) ### automation-run-list List Automation runs @@ -168,10 +176,12 @@ List Automation runs - `--started-before-ms` int64 — Start-time upper bound, Unix milliseconds. - `--status` string — Run status filter. · enum: queued | running | retrying | succeeded | partial | failed | skipped | abandoned - `--trigger-kind` string — Trigger kind filter. · enum: schedule | debug | manual | http_post | oncall_incident +- response: single object (`data` unwrapped to the top level) — fields: runs (array); total (integer) ### automation-template-list List Automation templates - `--locale` string — Template locale such as zh-CN or en-US. Omit to detect from the request locale. (≤16 chars) +- response: single object (`data` unwrapped to the top level) — fields: templates (array) ### automation-triggers-{trigger_id}-fire Fire an Automation HTTP POST trigger @@ -199,6 +209,7 @@ Create MCP server - `--transport` string (required) — Transport protocol. · enum: stdio | sse | streamable-http - `--url` string — Server URL (sse / streamable-http transport). - body-only (`--data`): env (object); headers (object) +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); ai_description (string); allow_insecure_oauth_http (boolean); allow_insecure_tls_skip_verify (boolean); args (array); auth_mode (string); call_timeout (integer); can_edit (boolean); command (string); connect_timeout (integer); created_at (integer); created_by (integer); description (string); env (object); environment_id (string); environment_kind (string); headers (object); list_error (string); oauth_metadata (string); proxy_url (string); secret_schema (string); server_id (string); server_name (string); source_template_name (string); status (string); team_id (integer); tool_count (integer); tools (array); transport (string); updated_at (integer); url (string) ### mcp-server-delete Delete MCP server @@ -215,6 +226,7 @@ Enable MCP server ### mcp-server-get Get MCP server detail - `` (positional, required) string — Target MCP server ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); ai_description (string); allow_insecure_oauth_http (boolean); allow_insecure_tls_skip_verify (boolean); args (array); auth_mode (string); call_timeout (integer); can_edit (boolean); command (string); connect_timeout (integer); created_at (integer); created_by (integer); description (string); env (object); environment_id (string); environment_kind (string); headers (object); list_error (string); oauth_metadata (string); proxy_url (string); secret_schema (string); server_id (string); server_name (string); source_template_name (string); status (string); team_id (integer); tool_count (integer); tools (array); transport (string); updated_at (integer); url (string) ### mcp-server-list List MCP servers @@ -225,6 +237,7 @@ List MCP servers - `--scope` string — Restrict results to a scope: 'account' for account-wide rows only, 'team' for the caller's own visible team rows only, or omit (defaults to 'all') for both, subject to team_ids/include_account. · enum: all | account | team - `--search-after-ctx` string - `--team-ids` intSlice — Filter to these team IDs; empty = the caller's visible set. +- response: single object (`data` unwrapped to the top level) — fields: servers (array); total (integer) ### mcp-server-update Update MCP server @@ -246,6 +259,7 @@ Update MCP server - `--transport` string — Transport protocol. · enum: stdio | sse | streamable-http - `--url` string — Server URL (sse / streamable-http transport). - body-only (`--data`): env (object); headers (object) +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); ai_description (string); allow_insecure_oauth_http (boolean); allow_insecure_tls_skip_verify (boolean); args (array); auth_mode (string); call_timeout (integer); can_edit (boolean); command (string); connect_timeout (integer); created_at (integer); created_by (integer); description (string); env (object); environment_id (string); environment_kind (string); headers (object); list_error (string); oauth_metadata (string); proxy_url (string); secret_schema (string); server_id (string); server_name (string); source_template_name (string); status (string); team_id (integer); tool_count (integer); tools (array); transport (string); updated_at (integer); url (string) ### session-delete Delete session @@ -262,6 +276,7 @@ Get session detail - `--search-after-ctx` string — Opaque keyset cursor from a previous response; pass it back to fetch the next older page. (≤4096 chars) - `` (positional, required) string — Target session ID. (≥1 chars) - `--share-token` string — Share token for accessing a session through its share link. Omit it for normal account-authorized access. (≤512 chars) +- response: single object (`data` unwrapped to the top level) — fields: events (array); has_more_older (boolean); search_after_ctx (string); session (object); suggest_init (boolean) ### session-list List sessions @@ -277,6 +292,7 @@ List sessions - `--search-after-ctx` string - `--status` string — Archive bucket: active (default) returns un-archived, archived returns archived, all returns both. · enum: active | archived | all - `--team-ids` intSlice — Optional explicit team filter; intersects with 'scope' and never expands access. +- response: single object (`data` unwrapped to the top level) — fields: sessions (array); suggest_init (boolean); total (integer) ### skill-delete Delete skill @@ -293,6 +309,7 @@ Enable skill ### skill-get Get skill detail - `` (positional, required) string — Target skill ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); author (string); can_edit (boolean); checksum (string); content (string); created (boolean); created_at (integer); created_by (integer); description (string); description_en (string); is_modified (boolean); license (string); s3_key (string); skill_id (string); skill_name (string); source_template_name (string); source_template_version (string); status (string); tags (array); team_id (integer); tools (array); update_available (boolean); updated_at (integer); version (string) ### skill-list List skills @@ -303,6 +320,7 @@ List skills - `--scope` string — Restrict results to 'all' (default), 'account'-only (team_id=0), or 'team'-only (excludes account-scoped rows). Overrides 'include_account' when set. · enum: all | account | team - `--search-after-ctx` string - `--team-ids` intSlice — Filter to these team IDs; empty = the caller's visible set. +- response: single object (`data` unwrapped to the top level) — fields: skills (array); total (integer) ### skill-update Update skill @@ -310,9 +328,11 @@ Update skill - `--description-en` string — New English description. Cannot contain '<' or '>'. Omit to leave unchanged; send an empty string to explicitly clear it. (≤1024 chars) - `` (positional, required) string — Target skill ID. - `--team-id` int64 — Reassign team scope: 0 = account-wide; >0 = team. Omit to leave unchanged. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); author (string); can_edit (boolean); checksum (string); content (string); created (boolean); created_at (integer); created_by (integer); description (string); description_en (string); is_modified (boolean); license (string); s3_key (string); skill_id (string); skill_name (string); source_template_name (string); source_template_version (string); status (string); tags (array); team_id (integer); tools (array); update_available (boolean); updated_at (integer); version (string) ### skill-upload Upload skill +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); author (string); can_edit (boolean); checksum (string); content (string); created (boolean); created_at (integer); created_by (integer); description (string); description_en (string); is_modified (boolean); license (string); s3_key (string); skill_id (string); skill_name (string); source_template_name (string); source_template_version (string); status (string); tags (array); team_id (integer); tools (array); update_available (boolean); updated_at (integer); version (string) diff --git a/skills/flashduty/reference/sourcemap.md b/skills/flashduty/reference/sourcemap.md index fea51a1..9c14463 100644 --- a/skills/flashduty/reference/sourcemap.md +++ b/skills/flashduty/reference/sourcemap.md @@ -47,6 +47,7 @@ List sourcemaps - `--type` string — Platform type. Defaults to 'browser' when omitted. · enum: browser | android | ios - `--uuid` string — iOS only. Filter by dSYM bundle UUID. Max 200 characters. - `--versions` stringSlice — Filter by version strings. Up to 100 values. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: created_at (integer); git_commit_sha (string); git_repository_url (string); key (string); metadata (object); service (string); size (integer); type (string); updated_at (integer); version (string) ### stack-enrich Enrich a stack trace @@ -61,6 +62,7 @@ Enrich a stack trace - `--variant` string — Android build variant used by older Gradle plugin versions. - `--version` string (required) — Application version used when the sourcemap was uploaded. - body-only (`--data`): binary_images (array) +- response: single object (`data` unwrapped to the top level) — fields: frames (array) diff --git a/skills/flashduty/reference/status-page.md b/skills/flashduty/reference/status-page.md index f223ba9..b83f670 100644 --- a/skills/flashduty/reference/status-page.md +++ b/skills/flashduty/reference/status-page.md @@ -51,6 +51,7 @@ fduty status-page change-active-list --type incident List active status page events - `` (positional, required) int64 — Status page ID. - `--type` string (required) — Event type filter. Required. Returns only in-progress (non-terminal) events — 'investigating'/'identified'/'monitoring' for 'incident', 'scheduled'/'ongoing' for 'maintenance'. · enum: incident | maintenance +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: affected_components (array); auto_update_by_schedule (boolean); change_id (integer); close_at_seconds (integer); description (string); is_retrospective (boolean); linked_change_ids (array); notify_subscribers (boolean); page_id (integer); responder_ids (array); start_at_seconds (integer); status (string); title (string); type (string); updates (array) ### change-create Create status page event @@ -67,6 +68,7 @@ Create status page event - `--title` string (required) — Event title, up to 255 characters. (≤255 chars) - `--type` string (required) — Event type. · enum: incident | maintenance - body-only (`--data`): updates (array) (required) +- response: single object (`data` unwrapped to the top level) — fields: change_id (integer); change_name (string) ### change-delete Delete status page event @@ -77,6 +79,7 @@ Delete status page event Get status page event detail - `--change-id` int64 (required) — Event (change) ID. - `--page-id` int64 (required) — Status page ID. +- response: single object (`data` unwrapped to the top level) — fields: affected_components (array); auto_update_by_schedule (boolean); change_id (integer); close_at_seconds (integer); description (string); is_retrospective (boolean); linked_change_ids (array); notify_subscribers (boolean); page_id (integer); responder_ids (array); start_at_seconds (integer); status (string); title (string); type (string); updates (array) ### change-list List status page events @@ -85,6 +88,7 @@ List status page events - `--start-at-seconds` string — Filter events started at or after this unix timestamp (seconds). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. - `--status` string (required) — Event status filter. Required. Must be a status valid for the given 'type' (e.g. 'investigating'/'identified'/'monitoring'/'resolved' for incidents; 'scheduled'/'ongoing'/'completed' for maintenances). · enum: investigating | identified | monitoring | resolved | scheduled | ongoing | completed - `--type` string (required) — Event type filter. Required. · enum: incident | maintenance +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: affected_components (array); auto_update_by_schedule (boolean); change_id (integer); close_at_seconds (integer); description (string); is_retrospective (boolean); linked_change_ids (array); notify_subscribers (boolean); page_id (integer); responder_ids (array); start_at_seconds (integer); status (string); title (string); type (string); updates (array) ### change-timeline-create Create event timeline entry @@ -94,6 +98,7 @@ Create event timeline entry - `--page-id` int64 (required) — Status page ID. - `--status` string (required) — New event status. Must match the event type. When the status transitions to 'resolved' or 'completed', all referenced components must become 'operational'. · enum: investigating | identified | monitoring | resolved | scheduled | ongoing | completed - body-only (`--data`): component_changes (array) +- response: single object (`data` unwrapped to the top level) — fields: update_id (string) ### change-timeline-delete Delete event timeline entry @@ -126,6 +131,7 @@ Delete status page component Upsert status page component - `` (positional, required) int64 — Status page ID. - body-only (`--data`): components (array) (required) +- response: single object (`data` unwrapped to the top level) — fields: component_ids (array) ### create Create status page @@ -140,6 +146,7 @@ Create status page - `--type` string (required) — Visibility type of the status page. · enum: public | internal - `--url-name` string (required) — URL-safe slug, unique per account and page type. (≤255 chars) - body-only (`--data`): custom_links (array); subscription (object) +- response: single object (`data` unwrapped to the top level) — fields: page_id (integer); page_name (string); page_url_name (string) ### delete Delete status page @@ -150,18 +157,21 @@ Get status page detail ### list List status pages +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: components (array); contact_info (string); custom_domain (string); custom_links (array); dark_logo (string); date_view (string); display_uptime_mode (string); favicon (string); logo (string); logo_url (string); name (string); page_footer (string); page_header (string); page_id (integer); sections (array); subscription (object); template_preference (string); type (string); url_name (string) ### migrate-email-subscribers Migrate email subscribers - `--api-key` string (required) — Atlassian Statuspage API key with access to the source page. - `--source-page-id` string (required) — Atlassian Statuspage source page ID. - `--target-page-id` int64 (required) — Flashduty target status page ID that will receive the imported subscribers. +- response: single object (`data` unwrapped to the top level) — fields: job_id (string) ### migrate-structure Migrate status page structure - `--api-key` string (required) — Atlassian Statuspage API key with access to the source page. - `` (positional, required) string — Atlassian Statuspage source page ID. - `--url-name` string — Target URL name for the migrated status page. When omitted, the source page's URL name is reused. +- response: single object (`data` unwrapped to the top level) — fields: job_id (string) ### migration-cancel Cancel status page migration @@ -170,6 +180,7 @@ Cancel status page migration ### migration-status Get migration status - `` (positional, required) string — Migration job ID returned by 'migrate-structure' or 'migrate-email-subscribers'. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); created_at (integer); error (string); job_id (string); phase (string); progress (object); source_page_id (string); status (string); target_page_id (integer); updated_at (integer) ### section-delete [...] Delete status page section @@ -180,6 +191,7 @@ Delete status page section Upsert status page section - `` (positional, required) int64 — Status page ID. - body-only (`--data`): sections (array) (required) +- response: single object (`data` unwrapped to the top level) — fields: section_ids (array) ### subscriber-export Export subscribers @@ -198,6 +210,7 @@ List status page subscribers - `--limit` int64 — Page size (1-100). (1-100) - `--page` int64 — Page number (1-based). (min 1) - `` (positional, required) int64 — Status page ID. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: all (boolean); components (array); locale (string); method (string); recipient (string) ### template-delete Delete status page template @@ -215,6 +228,7 @@ Upsert status page template - `` (positional, required) int64 — Status page ID. - `--type` string (required) — Template category. 'pre_defined' for predefined event templates; 'message' for notification message templates. · enum: pre_defined | message - body-only (`--data`): template (object) (required) +- response: single object (`data` unwrapped to the top level) — fields: template_id (string) ### update Update status page diff --git a/skills/flashduty/reference/team.md b/skills/flashduty/reference/team.md index 02bc7b0..d538815 100644 --- a/skills/flashduty/reference/team.md +++ b/skills/flashduty/reference/team.md @@ -66,16 +66,19 @@ Get team detail - `--id` int64 - `--name` string - `--ref-id` string +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); created_at (integer); creator_id (integer); creator_name (string); description (string); person_ids (array); ref_id (string); status (string); team_id (integer); team_name (string); updated_at (integer); updated_by (integer); updated_by_name (string) ### info Get team detail - `--ref-id` string — External reference ID. - `--team-id` int64 — Team ID. - `--team-name` string — Team name. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); created_at (integer); creator_id (integer); creator_name (string); description (string); person_ids (array); ref_id (string); status (string); team_id (integer); team_name (string); updated_at (integer); updated_by (integer); updated_by_name (string) ### infos [...] Batch get teams - `` (positional, required) intSlice — List of team IDs to look up. Max 100. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: person_ids (array); team_id (integer); team_name (string) ### list List teams @@ -85,6 +88,7 @@ List teams - `--orderby` string - `--page` int - `--person-id` int64 +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); created_at (integer); creator_id (integer); creator_name (string); description (string); person_ids (array); ref_id (string); status (string); team_id (integer); team_name (string); updated_at (integer); updated_by (integer); updated_by_name (string) ### update Update an existing team @@ -106,6 +110,7 @@ Create or update a team - `--reset-if-name-exist` bool — If true and a team with the same name already exists, reset its membership to the provided person_ids. - `--team-id` int64 — Team ID. Omit or set to 0 to create a new team. - `--team-name` string (required) — Team display name. 1–39 characters. (1-39 chars) +- response: single object (`data` unwrapped to the top level) — fields: team_id (integer); team_name (string) diff --git a/skills/flashduty/reference/template.md b/skills/flashduty/reference/template.md index 83a2c17..7cbf0c1 100644 --- a/skills/flashduty/reference/template.md +++ b/skills/flashduty/reference/template.md @@ -76,6 +76,7 @@ Create a template - `--wecom` string — WeCom robot message template source. - `--wecom-app` string — WeCom app message template source. - `--zoom` string — Zoom bot message template source. +- response: single object (`data` unwrapped to the top level) — fields: template_id (string); template_name (string) ### delete Delete a template @@ -88,10 +89,12 @@ List available template functions ### get-preset Get the preset template for a channel - `--channel` string +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); created_at (integer); creator_id (integer); deleted_at (integer); description (string); dingtalk (string); dingtalk_app (string); email (string); feishu (string); feishu_app (string); slack (string); slack_app (string); sms (string); status (string); team_id (integer); teams_app (string); telegram (string); template_id (string); template_name (string); updated_at (integer); updated_by (integer); voice (string); wecom (string); wecom_app (string); zoom (string) ### info Get template detail - `` (positional, required) string — Target template ID. Pass '000000000000000000000001' to address the built-in preset. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); created_at (integer); creator_id (integer); deleted_at (integer); description (string); dingtalk (string); dingtalk_app (string); email (string); feishu (string); feishu_app (string); slack (string); slack_app (string); sms (string); status (string); team_id (integer); teams_app (string); telegram (string); template_id (string); template_name (string); updated_at (integer); updated_by (integer); voice (string); wecom (string); wecom_app (string); zoom (string) ### list List templates @@ -104,6 +107,7 @@ List templates - `--query` string — Regex or substring match on template_name. - `--search-after-ctx` string - `--team-ids` intSlice — Filter by specific team IDs. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); created_at (integer); creator_id (integer); deleted_at (integer); description (string); dingtalk (string); dingtalk_app (string); email (string); feishu (string); feishu_app (string); slack (string); slack_app (string); sms (string); status (string); team_id (integer); teams_app (string); telegram (string); template_id (string); template_name (string); updated_at (integer); updated_by (integer); voice (string); wecom (string); wecom_app (string); zoom (string) ### preview Preview template @@ -111,6 +115,7 @@ Preview template - `--incident-id` string — Incident ID whose data is used to render the template; mock data is used when omitted. A MongoDB ObjectID hex string. - `--type` string (required) — Template channel type that selects the rendering engine. - body-only (`--data`): incident_card_hidden_fields (object) +- response: single object (`data` unwrapped to the top level) — fields: content (string); fixed_fields (array); message (string); success (boolean) ### update Update a template @@ -138,6 +143,7 @@ Validate and preview a template - `--channel` string - `--file` string - `--incident` string +- response: single object (`data` unwrapped to the top level) — fields: content (string); fixed_fields (array); message (string); success (boolean) ### variables List available template variables From 148eedbc4680cba76148544ae4700679c8991842 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 27 Jul 2026 03:36:30 -0700 Subject: [PATCH 3/7] docs(schedule,insight): correct stale/misleading guidance, regen fences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit schedule.md: - Stop recommending `oncall who` as a convenient global on-call snapshot. It returns every schedule in one call (77-82KB / ~2800 lines), always spills to a file, and still takes 3-10 follow-up reads to dig out 3-4 fields. Replace the hot-flow and gotcha with the scoped path: `schedule list --query|--team-ids` to find IDs, then per-schedule `info --start now --end +1h`. - Note `--output-format toon` is not jq-parseable; use `--json` to filter. insight.md: - Note `top-alerts` and `alert-topk-by-label` return the same top-K breakdown (alert-topk-by-label is the superset) — pick one. - Note `responder` has no `--limit`/`--page`; account-wide load analysis should take the one full response, not cap-and-refetch. - Note re-running a whole `insight` command to tweak a jq filter is a real repeated backend query; save `--json` once and iterate jq locally. Also regenerates both fences via `skilldoc gen` to pick up the response-shape summaries from the prior commit. --- skills/flashduty/reference/insight.md | 11 +++++++++++ skills/flashduty/reference/schedule.md | 25 +++++++++++++++---------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/skills/flashduty/reference/insight.md b/skills/flashduty/reference/insight.md index fe9ea36..5222c31 100644 --- a/skills/flashduty/reference/insight.md +++ b/skills/flashduty/reference/insight.md @@ -84,6 +84,7 @@ Get account-level insight - `--team-ids` intSlice — Filter by team IDs. At most 100 entries. - `--time-zone` string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. - body-only (`--data`): fields (object); labels (object) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); acknowledgement_pct (number); channel_id (integer); channel_name (string); hours (string); mean_seconds_to_ack (number); mean_seconds_to_close (number); noise_reduction_pct (number); responder_id (integer); responder_name (string); team_id (integer); team_name (string); total_alert_cnt (integer); total_alert_event_cnt (integer); total_engaged_seconds (integer); total_incident_cnt (integer); total_incidents_acknowledged (integer); total_incidents_auto_closed (integer); total_incidents_closed (integer); total_incidents_escalated (integer); total_incidents_manually_closed (integer); total_incidents_manually_escalated (integer); total_incidents_reassigned (integer); total_incidents_timeout_closed (integer); total_incidents_timeout_escalated (integer); total_interruptions (integer); total_notifications (integer); total_seconds_to_ack (integer); total_seconds_to_close (integer); ts (integer) ### alert-topk-by-label Get top-K alerts grouped by check or resource @@ -111,6 +112,7 @@ Get top-K alerts grouped by check or resource - `--team-ids` intSlice — Filter by team IDs. At most 100 entries. - `--time-zone` string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. - body-only (`--data`): fields (object); labels (object) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: hours (string); label (string); total_alert_cnt (integer); total_alert_event_cnt (integer) ### channel Get channel insight @@ -136,6 +138,7 @@ Get channel insight - `--team-ids` intSlice — Filter by team IDs. At most 100 entries. - `--time-zone` string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. - body-only (`--data`): fields (object); labels (object) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); acknowledgement_pct (number); channel_id (integer); channel_name (string); hours (string); mean_seconds_to_ack (number); mean_seconds_to_close (number); noise_reduction_pct (number); responder_id (integer); responder_name (string); team_id (integer); team_name (string); total_alert_cnt (integer); total_alert_event_cnt (integer); total_engaged_seconds (integer); total_incident_cnt (integer); total_incidents_acknowledged (integer); total_incidents_auto_closed (integer); total_incidents_closed (integer); total_incidents_escalated (integer); total_incidents_manually_closed (integer); total_incidents_manually_escalated (integer); total_incidents_reassigned (integer); total_incidents_timeout_closed (integer); total_incidents_timeout_escalated (integer); total_interruptions (integer); total_notifications (integer); total_seconds_to_ack (integer); total_seconds_to_close (integer); ts (integer) ### channel-export Export channel insight @@ -210,6 +213,7 @@ List insight incidents - `--team-ids` intSlice — Filter by team IDs. At most 100 entries. - `--time-zone` string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. - body-only (`--data`): fields (object); labels (object) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: acknowledgements (integer); assigned_to (object); assignments (integer); channel_id (integer); channel_name (string); closed_by (string); closer_id (integer); closer_name (string); created_at (integer); creator_id (integer); creator_name (string); description (string); engaged_seconds (integer); escalations (integer); ever_muted (boolean); fields (object); frequency (string); hours (string); incident_id (string); interruptions (integer); labels (object); manual_escalations (integer); notifications (integer); owner_id (integer); owner_name (string); progress (string); reassignments (integer); responders (array); seconds_to_ack (integer); seconds_to_close (integer); severity (string); snoozed_before (integer); team_id (integer); team_name (string); timeout_escalations (integer); title (string) ### incidents Query incidents with performance metrics @@ -217,6 +221,7 @@ Query incidents with performance metrics - `--page` int - `--since` string - `--until` string +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: acknowledgements (integer); assigned_to (object); assignments (integer); channel_id (integer); channel_name (string); closed_by (string); closer_id (integer); closer_name (string); created_at (integer); creator_id (integer); creator_name (string); description (string); engaged_seconds (integer); escalations (integer); ever_muted (boolean); fields (object); frequency (string); hours (string); incident_id (string); interruptions (integer); labels (object); manual_escalations (integer); notifications (integer); owner_id (integer); owner_name (string); progress (string); reassignments (integer); responders (array); seconds_to_ack (integer); seconds_to_close (integer); severity (string); snoozed_before (integer); team_id (integer); team_name (string); timeout_escalations (integer); title (string) ### responder Get responder insight @@ -242,6 +247,7 @@ Get responder insight - `--team-ids` intSlice — Filter by team IDs. At most 100 entries. - `--time-zone` string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. - body-only (`--data`): fields (object); labels (object) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); acknowledgement_pct (number); channel_id (integer); channel_name (string); hours (string); mean_seconds_to_ack (number); responder_id (integer); responder_name (string); team_id (integer); team_name (string); total_engaged_seconds (integer); total_incident_cnt (integer); total_incidents_acknowledged (integer); total_incidents_escalated (integer); total_incidents_manually_escalated (integer); total_incidents_reassigned (integer); total_incidents_timeout_escalated (integer); total_interruptions (integer); total_notifications (integer); total_seconds_to_ack (integer); ts (integer) ### responder-export Export responder insight @@ -292,6 +298,7 @@ Get team insight - `--team-ids` intSlice — Filter by team IDs. At most 100 entries. - `--time-zone` string — IANA time zone name used to interpret the time range (e.g. 'Asia/Shanghai'). Defaults to the account time zone. - body-only (`--data`): fields (object); labels (object) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); acknowledgement_pct (number); channel_id (integer); channel_name (string); hours (string); mean_seconds_to_ack (number); mean_seconds_to_close (number); noise_reduction_pct (number); responder_id (integer); responder_name (string); team_id (integer); team_name (string); total_alert_cnt (integer); total_alert_event_cnt (integer); total_engaged_seconds (integer); total_incident_cnt (integer); total_incidents_acknowledged (integer); total_incidents_auto_closed (integer); total_incidents_closed (integer); total_incidents_escalated (integer); total_incidents_manually_closed (integer); total_incidents_manually_escalated (integer); total_incidents_reassigned (integer); total_incidents_timeout_closed (integer); total_incidents_timeout_escalated (integer); total_interruptions (integer); total_notifications (integer); total_seconds_to_ack (integer); total_seconds_to_close (integer); ts (integer) ### team-export Export team insight @@ -324,6 +331,7 @@ Query top alert sources by label - `--limit` int - `--since` string - `--until` string +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: hours (string); label (string); total_alert_cnt (integer); total_alert_event_cnt (integer) @@ -348,6 +356,9 @@ Both families accept: relative duration (`30d`, `24h`), `now`, `+7d`, a date, or - **All `insight` commands hit the OLAP backend.** HTTP 500 means the backend is down — report it, do not retry. - **Empty result is authoritative.** A zero-row response means no matching data for that scope/window — do not widen filters or re-query with shifted keywords. - **`--aggregate-unit`** (on `account`, `alert-topk-by-label`, `channel`, `responder`, `team` and their exports) splits results into time buckets: `day` / `week` / `month`. When set, the window must span ≥24 h; `day` additionally caps the range at 31 days. +- **`top-alerts` and `alert-topk-by-label` return the same top-K breakdown** (`label`, `hours`, `total_alert_cnt`, `total_alert_event_cnt`) — `alert-topk-by-label` is the superset (adds `--team-ids`/`--channel-ids`/severity filters and `--start-time`/`--end-time`). Pick one; don't call both for the same question. +- **`responder` has no `--limit`/`--page`** — one call returns every responder's rollup for the account. For account-wide load analysis, use that single response; don't cap it and re-fetch for the rest. +- **Iterating on a `jq` filter? Save `--json` once, then re-run `jq` against the saved file.** Each `fduty insight ...` invocation is a real backend query — re-running the whole command per filter tweak multiplies OLAP load for nothing and risks a timeout on a heavy window. ## Worked example — identify noisiest check sources diff --git a/skills/flashduty/reference/schedule.md b/skills/flashduty/reference/schedule.md index c7bea34..c014e04 100644 --- a/skills/flashduty/reference/schedule.md +++ b/skills/flashduty/reference/schedule.md @@ -6,14 +6,14 @@ Prereq: `SKILL.md` read. **Read verbs are free. `delete` is irreversible — con "值班 / 排班 / 轮班 / 轮值 / 值班表 / 班表 / 谁在值班 / 当前值班 / 下一班 / 排班配置 / on-call / who is on call / schedule / rotation / shift / next on call / view or edit shifts" → **schedule**. This is the single home for everything 值班/on-call. The key ID you need is **`schedule_id` (int)** — get it from `schedule list`. -**Who is on call right now** is computed from a schedule, not stored: `schedule info --start now --end +1h` returns the current shift (and its `person_ids`). The legacy **`oncall who`** aggregates the live on-call across *all* schedules in one call, but the **`oncall` command group is being deprecated and folded into `schedule`** — use `oncall who` only as a convenience for the global snapshot; prefer `schedule info` for the durable path, and do not build new flows on `oncall *`. +**Who is on call right now** is computed from a schedule, not stored: `schedule info --start now --end +1h` returns the current shift (and its `person_ids`). The legacy **`oncall who`** returns the same data across *all* schedules in one call, but the response is 77-82KB (~2800 lines) — it always spills to a file, and still takes 3-10 follow-up reads to dig the 3-4 fields you wanted out of it. For several schedules at once, scope it instead: `schedule list --query ` or `--team-ids ` to get the schedule IDs, then `schedule info --start now --end +1h` per schedule. The **`oncall` command group is being deprecated and folded into `schedule`** — do not build new flows on `oncall *`. ## Intent → verb | want | verb | |---|---| | who is on call right now (one schedule) | `info --start now --end +1h` | -| who is on call right now (all schedules, legacy) | `oncall who` — *deprecated group; prefer per-schedule `info`* | +| who is on call right now (all schedules, legacy) | `oncall who` — *deprecated, whole-account dump (~80KB); prefer `schedule list` + per-schedule `info`* | | list all schedules (with name search / team filter) | `list` | | schedules I am assigned to | `self` | | detail + computed shifts for a schedule | `info ` | @@ -26,18 +26,17 @@ Prereq: `SKILL.md` read. **Read verbs are free. `delete` is irreversible — con ## Hot flow — who is on call right now ```bash -# 1. Find the schedule ID +# 1. Find the schedule ID(s) — scope by name or team; don't fetch every schedule fduty schedule list --query "SRE" --output-format toon +# or, for several: fduty schedule list --team-ids --output-format toon -# 2a. Current on-call for THIS schedule — a tiny now-window yields the live shift +# 2. Current on-call for each schedule — a tiny now-window yields the live shift fduty schedule info --start now --end +1h --output-format toon - -# 2b. Or the live on-call across ALL schedules in one call (legacy oncall group, -# being deprecated into schedule — fine for a quick global snapshot) -fduty oncall who --output-format toon ``` -Both return `person_ids` (integers), not names. Resolve every id in **one batch call** with `fduty person infos` (the sibling `person` group — takes positional ids or `--person-ids`): +`--output-format toon` is for reading, not piping — it is **not** jq-parseable. If you need to filter/extract with `jq`, use `--json` instead. + +`schedule info`'s on-call groups carry `person_ids` (integers), not names. Resolve every id in **one batch call** with `fduty person infos` (the sibling `person` group — takes positional ids or `--person-ids`): ```bash # person_ids come straight from the schedule/oncall output above @@ -109,6 +108,7 @@ Create schedule - `--start` string — Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. - `--team-id` int64 — Owning team ID. - body-only (`--data`): layers (array); notify (object) +- response: single object (`data` unwrapped to the top level) — fields: schedule_id (integer) ### delete [...] Delete schedules @@ -119,10 +119,12 @@ Get schedule info - `--end` string (required) — Preview end timestamp (Unix seconds, 10 digits). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. - `` (positional, required) int64 — Schedule ID. - `--start` string (required) — Preview start timestamp (Unix seconds, 10 digits). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); create_at (integer); create_by (integer); cur_oncall (object); description (any); disabled (any); end (integer); field (string); final_schedule (object); group_id (any); id (any); layer_schedules (array); layers (array); name (any); next_oncall (object); notify (object); schedule_id (integer); schedule_layers (array); schedule_name (any); start (integer); status (any); team_id (any); update_at (integer); update_by (integer) ### infos [...] Batch get schedules - `` (positional, required) intSlice — Schedule ID list. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); create_at (integer); create_by (integer); cur_oncall (object); description (any); disabled (any); end (integer); field (string); final_schedule (object); group_id (any); id (any); layer_schedules (array); layers (array); name (any); next_oncall (object); notify (object); schedule_id (integer); schedule_layers (array); schedule_name (any); start (integer); status (any); team_id (any); update_at (integer); update_by (integer) ### list List schedules @@ -135,6 +137,7 @@ List schedules - `--search-after-ctx` string - `--start` string — When set together with end, computed layer schedules are returned. Span must be less than 45 days. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. - `--team-ids` intSlice — Filter by team IDs. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); create_at (integer); create_by (integer); cur_oncall (object); description (any); disabled (any); end (integer); field (string); final_schedule (object); group_id (any); id (any); layer_schedules (array); layers (array); name (any); next_oncall (object); notify (object); schedule_id (integer); schedule_layers (array); schedule_name (any); start (integer); status (any); team_id (any); update_at (integer); update_by (integer) ### preview Preview schedule @@ -146,11 +149,13 @@ Preview schedule - `--start` string — Preview window start (Unix seconds, 10 digits). Required for /schedule/preview. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. - `--team-id` int64 — Owning team ID. - body-only (`--data`): layers (array); notify (object) +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); create_at (integer); create_by (integer); cur_oncall (object); description (any); disabled (any); end (integer); field (string); final_schedule (object); group_id (any); id (any); layer_schedules (array); layers (array); name (any); next_oncall (object); notify (object); schedule_id (integer); schedule_layers (array); schedule_name (any); start (integer); status (any); team_id (any); update_at (integer); update_by (integer) ### self List my schedules - `--end` string — Window end (Unix seconds, 10 digits). Must be within 30 days of start. Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. - `--start` string — Window start (Unix seconds, 10 digits). Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); create_at (integer); create_by (integer); cur_oncall (object); description (any); disabled (any); end (integer); field (string); final_schedule (object); group_id (any); id (any); layer_schedules (array); layers (array); name (any); next_oncall (object); notify (object); schedule_id (integer); schedule_layers (array); schedule_name (any); start (integer); status (any); team_id (any); update_at (integer); update_by (integer) ### update Update schedule @@ -182,7 +187,7 @@ Update schedule - **`list` without `--start`/`--end` omits computed shifts** — only schedule metadata is returned. Pass both flags (≤45 day span) to get rotation slots in the list response. - **`delete` is irreversible** — takes one or more `` positionals; double-check IDs before executing. - **`list` default page size is 10** — pass `--limit 100` when scanning all schedules. -- **Legacy `oncall who`:** `--team` does **not** filter server-side (any value returns the full list — scope by `--query ` instead), and an empty result is authoritative ("no one on call in that window") — report it, don't widen or fabricate a responder. The `oncall` group will be removed; don't depend on it. +- **Legacy `oncall who`:** returns ALL schedules in one call (77-82KB / ~2800 lines, always spills to a file) — prefer `schedule list --query|--team-ids` + per-schedule `info` instead. If you do use it: `--team` does **not** filter server-side (any value returns the full list — scope by `--query `), and an empty result is authoritative ("no one on call in that window") — report it, don't widen or fabricate a responder. The `oncall` group will be removed; don't depend on it. ## Worked example From 2af6f67f65e0a7bb5cc383462a5b1cff02eacb72 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 27 Jul 2026 04:10:47 -0700 Subject: [PATCH 4/7] fix(skilldoc): never assert single-object shape on a missed wrapper header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit responseShapeLine only recognized the items[]-wrapper shape by the literal substring "nested under items[]" in cligen's header. If that header wording ever drifts, the classification silently falls through to "single object" even though the sole top-level field is exactly one of cligen's own list-envelope wire names (items/docs/list, array type, per listEnvelope in internal/cmd/cligen/main.go) — asserting the one shape claim this feature exists to get right, wrong, with the generator's own authority behind it. Guard against that: when the fallback classification's sole field matches a known wrapper wire name, emit nothing instead of asserting "single object". A missing line is recoverable via --help; a confidently wrong one is not. Verified this guard is a no-op against every real command today (cligen's own header already says "wrapped" for every such case) — it only protects against future drift. Also generalizes the ground-truth cross-check from one hand-picked command (schedule list) to every real command that documents a Response fields block (~200), each rendered as an isolated single-command fence to avoid same-named-verb collisions across subgroups (e.g. "incident get" vs "incident war-room get" both heading "### get" in the full group fence). Classification drift anywhere now fails the build, not just for the one verb someone happened to write a test against. --- internal/cmd/skilldoc/main_test.go | 197 ++++++++++++++--------- internal/skilldoc/generate.go | 36 ++++- internal/skilldoc/response_shape_test.go | 78 +++++++++ 3 files changed, 235 insertions(+), 76 deletions(-) diff --git a/internal/cmd/skilldoc/main_test.go b/internal/cmd/skilldoc/main_test.go index 23dcf5b..758b59c 100644 --- a/internal/cmd/skilldoc/main_test.go +++ b/internal/cmd/skilldoc/main_test.go @@ -158,53 +158,16 @@ func TestRunGenAll_FillsEveryCardAndSkipsCardless(t *testing.T) { } } -// sectionFor returns the slice of a generated fence from "### " up to -// the next "### " heading (or end of string). Local copy of the helper -// skilldoc's own tests use — kept private to each package since exporting it -// would blur GenerateFence's real API surface just for test convenience. -func sectionFor(fence, verb string) string { - start := strings.Index(fence, "### "+verb) - if start < 0 { - return "" - } - rest := fence[start+len("### "+verb):] - if next := strings.Index(rest, "\n### "); next >= 0 { - return fence[start : start+len("### "+verb)+next] - } - return fence[start:] -} - -// TestGenerateFence_ScheduleList_ResponseShapeMatchesRealLong is the -// ground-truth cross-check the response-shape feature exists for. Before this -// change, skills/flashduty/reference/schedule.md carried zero envelope -// guidance for any of its verbs (unlike incident.md/change.md/automation.md/ -// enrichment.md/monit.md, the only 5 hand-written cards that happened to -// note their envelope shape). `schedule list` in particular is real, -// commonly-invoked, and — per the actual live CLI tree, not a fixture — -// documents an `{items: [...]}` page wrapper, NOT a bare top-level array -// (that phrasing belongs to the deprecated `oncall schedule list` twin, which -// calls the same SDK method but is a different command path with no card). -// -// The expected shape/fields below are derived by independently re-scanning -// that real Long text with throwaway logic — not by calling skilldoc's own -// extractor and not by pasting a literal expected string — so this fails if -// the generator's extraction ever silently drifts from what cligen actually -// wrote for this command. -func TestGenerateFence_ScheduleList_ResponseShapeMatchesRealLong(t *testing.T) { - d := dump() - - var long string - found := false - for _, c := range d.Commands { - if c.Path == "schedule list" { - long, found = c.Long, true - break - } - } - if !found { - t.Fatal("schedule list not found in the real CLI dump — has it been renamed?") - } - +// independentlyClassifyResponse re-derives a command's response envelope +// shape ("object" | "array" | "wrapped") and its top-level (or, for the +// wrapped shape, per-row) field names straight from its raw Long text, using +// logic deliberately NOT shared with skilldoc's own responseShapeLine +// extractor (internal/skilldoc/generate.go) — a from-scratch re-read of the +// same ground truth, not a call into the code under test. ok is false when +// Long documents no Response fields block, or the block yields zero fields +// at the target depth (skilldoc's own extractor also emits nothing for that +// case — nothing to cross-check). +func independentlyClassifyResponse(long string) (shape string, fields []string, ok bool) { lines := strings.Split(long, "\n") headerLine := -1 for i, l := range lines { @@ -214,44 +177,132 @@ func TestGenerateFence_ScheduleList_ResponseShapeMatchesRealLong(t *testing.T) { } } if headerLine < 0 { - t.Fatal("schedule list's real Long carries no Response fields block — has cligen's output changed?") + return "", nil, false } - if !strings.Contains(lines[headerLine], "nested under items[]") { - t.Fatalf("expected schedule list to be an items[]-wrapped page response; real header was:\n%s", lines[headerLine]) + + header := lines[headerLine] + switch { + case strings.Contains(header, "nested under items[]"): + shape = "wrapped" + case strings.Contains(header, "TOP-LEVEL array"): + shape = "array" + default: + shape = "object" + } + prefix := " - " + if shape == "wrapped" { + prefix = " - " // one level under the sole top-level "items" row } - var wantFields []string for _, l := range lines[headerLine+1:] { if strings.TrimSpace(l) == "" { break } - if strings.HasPrefix(l, " - ") { // one level under the sole top-level "items" row - name := strings.TrimPrefix(l, " - ") - if sp := strings.IndexAny(name, " ("); sp >= 0 { - name = name[:sp] - } - wantFields = append(wantFields, name) + if !strings.HasPrefix(l, prefix) { + continue } + name := strings.TrimPrefix(l, prefix) + if sp := strings.IndexAny(name, " ("); sp >= 0 { + name = name[:sp] + } + fields = append(fields, name) } - if len(wantFields) == 0 { - t.Fatal("independent scan of the real Long found no row fields under items — test logic is broken") - } + return shape, fields, len(fields) > 0 +} - fresh := skilldoc.GenerateFence(d, "schedule") - listSection := sectionFor(fresh, "list") - if listSection == "" { - t.Fatal("generated schedule fence has no `list` section") - } - if !strings.Contains(listSection, "page wrapper") || !strings.Contains(listSection, "jq '.items[]'") { - t.Errorf("schedule list card section must document the items[] page wrapper, got:\n%s", listSection) - } - if strings.Contains(listSection, "TOP-LEVEL array") { - t.Errorf("schedule list is NOT a top-level array — must not carry that phrasing:\n%s", listSection) +// isCligenWrapperWireName mirrors (independently — not by import) the three +// wire names cligen's own listEnvelope (internal/cmd/cligen/main.go) treats +// as a paginated-list envelope field. +func isCligenWrapperWireName(name string) bool { + return name == "items" || name == "docs" || name == "list" +} + +// responseLineOf returns the "- response: ..." bullet inside a rendered fence +// section, and whether one was present. +func responseLineOf(section string) (string, bool) { + for _, l := range strings.Split(section, "\n") { + if strings.HasPrefix(strings.TrimSpace(l), "- response: ") { + return l, true + } } - for _, f := range wantFields { - if !strings.Contains(listSection, f+" (") { - t.Errorf("schedule list card section missing real row field %q (from live Long):\n%s", f, listSection) + return "", false +} + +// TestGenerateFence_ResponseShapeMatchesRealLong_AllCommands is the +// coverage-complete ground-truth cross-check: for every real command whose +// live Long (built from the actual CLI tree, not a fixture) documents a +// Response fields block, independently reclassify its envelope shape and +// field names (independentlyClassifyResponse, above — separate logic from +// the generator) and assert the fence GenerateFence renders for that verb +// agrees. A single hand-picked example (`schedule list`) proved the +// mechanism works but only ever covered one of ~200 documented commands; +// this walks the whole real dump, so a classification drift ANYWHERE in the +// generator fails the build, not just for the one verb someone happened to +// write a test against. +func TestGenerateFence_ResponseShapeMatchesRealLong_AllCommands(t *testing.T) { + d := dump() + + checked := 0 + for _, c := range d.Commands { + wantShape, wantFields, hasBlock := independentlyClassifyResponse(c.Long) + if !hasBlock { + continue + } + checked++ + + // Render this ONE command in isolation (a single-command dump filtered + // to its own group) rather than slicing a section out of the whole + // group's fence: several real groups (e.g. "incident") flatten + // same-named leaves from different subgroups — "incident get" and + // "incident war-room get" both render as a "### get" heading — so a + // substring/heading search across the full group fence can grab the + // wrong command's section. A single-command fence has exactly one + // response line, unambiguously. + fence := skilldoc.GenerateFence(skilldoc.Dump{Commands: []skilldoc.Command{c}}, c.Group) + gotLine, hasLine := responseLineOf(fence) + + // Mirrors the wrapper-drift guard in responseShapeLine: a response + // this classifier reads as a top-level object whose sole field is one + // of cligen's own list-envelope wire names (items/docs/list, array + // type) is a case the generator deliberately suppresses rather than + // assert a possibly-wrong shape. No real command hits this today + // (cligen's own header would already say "wrapped" for it), but nothing + // here should hard-fail if drift ever makes one — that is the guard + // working as designed, not a bug. + if wantShape == "object" && len(wantFields) == 1 && isCligenWrapperWireName(wantFields[0]) { + if hasLine { + t.Errorf("%s: expected the wrapper-drift guard to suppress this line (sole field %q looks like a list envelope), got:\n%s", c.Path, wantFields[0], gotLine) + } + continue + } + + if !hasLine { + t.Errorf("%s: generated fence has no response line for a documented Response fields block", c.Path) + continue + } + switch wantShape { + case "wrapped": + if !strings.Contains(gotLine, "page wrapper") || !strings.Contains(gotLine, "jq '.items[]'") { + t.Errorf("%s: want items[] page wrapper, got:\n%s", c.Path, gotLine) + } + case "array": + if !strings.Contains(gotLine, "TOP-LEVEL array") { + t.Errorf("%s: want TOP-LEVEL array, got:\n%s", c.Path, gotLine) + } + default: // "object" + if !strings.Contains(gotLine, "single object") { + t.Errorf("%s: want single object, got:\n%s", c.Path, gotLine) + } } + for _, f := range wantFields { + if !strings.Contains(gotLine, f+" (") { + t.Errorf("%s: missing real field %q (from live Long) in generated line:\n%s", c.Path, f, gotLine) + } + } + } + if checked < 100 { + t.Fatalf("only cross-checked %d commands — expected on the order of 200; did Response-fields detection break, or has the real CLI shrunk?", checked) } + t.Logf("cross-checked response shape/fields for %d real commands", checked) } func TestRunGen_FillsFence(t *testing.T) { diff --git a/internal/skilldoc/generate.go b/internal/skilldoc/generate.go index 0d75678..91c8624 100644 --- a/internal/skilldoc/generate.go +++ b/internal/skilldoc/generate.go @@ -329,6 +329,16 @@ var responseHeaderRe = regexp.MustCompile(`^Response fields \((.*)\):$`) // " - person_ids (array) ...". Capture groups: indent, name, type. var responseFieldRe = regexp.MustCompile(`^( *)- ([a-zA-Z0-9_]+) \(([^)]*)\)`) +// wrapperWireNames are the exact wire names cligen's own listEnvelope +// (internal/cmd/cligen/main.go) treats as a paginated-list envelope field: +// a sole array-typed sibling named items, docs, or list. Mirrored here as a +// sanity check, not a duplicate classifier — see the guard in +// responseShapeLine below. +var wrapperWireNames = map[string]bool{"items": true, "docs": true, "list": true} + +// respField is one parsed Response-fields bullet row. +type respField struct{ name, typ string } + // responseShapeLine renders the one-line response-shape summary for a // command's Long, or "" when Long documents no Response fields block (mutation // verbs with an empty body, and a few hand-written commands that predate @@ -363,7 +373,7 @@ func responseShapeLine(long string) string { fieldIndent = " " // one level under the sole top-level "items" row } - var fields []string + var fields []respField for _, line := range lines[headerIdx+1:] { if strings.TrimSpace(line) == "" { break @@ -372,12 +382,28 @@ func responseShapeLine(long string) string { if m == nil || m[1] != fieldIndent { continue } - fields = append(fields, m[2]+" ("+m[3]+")") + fields = append(fields, respField{m[2], m[3]}) } if len(fields) == 0 { return "" } + // Safety net: this parser only ever recognizes the wrapped shape by the + // literal substring "nested under items[]" in the header (see the doc + // comment above the const block). If cligen's wording for that header + // ever drifts without this parser being updated to match, `wrapped` goes + // false here even though the response really is a page wrapper — and the + // sole top-level field is then exactly one of cligen's own wrapper wire + // names (items/docs/list, from listEnvelope in cligen/main.go), holding + // an array. Asserting "single object" in that case would be confidently + // WRONG about the one thing this whole feature exists to get right, so + // refuse to guess: say nothing rather than assert a shape we can no + // longer be sure of. A missing line is recoverable via `--help`; a + // wrong one isn't. + if !wrapped && len(fields) == 1 && wrapperWireNames[fields[0].name] && strings.HasPrefix(fields[0].typ, "array") { + return "" + } + var shape string switch { case wrapped: @@ -387,5 +413,9 @@ func responseShapeLine(long string) string { default: shape = "single object (`data` unwrapped to the top level)" } - return fmt.Sprintf("- response: %s — fields: %s\n", shape, strings.Join(fields, "; ")) + names := make([]string, len(fields)) + for i, f := range fields { + names[i] = f.name + " (" + f.typ + ")" + } + return fmt.Sprintf("- response: %s — fields: %s\n", shape, strings.Join(names, "; ")) } diff --git a/internal/skilldoc/response_shape_test.go b/internal/skilldoc/response_shape_test.go index 92c79fe..bed35dc 100644 --- a/internal/skilldoc/response_shape_test.go +++ b/internal/skilldoc/response_shape_test.go @@ -48,6 +48,45 @@ Request fields: --schedule-id int (required) — Schedule ID. ` +// driftedWrapperHeaderLong simulates a future cligen wording change to the +// items[]-wrapper header that no longer contains the literal substring +// "nested under items[]" this parser keys on (e.g. cligen's header text was +// reworded from "rows are nested under items[]" to "rows appear inside +// items[]"). The response is STILL genuinely items[]-wrapped — same shape as +// itemsWrappedShapeLong above, just described differently — so this must +// never be reported as "single object". +const driftedWrapperHeaderLong = `List schedules. + +Response fields ('data' envelope is unwrapped — rows appear inside items[]): + - items (array) (required) — Schedules on this page. + - schedule_id (integer) (required) — Schedule ID. +` + +// driftedWrapperHeaderDocsLong is the same drift scenario but with cligen's +// "docs" wire name (listEnvelope in internal/cmd/cligen/main.go recognizes +// items/docs/list interchangeably as a list-envelope field) instead of +// "items", to prove the guard isn't hardcoded to one wire name. +const driftedWrapperHeaderDocsLong = `List reports. + +Response fields ('data' envelope is unwrapped — rows appear inside docs[]): + - docs (array) (required) — Reports on this page. + - report_id (integer) (required) — Report ID. +` + +// singleArrayFieldObjectLong is a GENUINE single-object response (per +// cligen's own header, and correctly so — this mirrors the real +// Automations.RuleReadList shape, whose sole top-level field is "rules", not +// one of cligen's own list-envelope wire names) whose one top-level field +// happens to be an array. The guard must NOT suppress this: only the exact +// wrapper wire names (items/docs/list) are a drift signal, not "any object +// with one array field". +const singleArrayFieldObjectLong = `List automation rules. + +Response fields ('data' envelope is unwrapped — these fields are at the top level): + - rules (array) (required) — Automation rules. + - rule_id (string) (required) — Rule ID. +` + func TestResponseShapeLine_Object(t *testing.T) { got := responseShapeLine(objectShapeLong) if !strings.Contains(got, "single object") { @@ -112,6 +151,45 @@ func TestResponseShapeLine_NoBlockIsEmpty(t *testing.T) { } } +// TestResponseShapeLine_DriftedWrapperHeaderYieldsNothingNotWrongClaim covers +// the failure mode a reviewer traced: if cligen's items[]-wrapper header +// wording ever drifts away from the literal "nested under items[]" substring +// this parser matches on, the shape must NOT silently fall through to +// "single object" — that would be the exact confidently-wrong claim this +// whole feature exists to prevent. It must instead emit nothing. Covers both +// wrapper wire names cligen's listEnvelope recognizes: "items" and "docs". +func TestResponseShapeLine_DriftedWrapperHeaderYieldsNothingNotWrongClaim(t *testing.T) { + for _, tc := range []struct { + name string + long string + }{ + {"items", driftedWrapperHeaderLong}, + {"docs", driftedWrapperHeaderDocsLong}, + } { + t.Run(tc.name, func(t *testing.T) { + got := responseShapeLine(tc.long) + if got != "" { + t.Errorf("drifted wrapper header must yield no response line (not a false 'single object' claim), got:\n%s", got) + } + }) + } +} + +// TestResponseShapeLine_SingleArrayFieldObjectIsNotSuppressed proves the +// drift guard is scoped to cligen's own wrapper wire names (items/docs/list), +// not "any object shape with exactly one array field" — a real, correctly +// classified single-object response (mirroring Automations.RuleReadList, +// whose sole field is "rules") must still render normally. +func TestResponseShapeLine_SingleArrayFieldObjectIsNotSuppressed(t *testing.T) { + got := responseShapeLine(singleArrayFieldObjectLong) + if !strings.Contains(got, "single object") { + t.Errorf("a genuine single-object response with one array field must not be suppressed by the wrapper-drift guard:\n%s", got) + } + if !strings.Contains(got, "rules (array)") { + t.Errorf("missing the rules field:\n%s", got) + } +} + // TestGenerateFence_InjectsResponseShapePerVerb is the fence-level integration // check: a group with one verb of each shape must surface a "- response: " // line in that verb's own section, and a verb with no documented response From d78f22a15ec38ea8c2dbdd0fe9f36630677b5ea1 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 27 Jul 2026 04:10:55 -0700 Subject: [PATCH 5/7] docs: drop hand-written prose now redundant with generated response lines Five spots restated a verb's response envelope shape by hand, now fully covered by that verb's own generated "- response: ..." line: - monit.md: targets response-shape restatement removed; kept the updated_at="last seen, not online now" fact, which is real knowledge. - role.md: two bullets restating permission-list (items[]) and permission-factor-list (top-level array) shapes, both fully redundant. - team.md: infos and list gotchas trimmed to drop the shape restatement; kept the positional-args fact for infos. - channel.md: one bullet summarizing list + rule-list shapes collectively, now subsumed by the per-verb generated lines. - monit-query.md: rows bullet trimmed to drop the shape assertion; kept the values/fields semantics and the __value__ canonical key fact. --- skills/flashduty/reference/channel.md | 1 - skills/flashduty/reference/monit-query.md | 2 +- skills/flashduty/reference/monit.md | 2 +- skills/flashduty/reference/role.md | 2 -- skills/flashduty/reference/team.md | 3 +-- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/skills/flashduty/reference/channel.md b/skills/flashduty/reference/channel.md index 785f9d9..82f98a4 100644 --- a/skills/flashduty/reference/channel.md +++ b/skills/flashduty/reference/channel.md @@ -310,7 +310,6 @@ Update channel - **`channel create` requires `--channel-name` and `--team-id`** even though they are not marked `required` in the flag list — the server rejects the request without them. - **`delete` on a channel is irreversible** — all rules within it are also removed. Confirm the `channel-id` against `list` before proceeding. - **Empty rule list is authoritative** — if `escalate-rule-list` / `silence-rule-list` / etc. returns no rows, no rules exist; do not widen the query. -- **`list` response is a top-level array** (pipe `jq '.[]'`); rule-list responses nest under `items[]` (pipe `jq '.items[]'`). ## Worked example — look up a channel and inspect its escalation policy diff --git a/skills/flashduty/reference/monit-query.md b/skills/flashduty/reference/monit-query.md index 3a36e8b..d0c17ec 100644 --- a/skills/flashduty/reference/monit-query.md +++ b/skills/flashduty/reference/monit-query.md @@ -52,7 +52,7 @@ Raw datasource passthrough (returns values/rows as the datasource itself would) ## Key concepts -- **`rows` = raw passthrough.** Response `data` is a **top-level array** of row objects — pipe `jq '.[]'`, NOT `.items[]`. Numeric fields under `values` (metric canonical key `__value__`); labels/columns under `fields`. **Time belongs in the query expression**, not in flags. +- **`rows` = raw passthrough.** Numeric fields under `values` (metric canonical key `__value__`); labels/columns under `fields`. **Time belongs in the query expression**, not in flags. - **`diagnose` = pre-clustered evidence.** Its versioned response echoes the datasource, query, and RFC 3339 analysis window. Each result contains method-specific `pattern_evidence` (logs) or `series_evidence` (metrics), structured window statistics, and observations; log results also declare redaction and untrusted observed-data paths in `data_handling`. Takes `--time-start` / `--time-end` (relative like `-1h`, `now`, or unix seconds). ## Gotchas diff --git a/skills/flashduty/reference/monit.md b/skills/flashduty/reference/monit.md index d2d0442..eec1f0c 100644 --- a/skills/flashduty/reference/monit.md +++ b/skills/flashduty/reference/monit.md @@ -374,7 +374,7 @@ Invoke target tools **`query-diagnose` output**: results are versioned evidence, not the former summary-only pattern/series lists. Read `pattern_evidence` for logs or `series_evidence` for metrics; their optional comparison fields are absent when the edge has no evidence. Log output also includes `data_handling`, which declares redaction coverage and paths carrying untrusted observed data. -**`targets` response shape** — rows are under `items[]` (not `data[]`); pipe `jq '.items[]'`, not `jq '.[]'`. `updated_at` means "last seen", not "online now". +**`targets`**: `updated_at` means "last seen", not "online now". ## Gotchas diff --git a/skills/flashduty/reference/role.md b/skills/flashduty/reference/role.md index 041c1a4..c0c6d45 100644 --- a/skills/flashduty/reference/role.md +++ b/skills/flashduty/reference/role.md @@ -118,8 +118,6 @@ Create or update a role - **`permission-id` vs `permission-factor`**: `permission-list` returns coarse permission objects (id, name, class, scope, type=read|manage) — use these ids in `upsert --permission-ids`. `permission-factor-list` returns fine-grained factors (api/button/menu/url/visit strings like `template:read:info`) — useful for auditing what a permission covers, but not accepted by `upsert`. - **`permission-list --with-all`**: returns every permission in the system with `is_granted=true/false` for the requested `--role-ids`. Omit `--role-ids` + `--with-all` to see the full catalog without annotation. -- **`permission-list` response shape**: rows are under `items[]` — pipe `jq '.items[]'`, NOT `.data.items[]`. -- **`permission-factor-list` response shape**: top-level array — pipe `jq '.[]'`, NOT `.items[]`. ## Gotchas diff --git a/skills/flashduty/reference/team.md b/skills/flashduty/reference/team.md index d538815..e12dce8 100644 --- a/skills/flashduty/reference/team.md +++ b/skills/flashduty/reference/team.md @@ -117,7 +117,7 @@ Create or update a team ## Key concepts - **`status`** on `team list` rows: `enabled` | `disabled`. A disabled team still exists but is excluded from most operational contexts. -- **`infos [...]`** — takes team IDs as **positional args** (space-separated), not `--team-ids`. The response wraps under `items[]` (pipe `jq '.items[]'` with `--json`), NOT `.data.items[]`. +- **`infos [...]`** — takes team IDs as **positional args** (space-separated), not `--team-ids`. - **`upsert` lookup key** — matched by `--team-id` (if non-zero) or by `--team-name` (name collision). Pass `--reset-if-name-exist` to overwrite membership on a name match; omit it to leave the existing members untouched. ## Gotchas @@ -126,7 +126,6 @@ Create or update a team - **`get` vs `info`** — both fetch a single team; `get` accepts `--id`/`--name`/`--ref-id`; `get []` also allows the ID as a positional arg. `info` uses `--team-id`/`--team-name`/`--ref-id` flags only. Prefer `get` for interactive lookup. - **`delete` is irreversible** and requires confirmation unless `--force` is set. Always confirm the correct `--id` (not `--name`) in scripts to avoid name-collision accidents. - **`infos` positional trap** — the `use` is `infos [...]`; IDs are space-separated positional args, not a flag. `fduty team infos 101 102 103`, not `--team-ids 101,102,103`. -- **`list` JSON shape** — `--json` returns a top-level array; pipe `jq '.[]'`, NOT `.items[]`. - **`upsert` requires `--team-name`** even when updating by `--team-id`; omitting it returns a validation error. ## Worked example From 1ca6402fe394dde28635246a0ff9d0adf871738d Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 27 Jul 2026 04:51:34 -0700 Subject: [PATCH 6/7] docs(skilldoc): regenerate the incident fence with response-shape lines --- skills/flashduty/reference/incident.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/skills/flashduty/reference/incident.md b/skills/flashduty/reference/incident.md index c339bad..9ddfe32 100644 --- a/skills/flashduty/reference/incident.md +++ b/skills/flashduty/reference/incident.md @@ -140,10 +140,12 @@ List alerts of incident - `--limit` int64 — Page size, at most 1000. (0-1000) - `--page` int64 — Page number starting at 1. (min 0) - `--search-after-ctx` string +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); alert_id (string); alert_key (string); alert_severity (string); alert_status (string); channel_id (integer); channel_name (string); channel_status (string); created_at (integer); data_source_id (integer); data_source_name (string); data_source_ref_id (string); data_source_type (string); deleted_at (integer); description (string); end_time (integer); event_cnt (integer); events (array); ever_muted (boolean); images (array); incident (object); integration_id (integer); integration_name (string); integration_ref_id (string); integration_type (string); labels (object); last_time (integer); responder_email (string); responder_name (string); start_time (integer); title (string); title_rule (string); updated_at (integer) ### alerts View incident alerts - `--limit` int +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); alert_id (string); alert_key (string); alert_severity (string); alert_status (string); channel_id (integer); channel_name (string); channel_status (string); created_at (integer); data_source_id (integer); data_source_name (string); data_source_ref_id (string); data_source_type (string); deleted_at (integer); description (string); end_time (integer); event_cnt (integer); events (array); ever_muted (boolean); images (array); incident (object); integration_id (integer); integration_name (string); integration_ref_id (string); integration_type (string); labels (object); last_time (integer); responder_email (string); responder_name (string); start_time (integer); title (string); title_rule (string); updated_at (integer) ### assign Assign incident @@ -171,10 +173,12 @@ Create a new incident Execute custom action - `--incident-id` string (required) — Incident ID (MongoDB ObjectID). - `--integration-id` int64 (required) — Custom action integration ID. Must be enabled and associated with the incident's channel. +- response: single object (`data` unwrapped to the top level) — fields: message (string) ### detail View full incident detail with AI summary - `--fields` string +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); account_locale (string); account_name (string); account_time_zone (string); ack_time (integer); active_alert_cnt (integer); ai_summary (string); alert_cnt (integer); alert_event_cnt (integer); alerts (array); assigned_to (object); channel_id (integer); channel_name (string); channel_status (string); close_time (integer); closer (object); closer_id (integer); created_at (integer); creator (object); creator_id (integer); data_source_id (integer); data_source_ids (array); data_source_type (string); data_source_types (array); dedup_key (string); deleted_at (integer); description (string); detail_url (string); end_time (integer); equals_md5 (string); ever_muted (boolean); fields (object); frequency (string); group_method (string); images (array); impact (string); incident_id (string); incident_severity (string); incident_status (string); integration_id (integer); integration_ids (array); integration_type (string); integration_types (array); labels (object); last_time (integer); links (array); manual_overrides (array); num (string); owner (object); owner_id (integer); post_mortem_id (string); progress (string); reporter_email (string); resolution (string); responders (array); root_cause (string); silence_url (string); snoozed_before (integer); start_time (integer); title (string); updated_at (integer) ### disable-merge [...] Disable incident merge @@ -184,6 +188,7 @@ Disable incident merge View incident feed (paginated timeline) - `--limit` int - `--page` int +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); created_at (integer); creator_id (integer); deleted_at (integer); detail (object); ref_id (string); type (string); updated_at (integer) ### field-reset Update incident custom field @@ -193,11 +198,13 @@ Update incident custom field ### get [ ...] Get incident details +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); account_locale (string); account_name (string); account_time_zone (string); ack_time (integer); active_alert_cnt (integer); ai_summary (string); alert_cnt (integer); alert_event_cnt (integer); alerts (array); assigned_to (object); channel_id (integer); channel_name (string); channel_status (string); close_time (integer); closer (object); closer_id (integer); created_at (integer); creator (object); creator_id (integer); data_source_id (integer); data_source_ids (array); data_source_type (string); data_source_types (array); dedup_key (string); deleted_at (integer); description (string); detail_url (string); end_time (integer); equals_md5 (string); ever_muted (boolean); fields (object); frequency (string); group_method (string); images (array); impact (string); incident_id (string); incident_severity (string); incident_status (string); integration_id (integer); integration_ids (array); integration_type (string); integration_types (array); labels (object); last_time (integer); links (array); manual_overrides (array); num (string); owner (object); owner_id (integer); post_mortem_id (string); progress (string); reporter_email (string); resolution (string); responders (array); root_cause (string); silence_url (string); snoozed_before (integer); start_time (integer); title (string); updated_at (integer) ### info [] Get incident detail - `--incident-id` string — Incident ID (MongoDB ObjectID). - `--num` string — Short incident ID (the 6-character uppercased id shown in the UI). Not unique — resolves to the most recent match. Supply either incident_id or num. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); account_locale (string); account_name (string); account_time_zone (string); ack_time (integer); active_alert_cnt (integer); ai_summary (string); alert_cnt (integer); alert_event_cnt (integer); alerts (array); assigned_to (object); channel_id (integer); channel_name (string); channel_status (string); close_time (integer); closer (object); closer_id (integer); created_at (integer); creator (object); creator_id (integer); data_source_id (integer); data_source_ids (array); data_source_type (string); data_source_types (array); dedup_key (string); deleted_at (integer); description (string); detail_url (string); end_time (integer); equals_md5 (string); ever_muted (boolean); fields (object); frequency (string); group_method (string); images (array); impact (string); incident_id (string); incident_severity (string); incident_status (string); integration_id (integer); integration_ids (array); integration_type (string); integration_types (array); labels (object); last_time (integer); links (array); manual_overrides (array); num (string); owner (object); owner_id (integer); post_mortem_id (string); progress (string); reporter_email (string); resolution (string); responders (array); root_cause (string); silence_url (string); snoozed_before (integer); start_time (integer); title (string); updated_at (integer) ### list List incidents @@ -211,10 +218,12 @@ List incidents - `--severity` string - `--since` string - `--until` string +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); account_locale (string); account_name (string); account_time_zone (string); ack_time (integer); active_alert_cnt (integer); ai_summary (string); alert_cnt (integer); alert_event_cnt (integer); alerts (array); assigned_to (object); channel_id (integer); channel_name (string); channel_status (string); close_time (integer); closer (object); closer_id (integer); created_at (integer); creator (object); creator_id (integer); data_source_id (integer); data_source_ids (array); data_source_type (string); data_source_types (array); dedup_key (string); deleted_at (integer); description (string); detail_url (string); end_time (integer); equals_md5 (string); ever_muted (boolean); fields (object); frequency (string); group_method (string); images (array); impact (string); incident_id (string); incident_severity (string); incident_status (string); integration_id (integer); integration_ids (array); integration_type (string); integration_types (array); labels (object); last_time (integer); links (array); manual_overrides (array); num (string); owner (object); owner_id (integer); post_mortem_id (string); progress (string); reporter_email (string); resolution (string); responders (array); root_cause (string); silence_url (string); snoozed_before (integer); start_time (integer); title (string); updated_at (integer) ### list-by-ids [...] List incidents by IDs - `` (positional, required) stringSlice — Incident IDs to fetch. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); account_locale (string); account_name (string); account_time_zone (string); ack_time (integer); active_alert_cnt (integer); ai_summary (string); alert_cnt (integer); alert_event_cnt (integer); alerts (array); assigned_to (object); channel_id (integer); channel_name (string); channel_status (string); close_time (integer); closer (object); closer_id (integer); created_at (integer); creator (object); creator_id (integer); data_source_id (integer); data_source_ids (array); data_source_type (string); data_source_types (array); dedup_key (string); deleted_at (integer); description (string); detail_url (string); end_time (integer); equals_md5 (string); ever_muted (boolean); fields (object); frequency (string); group_method (string); images (array); impact (string); incident_id (string); incident_severity (string); incident_status (string); integration_id (integer); integration_ids (array); integration_type (string); integration_types (array); labels (object); last_time (integer); links (array); manual_overrides (array); num (string); owner (object); owner_id (integer); post_mortem_id (string); progress (string); reporter_email (string); resolution (string); responders (array); root_cause (string); silence_url (string); snoozed_before (integer); start_time (integer); title (string); updated_at (integer) ### merge Merge incidents into a target incident @@ -224,6 +233,7 @@ Merge incidents into a target incident List past incidents - `` (positional, required) string — Reference incident ID (MongoDB ObjectID). - `--limit` int64 — Maximum number of similar incidents to return. (0-100) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); account_locale (string); account_name (string); account_time_zone (string); ack_time (integer); active_alert_cnt (integer); ai_summary (string); alert_cnt (integer); alert_event_cnt (integer); alerts (array); assigned_to (object); channel_id (integer); channel_name (string); channel_status (string); close_time (integer); closer (object); closer_id (integer); created_at (integer); creator (object); creator_id (integer); data_source_id (integer); data_source_ids (array); data_source_type (string); data_source_types (array); dedup_key (string); deleted_at (integer); description (string); detail_url (string); end_time (integer); equals_md5 (string); ever_muted (boolean); fields (object); frequency (string); group_method (string); images (array); impact (string); incident_id (string); incident_severity (string); incident_status (string); integration_id (integer); integration_ids (array); integration_type (string); integration_types (array); labels (object); last_time (integer); links (array); manual_overrides (array); num (string); owner (object); owner_id (integer); post_mortem_id (string); progress (string); reporter_email (string); resolution (string); responders (array); root_cause (string); score (number); silence_url (string); snoozed_before (integer); start_time (integer); title (string); updated_at (integer) ### post-mortem-basics-reset Update post-mortem basics @@ -246,11 +256,13 @@ Update post-mortem follow-ups ### post-mortem-info Get post-mortem - `` (positional, required) string — Post-mortem ID. Deterministic hash derived from account ID and the set of linked incident IDs. +- response: single object (`data` unwrapped to the top level) — fields: basics (object); content (object); follow_ups (string); meta (object) ### post-mortem-init [...] Initialize post-mortem - `` (positional, required) stringSlice — Incident IDs to link to the report. 1-10 incidents. - `--template-id` string (required) — Template ID used to initialize the report. +- response: single object (`data` unwrapped to the top level) — fields: basics (object); content (object); follow_ups (string); meta (object) ### post-mortem-list List post-mortems @@ -264,6 +276,7 @@ List post-mortems - `--search-after-ctx` string — Cursor from a previous response for forward pagination. - `--status` string — Report status. Defaults to 'published' on the server when omitted. · enum: drafting | published - `--team-ids` intSlice — Team IDs to restrict the query to. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); author_ids (array); channel_id (integer); channel_name (string); created_at_seconds (integer); incident_ids (array); is_private (boolean); media_count (integer); post_mortem_id (string); status (string); team_id (integer); template_id (string); title (string); updated_at_seconds (integer) ### post-mortem-status-reset Update post-mortem status @@ -277,6 +290,7 @@ Delete post-mortem template ### post-mortem-template-info Get post-mortem template detail - `` (positional, required) string — Template ID. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); content (string); content_markdown (string); created_at_seconds (integer); description (string); name (string); team_id (integer); template_id (string); updated_at_seconds (integer) ### post-mortem-template-list List post-mortem templates @@ -285,6 +299,7 @@ List post-mortem templates - `--order-by` string — Field used to order results. · enum: created_at_seconds - `--page` int64 — Page number starting at 1. (min 0) - `--search-after-ctx` string — Cursor from a previous response for forward pagination. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); content (string); content_markdown (string); created_at_seconds (integer); description (string); name (string); team_id (integer); template_id (string); updated_at_seconds (integer) ### post-mortem-template-upsert Create or update post-mortem template @@ -294,6 +309,7 @@ Create or update post-mortem template - `--name` string (required) — Template name. - `--team-id` int64 — Managing team ID. Required when creating a custom template. - `--template-id` string — Template ID. Omit to create a new template; provide it to update an existing template. +- response: single object (`data` unwrapped to the top level) — fields: account_id (integer); content (string); content_markdown (string); created_at_seconds (integer); description (string); name (string); team_id (integer); template_id (string); updated_at_seconds (integer) ### post-mortem-title-reset Update post-mortem title @@ -342,6 +358,7 @@ Add incident responder Find similar incidents - `--fields` string - `--limit` int +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); account_locale (string); account_name (string); account_time_zone (string); ack_time (integer); active_alert_cnt (integer); ai_summary (string); alert_cnt (integer); alert_event_cnt (integer); alerts (array); assigned_to (object); channel_id (integer); channel_name (string); channel_status (string); close_time (integer); closer (object); closer_id (integer); created_at (integer); creator (object); creator_id (integer); data_source_id (integer); data_source_ids (array); data_source_type (string); data_source_types (array); dedup_key (string); deleted_at (integer); description (string); detail_url (string); end_time (integer); equals_md5 (string); ever_muted (boolean); fields (object); frequency (string); group_method (string); images (array); impact (string); incident_id (string); incident_severity (string); incident_status (string); integration_id (integer); integration_ids (array); integration_type (string); integration_types (array); labels (object); last_time (integer); links (array); manual_overrides (array); num (string); owner (object); owner_id (integer); post_mortem_id (string); progress (string); reporter_email (string); resolution (string); responders (array); root_cause (string); score (number); silence_url (string); snoozed_before (integer); start_time (integer); title (string); updated_at (integer) ### snooze [ ...] Snooze incidents @@ -349,6 +366,7 @@ Snooze incidents ### timeline View incident timeline +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); created_at (integer); creator_id (integer); deleted_at (integer); detail (object); ref_id (string); type (string); updated_at (integer) ### unack [...] Unacknowledge incident @@ -378,6 +396,7 @@ Create an incident war room ### default-observers Preview historical responders for war-room observer invitation +- response: single object (`data` unwrapped to the top level) — fields: observers (array) ### delete Delete an incident war room @@ -387,10 +406,12 @@ Delete an incident war room ### get Get incident war room details - `--integration` int64 +- response: single object (`data` unwrapped to the top level) — fields: chat_id (string); chat_name (string); share_link (string) ### list List incident war rooms - `--integration` int64 +- response: TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`) — fields: account_id (integer); chat_id (string); created_at (integer); created_by (integer); incident_id (string); integration_id (integer); plugin_type (string); status (string) ### war-room-add-member Add war-room member @@ -404,10 +425,12 @@ Create war room - `--incident-id` string (required) — Incident ID (MongoDB ObjectID). - `--integration-id` int64 (required) — IM integration ID. Must have war room enabled; Feishu, DingTalk, WeCom (self-built), Slack and Teams are supported. - `--member-ids` intSlice — Additional member IDs to add to the war room. +- response: single object (`data` unwrapped to the top level) — fields: chat_id (string); chat_name (string); share_link (string) ### war-room-default-observers Get war-room default observers - `` (positional, required) string — Incident ID, a MongoDB ObjectID hex string. +- response: single object (`data` unwrapped to the top level) — fields: observers (array) ### war-room-delete Delete war room @@ -418,11 +441,13 @@ Delete war room Get war room detail - `` (positional, required) string — Chat/group ID on the IM side. - `--integration-id` int64 (required) — IM integration ID that hosts the war room. +- response: single object (`data` unwrapped to the top level) — fields: chat_id (string); chat_name (string); share_link (string) ### war-room-list List war rooms - `` (positional, required) string — Incident ID (MongoDB ObjectID). - `--integration-id` int64 — Optional filter: only return war rooms for this IM integration. +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: account_id (integer); chat_id (string); created_at (integer); created_by (integer); incident_id (string); integration_id (integer); plugin_type (string); status (string) From 1b29bc02ca25d3bf55176b1589bb91eedd3ebd2b Mon Sep 17 00:00:00 2001 From: ysyneu Date: Mon, 27 Jul 2026 05:26:16 -0700 Subject: [PATCH 7/7] docs(skills): bind report claims to the queried scope, not just the verb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SKILL.md's evidence-binding rule only checked whether the right verb ran at all this turn ("that exact command"). It said nothing about whether the call's time window or entity matched the claim being made, so a report could publish per-day/WoW figures derived from a single rolling `--since 1d` window, cite a baseline window no call ever touched, or generalize one entity's configuration from sibling entities' results — and still satisfy the rule as written. Generalizes the rule to bind evidence to scope (window/entity), not just verb, with the same "未查询 — 可运行 " fallback already proven in incident.md. Adds the insight-specific instance in insight.md's Gotchas: a day-over-day/WoW claim needs one call spanning every window compared, pointing to --aggregate-unit as the concrete mechanism. Locks both with property-based tests (assert the rule's properties, not a pinned sentence) in internal/skilldoc/report_evidence_binding_test.go. --- .../skilldoc/report_evidence_binding_test.go | 91 +++++++++++++++++++ skills/flashduty/SKILL.md | 2 +- skills/flashduty/reference/insight.md | 1 + 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 internal/skilldoc/report_evidence_binding_test.go diff --git a/internal/skilldoc/report_evidence_binding_test.go b/internal/skilldoc/report_evidence_binding_test.go new file mode 100644 index 0000000..42ccd05 --- /dev/null +++ b/internal/skilldoc/report_evidence_binding_test.go @@ -0,0 +1,91 @@ +package skilldoc + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestSkillCardBindsEvidenceToScopeNotJustVerb locks in the generalized +// evidence-binding rule: a claim about a specific time window or entity may +// only be made if a tool call this turn actually covered that window or +// entity, not merely the same verb. This patches the gap behind three +// production failures — reporting per-day/WoW figures from a rolling-window +// call, inventing a baseline window no call touched, and generalizing one +// entity's config from its siblings' — all of which passed the older, +// narrower "did you run the command at all" check. +func TestSkillCardBindsEvidenceToScopeNotJustVerb(t *testing.T) { + card, err := os.ReadFile(filepath.Join("..", "..", "skills", "flashduty", "SKILL.md")) + if err != nil { + t.Fatal(err) + } + body := string(card) + + _, section, found := strings.Cut(body, "## Output — prefer `toon`") + if !found { + t.Fatal("SKILL.md is missing the Output section that carries the evidence-binding rule") + } + section, _, found = strings.Cut(section, "## Command names") + if !found { + t.Fatal("SKILL.md is missing the section after Output — prefer `toon`") + } + + // The rule must bind a claim to the scope actually queried (time window, + // entity) — not just to having run the right verb at some point this + // turn. A call for one window or entity must not license a claim about + // a different one. + if !strings.Contains(section, "scope") { + t.Error("SKILL.md evidence-binding rule must talk about matching the queried scope, not just the verb") + } + if !strings.Contains(section, "window") || !strings.Contains(section, "entity") { + t.Error("SKILL.md evidence-binding rule must name both axes it covers: time window and entity") + } + // It must forbid generalizing from adjacent evidence (a wider/different + // window, a sibling entity) — the specific failure mode this rule exists + // to stop, not just "don't invent from nothing". + if !strings.Contains(section, "does not transfer") && !strings.Contains(section, "extrapolat") { + t.Error("SKILL.md evidence-binding rule must forbid extrapolating a claim from a window or entity you queried differently") + } + // It must give a concrete fallback action, mirroring incident.md's + // established phrasing, not just a prohibition with nothing to do + // instead. + if !strings.Contains(section, "未查询") || !strings.Contains(section, "") { + t.Error("SKILL.md evidence-binding rule must give a concrete fallback action (未查询 — 可运行 ), not just a prohibition") + } +} + +// TestInsightCardTiesWindowComparisonsToSkillRule locks in the insight-card +// instance of the same rule: day-over-day / week-over-week claims require a +// single call spanning every window compared, with --aggregate-unit as the +// concrete way to get one. This is the domain-specific reinforcement, not a +// duplicate of the general SKILL.md rule. +func TestInsightCardTiesWindowComparisonsToSkillRule(t *testing.T) { + card, err := os.ReadFile(filepath.Join("..", "..", "skills", "flashduty", "reference", "insight.md")) + if err != nil { + t.Fatal(err) + } + body := string(card) + + _, gotchas, found := strings.Cut(body, "## Gotchas") + if !found { + t.Fatal("insight card is missing the Gotchas section") + } + gotchas, _, found = strings.Cut(gotchas, "## Worked example") + if !found { + t.Fatal("insight card is missing the section after Gotchas") + } + + if !strings.Contains(gotchas, "window") { + t.Error("insight Gotchas must warn that a window-over-window claim needs a call spanning every window compared") + } + if !strings.Contains(gotchas, "--aggregate-unit") { + t.Error("insight Gotchas must point to --aggregate-unit as the concrete way to get real per-bucket figures in one call") + } + // It should point back to the general rule rather than re-deriving it — + // SKILL.md and insight.md must not carry two competing versions of the + // same rule. + if !strings.Contains(gotchas, "SKILL.md") { + t.Error("insight Gotchas should reference the general SKILL.md evidence-binding rule instead of restating it") + } +} diff --git a/skills/flashduty/SKILL.md b/skills/flashduty/SKILL.md index 0ca9823..cb2ea26 100644 --- a/skills/flashduty/SKILL.md +++ b/skills/flashduty/SKILL.md @@ -31,7 +31,7 @@ Append `--output-format toon` to read commands: it drops the per-row repeated ke **Empty result = authoritative not-found.** A filter returning `[]` means no such entity in scope — report it (optionally the 1–2 closest names) and stop. Do **not** brute-force (no shifted-keyword re-queries, no widening past caps, no full-dump grep). Never infer "feature not enabled" from an empty list, and never fabricate data absent from tool output. -**A result you did not fetch is "unknown", never "empty".** You may report a command's result — including "returned empty" or any count/list/finding — **only if that exact command appears in your tool-call history this turn**. If you did not run it, the honest answer is "未查询 / not queried", followed by the command to run. Writing "`incident similar` 返回空" or "无变更" for a command you never executed is fabrication, not a summary. +**A result you did not fetch is "unknown", never "empty" — and "fetched" means the same scope, not just the same verb.** You may report a command's result for a given window, entity, or aspect — including "returned empty" or any count/list/finding — **only if a call covering that exact scope appears in your tool-call history this turn**. A wider or different time window, or a sibling entity's result, does not transfer: extrapolating from what you did fetch is the same fabrication as skipping the fetch. If the scope wasn't queried, the honest answer is "未查询 — 可运行 ", not a filled-in number or a generalized claim. ## Command names — don't guess, read the card diff --git a/skills/flashduty/reference/insight.md b/skills/flashduty/reference/insight.md index 5222c31..267570e 100644 --- a/skills/flashduty/reference/insight.md +++ b/skills/flashduty/reference/insight.md @@ -355,6 +355,7 @@ Both families accept: relative duration (`30d`, `24h`), `now`, `+7d`, a date, or - **`insight incidents` and `incident-list` are siblings**, not the same. `incidents` uses `--since`/`--until`, paginates, and is token-light. `incident-list` uses `--start-time`/`--end-time`, adds `--severities`/`--responder-ids`/`--query`/cursor (`--search-after-ctx`), and is the filterable variant. - **All `insight` commands hit the OLAP backend.** HTTP 500 means the backend is down — report it, do not retry. - **Empty result is authoritative.** A zero-row response means no matching data for that scope/window — do not widen filters or re-query with shifted keywords. +- **A window-over-window claim (day-over-day, WoW) needs one call spanning every window compared.** A `--since 1d` / narrow `--start-time` call only proves that one range — reporting other days, or a delta against a prior period no call touched, is the SKILL.md "未查询" fabrication, not a finding. Use `--aggregate-unit day|week|month` on a `--start-time`/`--end-time` range wide enough to cover every window you're comparing — one call, bucketed, so every number is real. - **`--aggregate-unit`** (on `account`, `alert-topk-by-label`, `channel`, `responder`, `team` and their exports) splits results into time buckets: `day` / `week` / `month`. When set, the window must span ≥24 h; `day` additionally caps the range at 31 days. - **`top-alerts` and `alert-topk-by-label` return the same top-K breakdown** (`label`, `hours`, `total_alert_cnt`, `total_alert_event_cnt`) — `alert-topk-by-label` is the superset (adds `--team-ids`/`--channel-ids`/severity filters and `--start-time`/`--end-time`). Pick one; don't call both for the same question. - **`responder` has no `--limit`/`--page`** — one call returns every responder's rollup for the account. For account-wide load analysis, use that single response; don't cap it and re-fetch for the rest.