From 527bc793cb9edac5e3632289e6da94be7c733c60 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Tue, 28 Jul 2026 05:48:20 -0700 Subject: [PATCH 1/3] feat(skilldoc): dedupe byte-identical response-shape lines within a card Several commands in the same group (create/get/update/enable/disable, ...) commonly return the same resource, so the generated "- response: ..." field list was repeated verbatim once per command inside a card's GENERATED fence -- up to several hundred bytes each, and up to a few KB per card. An agent reading the card paid that token cost on every read. Within one GENERATED group, only the first command to emit a given response shape keeps the full field list; every later command whose shape is byte-identical instead emits a short reference to it (e.g. "same shape as `get ` above"). Distinct shapes are left untouched, and the dedup map is scoped per card so cards stay self-contained -- no card ever references another card's command. --- internal/skilldoc/generate.go | 44 +++++++++++---- internal/skilldoc/response_shape_test.go | 65 +++++++++++++++++++++++ skills/flashduty/reference/alert.md | 2 +- skills/flashduty/reference/automation.md | 4 +- skills/flashduty/reference/channel.md | 6 +-- skills/flashduty/reference/incident.md | 16 +++--- skills/flashduty/reference/insight.md | 4 +- skills/flashduty/reference/monit.md | 18 +++---- skills/flashduty/reference/rum.md | 4 +- skills/flashduty/reference/safari.md | 12 ++--- skills/flashduty/reference/schedule.md | 6 +-- skills/flashduty/reference/status-page.md | 4 +- skills/flashduty/reference/team.md | 2 +- skills/flashduty/reference/template.md | 4 +- 14 files changed, 141 insertions(+), 50 deletions(-) diff --git a/internal/skilldoc/generate.go b/internal/skilldoc/generate.go index 91c8624..260fb68 100644 --- a/internal/skilldoc/generate.go +++ b/internal/skilldoc/generate.go @@ -31,11 +31,17 @@ func GenerateFence(d Dump, group string) string { var b strings.Builder fmt.Fprintf(&b, fenceStartFmt+"\n\n", group) + // seenShapes maps a verbatim response-shape line to the name of the first + // command in THIS group (only — never across cards) that rendered it in + // full, so a later command with a byte-identical shape can point back at + // it instead of repeating the field list. Scoped to one GenerateFence call, + // so cards stay self-contained. + seenShapes := map[string]string{} for i, c := range cmds { if i > 0 { b.WriteString("\n") } - writeCommand(&b, c) + writeCommand(&b, c, seenShapes) } fmt.Fprintf(&b, "\n"+fenceEndFmt, group) return b.String() @@ -57,18 +63,14 @@ func groupCommands(d Dump, group string) []Command { return cmds } -func writeCommand(b *strings.Builder, c Command) { - verb := verbOf(c.Path) +func writeCommand(b *strings.Builder, c Command, seenShapes map[string]string) { + name := commandName(c) positionals := positionalsOf(c.Use) // Heading carries the positional signature verbatim from Use (authoritative), // e.g. "change-active-list ", so the reader sees the exact argument // order the binary requires. - if len(positionals) > 0 { - fmt.Fprintf(b, "### %s %s\n", verb, strings.Join(positionals, " ")) - } else { - fmt.Fprintf(b, "### %s\n", verb) - } + fmt.Fprintf(b, "### %s\n", name) if c.Short != "" { fmt.Fprintf(b, "%s\n", c.Short) } @@ -90,8 +92,32 @@ func writeCommand(b *strings.Builder, c Command) { fmt.Fprintf(b, "- body-only (`--data`): %s\n", strings.Join(fields.bodyOnly, "; ")) } if shape := responseShapeLine(c.Long); shape != "" { - b.WriteString(shape) + if first, ok := seenShapes[shape]; ok { + // A later command in the same group whose response is byte-identical + // to one already rendered in full — point back at it instead of + // repeating the field list. Several commands in one group commonly + // return the same resource (create/get/update/…), so this is the + // common case, not the exception. + fmt.Fprintf(b, "- response: same shape as `%s` above\n", first) + } else { + seenShapes[shape] = name + b.WriteString(shape) + } + } +} + +// commandName returns the heading text for a command: the leaf verb plus its +// positional signature verbatim from Use, e.g. "change-active-list " +// or, with no positional, just the verb. Also used as the referent name in a +// deduplicated response-shape line ("same shape as `` above"), so +// the reference always names exactly what the reader sees in that other +// section's own heading. +func commandName(c Command) string { + verb := verbOf(c.Path) + if positionals := positionalsOf(c.Use); len(positionals) > 0 { + return verb + " " + strings.Join(positionals, " ") } + return verb } // positionalsOf returns the placeholder tokens after the leaf verb in a Use diff --git a/internal/skilldoc/response_shape_test.go b/internal/skilldoc/response_shape_test.go index bed35dc..4eb1844 100644 --- a/internal/skilldoc/response_shape_test.go +++ b/internal/skilldoc/response_shape_test.go @@ -215,3 +215,68 @@ func TestGenerateFence_InjectsResponseShapePerVerb(t *testing.T) { t.Errorf("delete section must not fabricate a response line when Long documents none:\n%s", deleteSec) } } + +// TestGenerateFence_DedupesIdenticalResponseShapesWithinGroup covers the fence +// bloat a group of create/get/update-style verbs commonly produces: several +// commands documenting the exact same resource repeat an identical ~700-800 +// byte field list once per verb. The first occurrence must keep the full +// list; every later BYTE-IDENTICAL occurrence in the same group must instead +// reference the first by name; a genuinely different shape must never be +// touched. +func TestGenerateFence_DedupesIdenticalResponseShapesWithinGroup(t *testing.T) { + d := Dump{Commands: []Command{ + {Path: "widget create", Group: "widget", Short: "Create widget", Use: "create", Long: objectShapeLong}, + {Path: "widget get", Group: "widget", Short: "Get widget", Use: "get ", Long: objectShapeLong}, + {Path: "widget list", Group: "widget", Short: "List widgets", Use: "list", Long: itemsWrappedShapeLong}, + }} + out := GenerateFence(d, "widget") + + createSec := sectionFor(out, "create") + if !strings.Contains(createSec, "- response: single object") || !strings.Contains(createSec, "schedule_id (integer)") { + t.Errorf("first occurrence must keep the full field list:\n%s", createSec) + } + if strings.Contains(createSec, "same shape as") { + t.Errorf("first occurrence must not reference itself:\n%s", createSec) + } + + getSec := sectionFor(out, "get") + if !strings.Contains(getSec, "- response: same shape as `create` above") { + t.Errorf("later byte-identical shape must reference the first occurrence by name, got:\n%s", getSec) + } + if strings.Contains(getSec, "schedule_id (integer)") { + t.Errorf("deduped occurrence must not repeat the field list:\n%s", getSec) + } + + listSec := sectionFor(out, "list") + if !strings.Contains(listSec, "page wrapper") || !strings.Contains(listSec, "schedule_id (integer)") { + t.Errorf("a genuinely different shape must render in full, not be deduped:\n%s", listSec) + } + if strings.Contains(listSec, "same shape as") { + t.Errorf("a genuinely different shape must not be treated as a duplicate:\n%s", listSec) + } + + if out != GenerateFence(d, "widget") { + t.Errorf("GenerateFence dedup must be deterministic") + } +} + +// TestGenerateFence_ResponseShapeDedupIsPerGroupNotGlobal proves the dedup map +// is scoped to one GenerateFence call: two unrelated groups documenting the +// identical response shape must each render their own full field list in +// full — a card must stay self-contained and never reference a command in +// another card. +func TestGenerateFence_ResponseShapeDedupIsPerGroupNotGlobal(t *testing.T) { + d := Dump{Commands: []Command{ + {Path: "widget create", Group: "widget", Short: "Create widget", Use: "create", Long: objectShapeLong}, + {Path: "gadget create", Group: "gadget", Short: "Create gadget", Use: "create", Long: objectShapeLong}, + }} + + widgetSec := sectionFor(GenerateFence(d, "widget"), "create") + if !strings.Contains(widgetSec, "schedule_id (integer)") || strings.Contains(widgetSec, "same shape as") { + t.Errorf("widget's create must render its own full field list:\n%s", widgetSec) + } + gadgetSec := sectionFor(GenerateFence(d, "gadget"), "create") + if !strings.Contains(gadgetSec, "schedule_id (integer)") || strings.Contains(gadgetSec, "same shape as") { + t.Errorf("gadget's create must render its own full field list, not reference widget's card:\n%s", gadgetSec) + } +} diff --git a/skills/flashduty/reference/alert.md b/skills/flashduty/reference/alert.md index db6afe1..6f4b3ba 100644 --- a/skills/flashduty/reference/alert.md +++ b/skills/flashduty/reference/alert.md @@ -83,7 +83,7 @@ Get alert detail ### 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) +- response: same shape as `get ` above ### list List alerts diff --git a/skills/flashduty/reference/automation.md b/skills/flashduty/reference/automation.md index b576c81..5fc2186 100644 --- a/skills/flashduty/reference/automation.md +++ b/skills/flashduty/reference/automation.md @@ -132,7 +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) +- response: same shape as `create` above ### list List visible Automations @@ -177,7 +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) +- response: same shape as `create` above diff --git a/skills/flashduty/reference/channel.md b/skills/flashduty/reference/channel.md index 82f98a4..f17ee1a 100644 --- a/skills/flashduty/reference/channel.md +++ b/skills/flashduty/reference/channel.md @@ -155,7 +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) +- response: same shape as `escalate-rule-create` above ### inhibit-rule-delete Delete inhibit rule @@ -204,7 +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) +- response: same shape as `escalate-rule-create` above ### silence-rule-delete Delete silence rule @@ -244,7 +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) +- response: same shape as `escalate-rule-create` above ### unsubscribe-rule-delete Delete drop rule diff --git a/skills/flashduty/reference/incident.md b/skills/flashduty/reference/incident.md index 3a9fd54..9d82354 100644 --- a/skills/flashduty/reference/incident.md +++ b/skills/flashduty/reference/incident.md @@ -204,7 +204,7 @@ Get incident details 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) +- response: same shape as `detail ` above ### list List incidents @@ -218,7 +218,7 @@ 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) +- response: same shape as `get [ ...]` above ### list-by-ids [...] List incidents by IDs @@ -269,7 +269,7 @@ Get post-mortem 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) +- response: same shape as `post-mortem-info ` above ### post-mortem-list List post-mortems @@ -316,7 +316,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) +- response: same shape as `post-mortem-template-info ` above ### post-mortem-title-reset Update post-mortem title @@ -388,7 +388,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) +- response: same shape as `feed ` above ### unack [...] Unacknowledge incident @@ -447,12 +447,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) +- response: same shape as `get ` above ### 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) +- response: same shape as `default-observers ` above ### war-room-delete Delete war room @@ -463,7 +463,7 @@ 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) +- response: same shape as `get ` above ### war-room-list List war rooms diff --git a/skills/flashduty/reference/insight.md b/skills/flashduty/reference/insight.md index 267570e..9da8e3c 100644 --- a/skills/flashduty/reference/insight.md +++ b/skills/flashduty/reference/insight.md @@ -138,7 +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) +- response: same shape as `account` above ### channel-export Export channel insight @@ -298,7 +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) +- response: same shape as `account` above ### team-export Export team insight diff --git a/skills/flashduty/reference/monit.md b/skills/flashduty/reference/monit.md index 6ce31ac..3bc8c00 100644 --- a/skills/flashduty/reference/monit.md +++ b/skills/flashduty/reference/monit.md @@ -106,7 +106,7 @@ 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) +- response: same shape as `datasource-create` above ### datasource-list List datasources @@ -136,7 +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) +- response: same shape as `datasource-create` above ### preview-sync Preview datasource query @@ -240,7 +240,7 @@ Import alert rules ### 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) +- response: same shape as `rule-create` above ### rule-list-basic List alert rules @@ -251,12 +251,12 @@ List alert rules 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) +- response: same shape as `rule-import` above ### 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) +- response: same shape as `rule-counter-status` above ### rule-update Update alert rule @@ -283,7 +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) +- response: same shape as `rule-create` above ### rule-update-fields Batch update rule fields @@ -301,7 +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) +- response: same shape as `rule-import` above ### store-ruleset-create Create ruleset @@ -318,7 +318,7 @@ 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) +- response: same shape as `store-ruleset-create` above ### store-ruleset-list List rulesets @@ -331,7 +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) +- response: same shape as `store-ruleset-create` above ### targets List monitored targets diff --git a/skills/flashduty/reference/rum.md b/skills/flashduty/reference/rum.md index 80292ce..afd473a 100644 --- a/skills/flashduty/reference/rum.md +++ b/skills/flashduty/reference/rum.md @@ -89,7 +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) +- response: same shape as `application-infos [...]` above ### application-update Update application @@ -136,7 +136,7 @@ List RUM facet fields 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) +- response: same shape as `facet-list` above ### issue-info Get issue detail diff --git a/skills/flashduty/reference/safari.md b/skills/flashduty/reference/safari.md index 331a2e3..75882af 100644 --- a/skills/flashduty/reference/safari.md +++ b/skills/flashduty/reference/safari.md @@ -129,7 +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) +- response: same shape as `automation-rule-create` above ### automation-rule-list List Automation rules @@ -164,7 +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) +- response: same shape as `automation-rule-create` above ### automation-run-list List Automation runs @@ -226,7 +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) +- response: same shape as `mcp-server-create` above ### mcp-server-list List MCP servers @@ -259,7 +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) +- response: same shape as `mcp-server-create` above ### session-delete Delete session @@ -328,11 +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) +- response: same shape as `skill-get ` above ### 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) +- response: same shape as `skill-get ` above diff --git a/skills/flashduty/reference/schedule.md b/skills/flashduty/reference/schedule.md index c014e04..80a9f8e 100644 --- a/skills/flashduty/reference/schedule.md +++ b/skills/flashduty/reference/schedule.md @@ -137,7 +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) +- response: same shape as `infos [...]` above ### preview Preview schedule @@ -149,13 +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) +- response: same shape as `info ` above ### 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) +- response: same shape as `infos [...]` above ### update Update schedule diff --git a/skills/flashduty/reference/status-page.md b/skills/flashduty/reference/status-page.md index b83f670..2ce94ae 100644 --- a/skills/flashduty/reference/status-page.md +++ b/skills/flashduty/reference/status-page.md @@ -88,7 +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) +- response: same shape as `change-active-list ` above ### change-timeline-create Create event timeline entry @@ -171,7 +171,7 @@ 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) +- response: same shape as `migrate-email-subscribers` above ### migration-cancel Cancel status page migration diff --git a/skills/flashduty/reference/team.md b/skills/flashduty/reference/team.md index e12dce8..d36093f 100644 --- a/skills/flashduty/reference/team.md +++ b/skills/flashduty/reference/team.md @@ -73,7 +73,7 @@ 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) +- response: same shape as `get []` above ### infos [...] Batch get teams diff --git a/skills/flashduty/reference/template.md b/skills/flashduty/reference/template.md index 91c3d1b..d252bd5 100644 --- a/skills/flashduty/reference/template.md +++ b/skills/flashduty/reference/template.md @@ -95,7 +95,7 @@ Get the preset template for a channel ### 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); feishu_app_card_table_enabled (boolean); 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) +- response: same shape as `get-preset` above ### list List templates @@ -145,7 +145,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) +- response: same shape as `preview` above ### variables List available template variables From 237e0aba1682692ea60dc723b79d61ccb39d5bc0 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Tue, 28 Jul 2026 05:57:12 -0700 Subject: [PATCH 2/3] feat(skilldoc): name a page-wrapper response's pagination siblings The response-shape line for a `{items: [...]}` page-wrapper response only ever named the row fields nested under "items" -- it silently dropped the wrapper's OTHER top-level fields (total, has_next_page, search_after_ctx, and similar pagination metadata cligen's listEnvelope documents alongside "items"). A card would show a --search-after-ctx request flag for pagination but never say where the returned cursor actually lives in the response. Name those siblings in the wrapper descriptor itself, e.g.: - response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper -- ... -- items fields: ... ("items fields:" replaces "fields:" for this shape only, so the row list stays visually distinct from the siblings just named). Wrapper responses with no such siblings render unchanged. This also corrects two page-wrapper response instances (schedule `list`, rum `application-list`) that used to render as duplicates of an unrelated command's response purely because both had their real siblings dropped -- each now renders its own accurate shape. --- internal/skilldoc/generate.go | 59 ++++++++++++++++++---- internal/skilldoc/response_shape_test.go | 60 +++++++++++++++++++++++ skills/flashduty/reference/alert.md | 8 +-- skills/flashduty/reference/calendar.md | 4 +- skills/flashduty/reference/channel.md | 10 ++-- skills/flashduty/reference/enrichment.md | 8 +-- skills/flashduty/reference/incident.md | 14 +++--- skills/flashduty/reference/insight.md | 8 +-- skills/flashduty/reference/member.md | 4 +- skills/flashduty/reference/monit.md | 2 +- skills/flashduty/reference/role.md | 4 +- skills/flashduty/reference/route.md | 2 +- skills/flashduty/reference/rum.md | 10 ++-- skills/flashduty/reference/safari.md | 2 +- skills/flashduty/reference/schedule.md | 4 +- skills/flashduty/reference/sourcemap.md | 2 +- skills/flashduty/reference/status-page.md | 6 +-- skills/flashduty/reference/team.md | 2 +- skills/flashduty/reference/template.md | 2 +- 19 files changed, 155 insertions(+), 56 deletions(-) diff --git a/internal/skilldoc/generate.go b/internal/skilldoc/generate.go index 260fb68..85340fa 100644 --- a/internal/skilldoc/generate.go +++ b/internal/skilldoc/generate.go @@ -373,13 +373,21 @@ type respField struct{ name, typ string } // - 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. +// - `{items: [...]}` page wrapper: the block's top-level array field is +// `items` (row fields nested one level deeper under it), alongside +// scalar pagination siblings — `total`, `has_next_page`, +// `search_after_ctx`, … (cligen's listEnvelope requires every +// non-`items` top-level field to be a scalar, so these are always +// pagination metadata, never more row data). // // 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. +// wrapper key. For the wrapper shape, the pagination siblings are named too +// (by name only — their type is implied by cligen's scalar-sibling +// invariant), since an agent told to paginate via `--search-after-ctx` still +// needs to know the returned cursor lives at `.search_after_ctx` next to +// `.items`, not buried in a field list it never sees. func responseShapeLine(long string) string { lines := strings.Split(long, "\n") headerIdx, header := -1, "" @@ -396,19 +404,31 @@ func responseShapeLine(long string) string { wrapped := strings.Contains(header, "nested under items[]") fieldIndent := " " if wrapped { - fieldIndent = " " // one level under the sole top-level "items" row + fieldIndent = " " // one level under the top-level "items" row } - var fields []respField + // fields holds the row-level fields the summary's "fields:" list names + // (top-level for object/array shapes, one level under "items" for the + // wrapper shape). siblings holds the wrapper shape's OTHER top-level + // fields — pagination metadata alongside "items" (total, has_next_page, + // search_after_ctx, …) — collected only when wrapped, since the + // unwrapped shapes have no such siblings to speak of. + var fields, siblings []respField for _, line := range lines[headerIdx+1:] { if strings.TrimSpace(line) == "" { break } m := responseFieldRe.FindStringSubmatch(line) - if m == nil || m[1] != fieldIndent { + if m == nil { continue } - fields = append(fields, respField{m[2], m[3]}) + indent, name, typ := m[1], m[2], m[3] + switch { + case indent == fieldIndent: + fields = append(fields, respField{name, typ}) + case wrapped && indent == " " && !wrapperWireNames[name]: + siblings = append(siblings, respField{name, typ}) + } } if len(fields) == 0 { return "" @@ -430,18 +450,37 @@ func responseShapeLine(long string) string { return "" } - var shape string + var shape, fieldsLabel string switch { case wrapped: - shape = "`{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`)" + shape = fmt.Sprintf("`{items: [...]%s}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`)", siblingSuffix(siblings)) + fieldsLabel = "items fields" // disambiguates row fields from the wrapper's own pagination siblings named above case strings.Contains(header, "TOP-LEVEL array"): shape = "TOP-LEVEL array — pipe `--json | jq '.[]'` (NOT `.items[]`)" + fieldsLabel = "fields" default: shape = "single object (`data` unwrapped to the top level)" + fieldsLabel = "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, "; ")) + return fmt.Sprintf("- response: %s — %s: %s\n", shape, fieldsLabel, strings.Join(names, "; ")) +} + +// siblingSuffix renders a wrapped response's top-level pagination-metadata +// siblings — e.g. ", total, has_next_page, search_after_ctx" — appended +// inside the `{items: [...]}` wrapper descriptor, in the order cligen +// documented them. Empty when the envelope carries no siblings beyond the +// row array itself. +func siblingSuffix(siblings []respField) string { + if len(siblings) == 0 { + return "" + } + names := make([]string, len(siblings)) + for i, s := range siblings { + names[i] = s.name + } + return ", " + strings.Join(names, ", ") } diff --git a/internal/skilldoc/response_shape_test.go b/internal/skilldoc/response_shape_test.go index 4eb1844..1dc7ab6 100644 --- a/internal/skilldoc/response_shape_test.go +++ b/internal/skilldoc/response_shape_test.go @@ -48,6 +48,24 @@ Request fields: --schedule-id int (required) — Schedule ID. ` +// itemsWrappedWithPaginationSiblingsShapeLong reproduces the real shape of +// `fduty insight incident-list` (internal/cli/zz_generated_analytics.go): a +// page-wrapper response whose top level carries "items" ALONGSIDE scalar +// pagination metadata — has_next_page, search_after_ctx, total — not just +// "items" alone. cligen's listEnvelope requires every non-"items" top-level +// field here to be a scalar, so these three are always pagination metadata, +// never more row data. +const itemsWrappedWithPaginationSiblingsShapeLong = `List insight incidents. + +Response fields ('data' envelope is unwrapped — rows are nested under items[]; pipe 'jq '.items[]'', NOT '.data.items[]'): + - has_next_page (boolean) + - items (array) + - incident_id (string) + - title (string) + - search_after_ctx (string) — Cursor token to fetch the next page. Pass it back in the next request's 'search_after_ctx'. + - total (integer) — Total matching incidents. +` + // 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 @@ -145,6 +163,48 @@ func TestResponseShapeLine_ItemsWrapped(t *testing.T) { } } +// TestResponseShapeLine_ItemsWrappedNamesPaginationSiblings covers the real +// `insight incident-list` shape: the wrapper's top level carries "items" +// ALONGSIDE scalar pagination metadata (has_next_page, search_after_ctx, +// total). Those siblings must be named in the wrapper descriptor itself — +// an agent told to paginate via `--search-after-ctx` needs to know the +// returned cursor lives at `.search_after_ctx` next to `.items`, not silently +// dropped because the parser only ever looked one level under "items". +func TestResponseShapeLine_ItemsWrappedNamesPaginationSiblings(t *testing.T) { + got := responseShapeLine(itemsWrappedWithPaginationSiblingsShapeLong) + if !strings.Contains(got, "items: [...]") || !strings.Contains(got, "page wrapper") { + t.Errorf("items[] wrapper shape not detected:\n%s", got) + } + // The siblings are named inside the wrapper descriptor, in the order + // cligen documented them (source order, already alphabetical here). + if !strings.Contains(got, "{items: [...], has_next_page, search_after_ctx, total}") { + t.Errorf("wrapper descriptor must name the pagination siblings:\n%s", got) + } + // The row fields (nested under items) are still named, under a label that + // disambiguates them from the siblings just named above. + if !strings.Contains(got, "items fields: incident_id (string); title (string)") { + t.Errorf("row fields must still be listed under an unambiguous label:\n%s", got) + } + // The siblings must not ALSO appear duplicated in the row-field list. + fieldsPart := strings.SplitN(got, "items fields:", 2)[1] + for _, sib := range []string{"has_next_page", "search_after_ctx", "total"} { + if strings.Contains(fieldsPart, sib) { + t.Errorf("pagination sibling %q must not be duplicated in the row-field list:\n%s", sib, got) + } + } +} + +// TestResponseShapeLine_ItemsWrappedNoSiblingsUnchanged proves the sibling +// feature is additive: a wrapper with no pagination siblings beyond "items" +// (itemsWrappedShapeLong) renders the exact same wrapper descriptor as +// before — no trailing ", " artifact from an empty sibling list. +func TestResponseShapeLine_ItemsWrappedNoSiblingsUnchanged(t *testing.T) { + got := responseShapeLine(itemsWrappedShapeLong) + if !strings.Contains(got, "`{items: [...]}` page wrapper") { + t.Errorf("wrapper with no siblings must render the bare descriptor, got:\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) diff --git a/skills/flashduty/reference/alert.md b/skills/flashduty/reference/alert.md index 6f4b3ba..72c7e78 100644 --- a/skills/flashduty/reference/alert.md +++ b/skills/flashduty/reference/alert.md @@ -60,7 +60,7 @@ 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) +- response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — 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) ### events List alert events @@ -74,7 +74,7 @@ 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) +- response: `{items: [...], has_next_page}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: account_id (integer); created_at (integer); creator_id (integer); detail (object); ref_id (string); type (string); updated_at (integer) ### get Get alert detail @@ -102,7 +102,7 @@ List alerts ### 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) +- response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — 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) ### merge [...] Merge alerts into an incident @@ -120,7 +120,7 @@ Get alert pipeline ### 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/calendar.md b/skills/flashduty/reference/calendar.md index c411d78..287f52b 100644 --- a/skills/flashduty/reference/calendar.md +++ b/skills/flashduty/reference/calendar.md @@ -83,7 +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) +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -105,7 +105,7 @@ Get calendar info 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) +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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/channel.md b/skills/flashduty/reference/channel.md index f17ee1a..7209fe0 100644 --- a/skills/flashduty/reference/channel.md +++ b/skills/flashduty/reference/channel.md @@ -123,7 +123,7 @@ Get escalation rule detail ### 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -144,7 +144,7 @@ Get channel detail ### 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: channel_id (integer); channel_name (string); status (string) ### inhibit-rule-create Create inhibit rule @@ -175,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -224,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -264,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/enrichment.md b/skills/flashduty/reference/enrichment.md index 210ff3f..6efe867 100644 --- a/skills/flashduty/reference/enrichment.md +++ b/skills/flashduty/reference/enrichment.md @@ -80,7 +80,7 @@ Get enrichment rules ### 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -105,7 +105,7 @@ Get mapping API detail ### 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) +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -137,7 +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) +- response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: created_at (integer); fields (object); key (string); updated_at (integer) ### mapping-data-truncate Truncate mapping data @@ -174,7 +174,7 @@ Get mapping schema detail ### 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) +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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/incident.md b/skills/flashduty/reference/incident.md index 9d82354..1c8aeb2 100644 --- a/skills/flashduty/reference/incident.md +++ b/skills/flashduty/reference/incident.md @@ -140,7 +140,7 @@ 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) +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — 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) ### alerts View incident alerts @@ -223,7 +223,7 @@ List incidents ### 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) +- response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — 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) ### merge Merge incidents into a target incident @@ -233,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — 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) ### post-mortem-basics-reset Update post-mortem basics @@ -283,7 +283,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); generation (integer); incident_ids (array); is_private (boolean); media_count (integer); post_mortem_id (string); revision (integer); status (string); team_id (integer); template_id (string); title (string); updated_at_seconds (integer) +- response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: account_id (integer); author_ids (array); channel_id (integer); channel_name (string); created_at_seconds (integer); generation (integer); incident_ids (array); is_private (boolean); media_count (integer); post_mortem_id (string); revision (integer); status (string); team_id (integer); template_id (string); title (string); updated_at_seconds (integer) ### post-mortem-status-reset Update post-mortem status @@ -306,7 +306,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) +- response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -374,7 +374,7 @@ Get ServiceDeskPlus linked incidents - `--search-after-ctx` string — Cursor returned by the previous page. - `--start-time` string — Window start, Unix seconds. Optional when 'incident_id' is provided. (min 0) Accepts a duration (7d, 24h), '+7d' for the future, 'now', a date, or Unix seconds. - `--status` string — Synchronization status filter. · enum: success | failed -- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — fields: channel_id (integer); channel_name (string); created_at (integer); error_message (string); incident_id (string); incident_title (string); integration_id (integer); request_id (string); request_link (string); status (string) +- response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: channel_id (integer); channel_name (string); created_at (integer); error_message (string); incident_id (string); incident_title (string); integration_id (integer); request_id (string); request_link (string); status (string) ### similar Find similar incidents @@ -469,7 +469,7 @@ Get war room detail 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — 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) diff --git a/skills/flashduty/reference/insight.md b/skills/flashduty/reference/insight.md index 9da8e3c..440507b 100644 --- a/skills/flashduty/reference/insight.md +++ b/skills/flashduty/reference/insight.md @@ -84,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -112,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: hours (string); label (string); total_alert_cnt (integer); total_alert_event_cnt (integer) ### channel Get channel insight @@ -213,7 +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) +- response: `{items: [...], has_next_page, search_after_ctx, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — 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) ### incidents Query incidents with performance metrics @@ -247,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/member.md b/skills/flashduty/reference/member.md index 26b590c..7ae93ea 100644 --- a/skills/flashduty/reference/member.md +++ b/skills/flashduty/reference/member.md @@ -82,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: member_id (integer); member_name (string) ### list List members @@ -93,7 +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) +- response: `{items: [...], limit, p, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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.md b/skills/flashduty/reference/monit.md index 3bc8c00..f6ba5d5 100644 --- a/skills/flashduty/reference/monit.md +++ b/skills/flashduty/reference/monit.md @@ -339,7 +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) +- response: `{items: [...], next_cursor, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/role.md b/skills/flashduty/reference/role.md index c0c6d45..9ade3ba 100644 --- a/skills/flashduty/reference/role.md +++ b/skills/flashduty/reference/role.md @@ -81,7 +81,7 @@ Get role detail 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) +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -102,7 +102,7 @@ List permission factors 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/route.md b/skills/flashduty/reference/route.md index 4a127f3..243925f 100644 --- a/skills/flashduty/reference/route.md +++ b/skills/flashduty/reference/route.md @@ -57,7 +57,7 @@ Get routing rule detail ### 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 afd473a..d13ec82 100644 --- a/skills/flashduty/reference/rum.md +++ b/skills/flashduty/reference/rum.md @@ -77,7 +77,7 @@ Get application detail ### 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -89,7 +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: same shape as `application-infos [...]` above +- response: `{items: [...], has_next_page, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -124,13 +124,13 @@ 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -160,7 +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) +- response: `{items: [...], has_next_page, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/safari.md b/skills/flashduty/reference/safari.md index 75882af..c1989a2 100644 --- a/skills/flashduty/reference/safari.md +++ b/skills/flashduty/reference/safari.md @@ -85,7 +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) +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/schedule.md b/skills/flashduty/reference/schedule.md index 80a9f8e..0ae53be 100644 --- a/skills/flashduty/reference/schedule.md +++ b/skills/flashduty/reference/schedule.md @@ -124,7 +124,7 @@ Get schedule info ### 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -137,7 +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: same shape as `infos [...]` above +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/sourcemap.md b/skills/flashduty/reference/sourcemap.md index 9c14463..16a21be 100644 --- a/skills/flashduty/reference/sourcemap.md +++ b/skills/flashduty/reference/sourcemap.md @@ -47,7 +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) +- response: `{items: [...], total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 diff --git a/skills/flashduty/reference/status-page.md b/skills/flashduty/reference/status-page.md index 2ce94ae..980bcee 100644 --- a/skills/flashduty/reference/status-page.md +++ b/skills/flashduty/reference/status-page.md @@ -51,7 +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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -157,7 +157,7 @@ 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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 @@ -210,7 +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) +- response: `{items: [...], has_next_page, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: all (boolean); components (array); locale (string); method (string); recipient (string) ### template-delete Delete status page template diff --git a/skills/flashduty/reference/team.md b/skills/flashduty/reference/team.md index d36093f..21818bb 100644 --- a/skills/flashduty/reference/team.md +++ b/skills/flashduty/reference/team.md @@ -78,7 +78,7 @@ Get team detail ### 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) +- response: `{items: [...]}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: person_ids (array); team_id (integer); team_name (string) ### list List teams diff --git a/skills/flashduty/reference/template.md b/skills/flashduty/reference/template.md index d252bd5..6c99c45 100644 --- a/skills/flashduty/reference/template.md +++ b/skills/flashduty/reference/template.md @@ -108,7 +108,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); feishu_app_card_table_enabled (boolean); 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) +- response: `{items: [...], has_next_page, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items 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); feishu_app_card_table_enabled (boolean); 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 From 860f9e2ac6c3d182e32e2c4c4391c71fe28f8695 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Tue, 28 Jul 2026 06:46:22 -0700 Subject: [PATCH 3/3] fix(skilldoc): don't misparse a description's bracketed prose as an enum The Request-fields enum extractor matched the FIRST "[...]" bracket anywhere in a flag's tail text and treated its contents as an enum declaration. cligen's own trailing enum bracket is always the last thing on the line (before an optional constraint suffix), but a description can legitimately quote an unrelated bracket earlier in its own prose -- e.g. a regex character class ("1-40 chars of '[a-zA-Z0-9_]'"). The old code fabricated a one-value enum from that character class AND deleted it from the sentence, leaving an empty quoted pair where the class used to be. Only recognize a bracket anchored to the end of the tail (once an optional trailing "(constraint)" is set aside) as a genuine enum -- exactly where cligen's generator appends one. Anything earlier in the text is left untouched, description and constraint both. Regenerating all cards with this fix touches only field.md (the sole card affected): the fabricated enum tag is gone and the character class renders intact in the description. --- internal/skilldoc/generate.go | 56 +++++++++++--- internal/skilldoc/generate_test.go | 109 ++++++++++++++++++++++++++++ skills/flashduty/reference/field.md | 2 +- 3 files changed, 157 insertions(+), 10 deletions(-) diff --git a/internal/skilldoc/generate.go b/internal/skilldoc/generate.go index 85340fa..1431f90 100644 --- a/internal/skilldoc/generate.go +++ b/internal/skilldoc/generate.go @@ -255,10 +255,46 @@ type requestFields struct { var ( flagLineRe = regexp.MustCompile(`^\s{2}--([a-z0-9-]+)\s+\S+\s*(.*)$`) bodyLineRe = regexp.MustCompile(`^\s{2}([a-z0-9_]+)\s+\(([^,)]*)[^)]*\)\s*(.*)$`) - enumRe = regexp.MustCompile(`\[([^\]]+)\]`) requiredTag = "(required)" ) +// trailingConstraintRe matches a trailing "(...)" constraint parenthetical -- +// cligen appends this immediately after an enum bracket when a flag has both +// (internal/cmd/cligen/main.go writes f.Enum then f.Constraint, in that +// order). Setting it aside first lets enumTailRe check what's left for a +// genuine trailing enum bracket without the constraint's own parens getting +// in the way, and lets that constraint be reattached untouched afterward. +var trailingConstraintRe = regexp.MustCompile(`\s*\([^()]*\)\s*$`) + +// enumTailRe matches a "[a, b, c]" bracket anchored to the END of its input -- +// exactly where cligen's own generator appends a real enum ("line += " [" + +// strings.Join(f.Enum, ", ") + "]"", see cligen/main.go). Anchoring to the +// end is what excludes an arbitrary "[...]" appearing earlier in a +// description's free text -- e.g. a regex character class quoted inside a +// constraint sentence ("1-40 chars of '[a-zA-Z0-9_]'", see +// zz_generated_alert_enrichment.go's field-name flag) -- which an unanchored +// match would misidentify as a one-value enum declaration AND delete from +// the description, corrupting both the fabricated enum tag and the sentence. +var enumTailRe = regexp.MustCompile(`\s*\[([^\]]+)\]\s*$`) + +// splitTrailingEnum peels cligen's own trailing enum bracket, if any, off +// tail: enum is the bracket's inner text ("" when there is none), and rest is +// tail with that bracket removed. A trailing "(constraint)" is set aside +// before looking for the bracket and reattached untouched afterward, so it +// never masks a genuine enum and is never itself mistaken for one. When the +// tail-most token (once any constraint is set aside) isn't a bracket at all, +// enum is "" and rest is tail unchanged -- any "[...]" earlier in the text is +// incidental prose, not a declared enum, and must survive intact. +func splitTrailingEnum(tail string) (enum, rest string) { + constraint := trailingConstraintRe.FindString(tail) + body := strings.TrimSuffix(tail, constraint) + loc := enumTailRe.FindStringSubmatchIndex(body) + if loc == nil { + return "", tail + } + return body[loc[2]:loc[3]], body[:loc[0]] + body[loc[1]:] + constraint +} + // parseRequestFields extracts the per-flag required/enum/usage and the // body-only (--data) field summaries from a command's Long "Request fields:" // block. Returns empty maps when the block is absent (read-only verbs). @@ -303,13 +339,14 @@ func parseRequestFields(long string) requestFields { return rf } -// parseEnum pulls the enum members out of a trailing "[a, b, c]" marker. +// parseEnum pulls the enum members out of tail's genuine trailing "[a, b, c]" +// marker (see splitTrailingEnum), or returns nil when tail carries none. func parseEnum(tail string) []string { - m := enumRe.FindStringSubmatch(tail) - if m == nil { + enum, _ := splitTrailingEnum(tail) + if enum == "" { return nil } - parts := strings.Split(m[1], ",") + parts := strings.Split(enum, ",") out := make([]string, 0, len(parts)) for _, p := range parts { if v := strings.TrimSpace(p); v != "" { @@ -319,11 +356,12 @@ func parseEnum(tail string) []string { return out } -// cleanUsage strips the leading em-dash, the (required) tag, and the trailing -// enum bracket from a flag line's tail, leaving the human description. +// cleanUsage strips the leading em-dash, the (required) tag, and tail's +// genuine trailing enum bracket (see splitTrailingEnum) from a flag line's +// tail, leaving the human description -- including any trailing constraint +// and any incidental "[...]" that isn't cligen's own enum marker. func cleanUsage(tail string) string { - s := tail - s = enumRe.ReplaceAllString(s, "") + _, s := splitTrailingEnum(tail) s = strings.ReplaceAll(s, requiredTag, "") s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "—") diff --git a/internal/skilldoc/generate_test.go b/internal/skilldoc/generate_test.go index e17db20..7494238 100644 --- a/internal/skilldoc/generate_test.go +++ b/internal/skilldoc/generate_test.go @@ -158,6 +158,115 @@ func TestGenerateFence_PositionalArg(t *testing.T) { } } +// TestSplitTrailingEnum_IgnoresBracketedProseInsideDescription reproduces the +// real `field create --field-name` bug: its description quotes a regex +// character class mid-sentence ("1-40 chars of '[a-zA-Z0-9_]'") followed by +// more prose and a trailing constraint. An unanchored bracket match treats +// that as a one-value enum declaration, fabricating an "enum: a-zA-Z0-9_" tag +// AND deleting the bracket from the sentence (leaving an empty pair of +// quotes where the character class used to be). Since +// the bracket isn't at the tail's end (more text and the constraint follow +// it), it must be left alone entirely: no enum, description word-for-word. +func TestSplitTrailingEnum_IgnoresBracketedProseInsideDescription(t *testing.T) { + tail := `(required) — Machine name. Must start with a letter or underscore; 1–40 chars of '[a-zA-Z0-9_]'. Immutable after creation. (≤39 chars)` + if enum := parseEnum(tail); enum != nil { + t.Errorf("bracketed prose must not be parsed as an enum, got %v", enum) + } + got := cleanUsage(tail) + want := `Machine name. Must start with a letter or underscore; 1–40 chars of '[a-zA-Z0-9_]'. Immutable after creation. (≤39 chars)` + if got != want { + t.Errorf("cleanUsage must leave the character class and constraint untouched:\ngot: %q\nwant: %q", got, want) + } +} + +// TestSplitTrailingEnum_RecognizesGenuineTrailingEnum proves the fix doesn't +// overcorrect: cligen's own trailing enum bracket ("line += " [" + ... + +// "]"", cligen/main.go) -- anchored at the tail's end, nothing else +// following it -- is still recognized and still stripped from the usage +// text. +func TestSplitTrailingEnum_RecognizesGenuineTrailingEnum(t *testing.T) { + tail := `(required) — Event type. [incident, maintenance]` + if enum := parseEnum(tail); !equalStrings(enum, []string{"incident", "maintenance"}) { + t.Errorf("genuine trailing enum not recognized, got %v", enum) + } + if got, want := cleanUsage(tail), "Event type."; got != want { + t.Errorf("cleanUsage should strip the genuine enum bracket:\ngot: %q\nwant: %q", got, want) + } +} + +// TestSplitTrailingEnum_GenuineEnumWithTrailingConstraint covers a flag that +// has BOTH an enum and a constraint (cligen writes enum then constraint, in +// that order, when both are present): the enum must still be recognized and +// stripped, and the constraint must survive untouched and back in its +// original trailing position -- not swallowed by the enum-bracket removal. +func TestSplitTrailingEnum_GenuineEnumWithTrailingConstraint(t *testing.T) { + tail := `— Some usage. [a, b] (≤5 chars)` + if enum := parseEnum(tail); !equalStrings(enum, []string{"a", "b"}) { + t.Errorf("enum with a trailing constraint not recognized, got %v", enum) + } + if got, want := cleanUsage(tail), "Some usage. (≤5 chars)"; got != want { + t.Errorf("cleanUsage must strip only the enum bracket, keeping the constraint:\ngot: %q\nwant: %q", got, want) + } +} + +// TestSplitTrailingEnum_SingleValueEnumAtEnd covers a one-member enum (e.g. +// the real `--environment-kind [byoc]`), proving the fix doesn't require a +// comma to recognize a genuine trailing bracket as an enum. +func TestSplitTrailingEnum_SingleValueEnumAtEnd(t *testing.T) { + tail := `— Pin to a specific runner. [byoc]` + if enum := parseEnum(tail); !equalStrings(enum, []string{"byoc"}) { + t.Errorf("single-value trailing enum not recognized, got %v", enum) + } +} + +// TestGenerateFence_BracketedProseSurvivesAlongsideGenuineEnum is the +// fence-level integration check: one command with a description-embedded +// character class (must NOT become an enum) and, on a different flag in the +// same command, a genuine trailing enum (must still render as one) — +// proving the fix distinguishes them within the same Request-fields block, +// not just in isolation. +func TestGenerateFence_BracketedProseSurvivesAlongsideGenuineEnum(t *testing.T) { + d := Dump{Commands: []Command{{ + Path: "field create", + Group: "field", + Short: "Create field", + Use: "create", + Long: `Create field. + +Request fields: + --field-name string (required) — Machine name. Must start with a letter or underscore; 1–40 chars of '[a-zA-Z0-9_]'. Immutable after creation. (≤39 chars) + --field-type string (required) — Field input type. [checkbox, text] +`, + Flags: []Flag{{Name: "field-name", Type: "string"}, {Name: "field-type", Type: "string"}}, + }}} + sec := sectionFor(GenerateFence(d, "field"), "create") + if !strings.Contains(sec, "1–40 chars of '[a-zA-Z0-9_]'") { + t.Errorf("character class embedded in prose must render intact:\n%s", sec) + } + if strings.Contains(sec, "chars of ''") { + t.Errorf("character class must not be deleted from the description:\n%s", sec) + } + if strings.Contains(sec, "enum: a-zA-Z0-9_") { + t.Errorf("character class must not be fabricated into an enum tag:\n%s", sec) + } + if !strings.Contains(sec, "enum: checkbox | text") { + t.Errorf("the genuine enum on the OTHER flag in the same command must still render:\n%s", sec) + } +} + +// equalStrings compares two string slices for the enum-parsing tests above. +func equalStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + // sectionFor returns the slice of out from "### " to the next "### " or end. func sectionFor(out, verb string) string { start := strings.Index(out, "### "+verb) diff --git a/skills/flashduty/reference/field.md b/skills/flashduty/reference/field.md index e42b638..b686fb7 100644 --- a/skills/flashduty/reference/field.md +++ b/skills/flashduty/reference/field.md @@ -45,7 +45,7 @@ fduty field update \ Create field - `--description` string — Optional free-text description. (≤499 chars) - `--display-name` string (required) — Human-readable name. Must be unique within the account. (≤39 chars) -- `--field-name` string (required) — Machine name. Must start with a letter or underscore; 1–40 chars of ''. Immutable after creation. (≤39 chars) · enum: a-zA-Z0-9_ +- `--field-name` string (required) — Machine name. Must start with a letter or underscore; 1–40 chars of '[a-zA-Z0-9_]'. Immutable after creation. (≤39 chars) - `--field-type` string (required) — Field input type. Immutable after creation. · enum: checkbox | multi_select | single_select | text - `--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