From 6eb07213365dcd4a5b613a8ce087b5399f2f3bb7 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Wed, 8 Jul 2026 21:04:43 -0700 Subject: [PATCH] docs: guide flashduty large-list scans --- internal/skilldoc/skill_guidance_test.go | 50 ++++++++++++++++++++++++ skills/flashduty/SKILL.md | 2 + skills/flashduty/reference/incident.md | 3 ++ skills/flashduty/reference/insight.md | 3 ++ 4 files changed, 58 insertions(+) create mode 100644 internal/skilldoc/skill_guidance_test.go diff --git a/internal/skilldoc/skill_guidance_test.go b/internal/skilldoc/skill_guidance_test.go new file mode 100644 index 0000000..fa59df2 --- /dev/null +++ b/internal/skilldoc/skill_guidance_test.go @@ -0,0 +1,50 @@ +package skilldoc + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestFlashdutySkillGuidesLargeListScans(t *testing.T) { + body := readSkillFile(t, "SKILL.md") + incident := readSkillFile(t, "reference", "incident.md") + insight := readSkillFile(t, "reference", "insight.md") + + for _, want := range []string{ + "Large list scans", + "redirect JSON to a temp file", + "Do not dump page-sized JSON", + } { + if !strings.Contains(body, want) { + t.Errorf("SKILL.md missing large-list guidance %q", want) + } + } + for _, want := range []string{ + "Do not re-run `incident alerts`, `incident similar`, `incident timeline`, or `change list`", + "Page-sized scans go to files", + } { + if !strings.Contains(incident, want) { + t.Errorf("incident card missing large-list/summary guidance %q", want) + } + } + for _, want := range []string{ + "For multi-dimensional reports, fetch once to a temp file", + "Do not run the same account-wide list again for each `jq` bucket", + } { + if !strings.Contains(insight, want) { + t.Errorf("insight card missing reusable JSON guidance %q", want) + } + } +} + +func readSkillFile(t *testing.T, parts ...string) string { + t.Helper() + p := filepath.Join(append([]string{"..", "..", "skills", "flashduty"}, parts...)...) + b, err := os.ReadFile(p) + if err != nil { + t.Fatalf("read %s: %v", p, err) + } + return string(b) +} diff --git a/skills/flashduty/SKILL.md b/skills/flashduty/SKILL.md index 022bb11..bb0beb4 100644 --- a/skills/flashduty/SKILL.md +++ b/skills/flashduty/SKILL.md @@ -29,6 +29,8 @@ Append `--output-format toon` to read commands: it drops the per-row repeated ke **Shape the payload before you fetch it.** For ID scans, counts, or "find the matching row" tasks, prefer `--fields` projections and compact list verbs over full detail dumps. Huge raw JSON dumps are a last resort, not a default. +**Large list scans: file first, summarize second.** When you truly need `--json` for `jq` over a page-sized list (`--limit 100`, broad channel/account windows, incident/alert/on-call reports), redirect JSON to a temp file and emit only counts or selected fields: `fduty ... --json > /tmp/fduty_rows.json && jq '...' /tmp/fduty_rows.json`. Do not dump page-sized JSON into the chat transcript, and do not run the same list again for each aggregation bucket. + **Empty result = authoritative not-found.** A filter returning `[]` means no such entity in scope — report it (optionally the 1–2 closest names) and stop. Do **not** brute-force (no shifted-keyword re-queries, no widening past caps, no full-dump grep). Never infer "feature not enabled" from an empty list, and never fabricate data absent from tool output. **A result you did not fetch is "unknown", never "empty".** You may report a command's result — including "returned empty" or any count/list/finding — **only if that exact command appears in your tool-call history this turn**. If you did not run it, the honest answer is "未查询 / not queried", followed by the command to run. Writing "`incident similar` 返回空" or "无变更" for a command you never executed is fabrication, not a summary. diff --git a/skills/flashduty/reference/incident.md b/skills/flashduty/reference/incident.md index 4063413..b18560c 100644 --- a/skills/flashduty/reference/incident.md +++ b/skills/flashduty/reference/incident.md @@ -73,6 +73,8 @@ bash /scripts/incident-summary.sh `` is this skill's base directory — you were given it when the skill loaded (it is also the folder you read this card from). The script runs every command below and prints the results in one block, so each section of your summary is backed by real output and there is nothing to guess. (To tie post-mortems to *this* incident, re-run `incident post-mortem-list --channel-ids ` with the `channel_id` from `detail`.) +Do not re-run `incident alerts`, `incident similar`, `incident timeline`, or `change list` after `incident-summary.sh` unless the script output is missing/truncated for that exact section or you need a narrower follow-up query. + If you fetch the pieces by hand instead, run **all six** — they are cheap reads: ```bash @@ -364,6 +366,7 @@ List war rooms - **`update` vs `reset`**: `update ` edits title/description/severity/custom fields. `reset ` additionally supports `--impact`, `--root-cause`, `--resolution` (the AI narrative fields). Use `reset` for post-incident write-back. - **If `list` returns a `total`, use it instead of page-walking.** For "how many incidents are Triggered / Processing / Closed", run one filtered `incident list --progress ...` per bucket and read the returned `total`. Do not fetch page 1/2/3 just to derive counts the server already computed. - **Use `--fields` to keep list scans compact.** When the goal is to identify matching incidents or collect IDs/numbers/titles, project only the needed columns first, then fetch one target incident with `detail` / `alerts` / `timeline`. +- **Page-sized scans go to files.** For analytics over many incidents, use `--json > /tmp/incidents.json` and run `jq` against the file. Print only the aggregate or selected fields; avoid raw `--limit 100` JSON/toon dumps in chat. - **`--list` window cap**: `--since`/`--until` window must be < 31 days; `--limit` max 100. Empty result is authoritative — do not widen filters or retry. - **`merge` is irreversible**: source incidents are absorbed into target permanently. Always list and confirm both IDs before running. - **`remove --force`** bypasses the interactive confirmation prompt — never pass `--force` unless the user has explicitly said so. diff --git a/skills/flashduty/reference/insight.md b/skills/flashduty/reference/insight.md index 346cbbb..9020c82 100644 --- a/skills/flashduty/reference/insight.md +++ b/skills/flashduty/reference/insight.md @@ -328,6 +328,8 @@ Two families with **identical value syntax** but different flag names: Both families accept: relative duration (`30d`, `24h`), `now`, `+7d`, a date, or Unix seconds — **except** `incident-export`, which takes **epoch seconds only** for `--start-time`/`--end-time` (flag type is int64). +For multi-dimensional reports, fetch once to a temp file, then run every `jq` aggregation against that same file. Do not run the same account-wide list again for each `jq` bucket; if you need status, severity, channel, and responder cuts, one JSON capture plus local aggregations is cheaper and more consistent. + ## Gotchas - **Two time-flag families.** Passing `--since` to an `--start-time` command (or vice-versa) fails with `unknown flag`. See the table above. @@ -335,6 +337,7 @@ Both families accept: relative duration (`30d`, `24h`), `now`, `+7d`, a date, or - **`top-alerts --label`** only accepts `check` or `resource`. Any other value (e.g. `integration_name`) returns HTTP 400. - **Export commands output raw CSV, not JSON.** Redirect to a file; dumping CSV into context burns tokens and is unreadable. No `--limit`/`--page` — exports emit the full filtered set. - **`insight incidents` and `incident-list` are siblings**, not the same. `incidents` uses `--since`/`--until`, paginates, and is token-light. `incident-list` uses `--start-time`/`--end-time`, adds `--severities`/`--responder-ids`/`--query`/cursor (`--search-after-ctx`), and is the filterable variant. +- **Large `incident-list` pages belong in files.** For any `--limit 100` or broad-window `incident-list`, redirect JSON to `/tmp/*.json`, then print only totals or selected fields. Do not paste page-sized JSON/toon into the transcript. - **All `insight` commands hit the OLAP backend.** HTTP 500 means the backend is down — report it, do not retry. - **Empty result is authoritative.** A zero-row response means no matching data for that scope/window — do not widen filters or re-query with shifted keywords. - **`--aggregate-unit`** (on `account`, `alert-topk-by-label`, `channel`, `responder`, `team` and their exports) splits results into time buckets: `day` / `week` / `month`. When set, the window must span ≥24 h; `day` additionally caps the range at 31 days.