Skip to content

feat: self-correcting flag/command errors, global --fields, query window notes - #217

Merged
ysyneu merged 3 commits into
mainfrom
feat/self-correcting-errors-and-window-notes
Oct 9, 2026
Merged

ysyneu merged 3 commits into
mainfrom
feat/self-correcting-errors-and-window-notes

Conversation

@ysyneu

@ysyneu ysyneu commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Callers (people typing from memory, scripts, agents) often guess a flag or verb name by analogy with a sibling command. This PR makes those errors list the valid candidates so the next call can be correct, adds --fields to every command's structured output, and prints the time window a query actually sent.

Unknown flag / command errors list candidates

$ flashduty alert list --channel-id 1
Error: unknown flag: --channel-id for "flashduty alert list"

Did you mean this?
	--channel
Flags: --active, --alert-ids, --alert-keys, --asc, --by-updated-at, --channel, --fields, --integration, --limit, --muted, --page, --recovered, --severity, --since, --until
Global flags: --base-url, --json, --no-trunc, --output-format

$ flashduty silence
Error: unknown command "silence" for "flashduty"

Did you mean this?
	flashduty channel silence-rule-list
	flashduty channel silence-rule-create
	...
Available commands: account, alert, alert-event, ...

$ flashduty schedule get 1
Error: unknown command "get" for "flashduty schedule"

Did you mean this?
	flashduty schedule info
	flashduty schedule infos
Available commands: by-person, create, delete, info, infos, list, preview, self, update
  • The root FlagErrorFunc is inherited by every curated and generated command. Only unknown-flag errors are extended; a bad value for a known flag keeps pflag's message.
  • Unknown commands are matched in this order:
    1. siblings
    2. single-record verb synonyms (get/info/infos/detail)
    3. every leaf in the tree whose path contains the token on hyphen-token boundaries
  • The check runs before cobra dispatch. Before this, flashduty alert lsit --help printed the alert help and exited 0, because cobra handles --help before any Args validator runs.
  • --version now works and prints the same line as flashduty version.
  • No new aliases and no verb renames. Renames would break existing scripts, and the suggestions cover the whole class of mistakes rather than one spelling.

--fields on every command

  • --fields is now a global output flag, like --output-format. In json/toon mode it keeps the named top-level fields of each list row, or of a single record. List envelope keys such as total and cursors are kept.
  • On generated commands, projection happens before the 16 KiB structured-output bound, so the bound applies to the projected rows.
  • A requested name that matches no key gets a stderr note listing the keys that exist. It is a note rather than an error because an omitempty key can be absent from a whole page.
  • Any output --fields cannot project prints unchanged, with a stderr note saying so: acknowledgements, raw (CSV/file) bodies, table mode, and commands that print their own output (version, config show). One post-run hook emits the note, so no command ignores the flag silently. A list of scalars is an error.
  • Commands that declare their own --fields keep it, since the local flag shadows the global one. That covers incident list/detail/similar, alert list, alert-event list and others with default projections, plus monit rule-update-fields, where --fields is a request field.
  • Table output is unchanged (apart from the stderr note).

Query window notes

note: window 2026-09-09T14:00:00+08:00..2026-10-09T14:00:00+08:00 (30d, ended now; default --since 30d because --progress lists only open states)
note: window 2026-10-09T21:00:00+08:00..2026-10-09T22:30:00+08:00 (1h30m, ends in 8h30m)
  • The note goes to stderr only; stdout stays pure json/toon. It shows the local timezone, the span, and where the end falls relative to now. An epoch converted from the wrong timezone shows up as a window in the future.
  • Covered:
    • the curated windowed verbs: incident, alert, alert-event, change, audit, insight, oncall
    • monit-query (its execution.from_ms/to_ms)
    • generated --start-time/--end-time pairs, via cligen
  • incident list --progress Triggered[,Processing] and alert list --active now default to 30d instead of 24h when --since is not given. Open records are often older than a day, and 30d is the widest window the list endpoints accept.
  • A defaulted --since (24h or 30d) now spans back from --until, not from now. Before, --until +1d with the 30d default produced a 31-day window, which the server rejects. A past --until with the 24h default produced an inverted window.

skilldoc

  • The dump now carries the root's flags.
  • The validator now checks the flags of a bare `fduty --flag` example against the root flags. Before, such examples were skipped as prose.
  • Global flags now come from the dump instead of a hand-kept list.

Behavior changes

For release notes:

  • Wider default window for open-only lists. incident list --progress Triggered (or Processing, or both) and alert list --active now query the last 30 days when --since is not given, instead of the last 24 hours. Results can contain more rows than before. Pass --since 24h for the previous window.
  • Defaulted --since is measured from --until. When only --until is given, the default lookback ends at --until instead of at the current time.
  • Window notes on stderr. Windowed commands print a note: window ... line to stderr: incident, alert, alert-event, change, audit, insight, oncall, monit-query, and generated commands with --start-time/--end-time. stdout is unchanged. Callers that merge the streams (2>&1) and parse the result as JSON should read stdout only.
  • A mistyped subcommand fails even with --help. flashduty <group> <typo> --help now prints the unknown-command error with suggestions and exits 1. Before, it printed the group's help and exited 0.
  • --fields is a global flag. Commands that did not accept it before now project json/toon output, or print a stderr note when the flag does not apply. Commands with their own --fields behave as before.
  • --version is accepted on the root command.

Test plan

  • make check (gofmt/gci, golangci-lint v2.11.4 with 0 issues, go test -race ./..., build): all pass.
  • make check-cards: cards OK. make gen-cards and go run ./internal/cmd/cligen produce no further diff.
  • New table tests:
    • suggest_test.go: flag and command suggestions; --help after a typo; lazy help/completion/__complete still dispatch; --version.
    • window_test.go: note text with a pinned clock and timezone; 30d defaults per --progress/--active combination; default --since anchored at --until +1d (30d span) and at a past --until (24h span); generated and monit-query notes; stdout purity.
    • outputfields_test.go: generated list envelope, curated detail, unmatched-name note, table mode, acknowledgement note, version / config show / table-mode notes, local --fields shadowing, shape error.
    • parse_test.go: Lookback accepts past durations only (rejects +1d, now, dates, epochs).
    • validate_test.go: a bogus root flag fails; --help and --version pass.

🤖 Generated with Claude Code

ysyneu added 2 commits October 9, 2026 04:36
…dow notes

Callers frequently guess flag and verb names by analogy with a sibling
command. The errors now carry the candidates, so the next call can be
right without a separate --help round trip:

- Unknown flag: lists the closest flag names, the command's own flags and
  the global flags (root FlagErrorFunc, inherited by curated and generated
  commands). Other flag errors pass through unchanged.
- Unknown command: suggests siblings, then single-record verb synonyms
  (get/info/infos/detail), then any leaf in the whole tree whose path
  contains the token on hyphen-token boundaries (`silence` ->
  `channel silence-rule-list`, `monit-datasource` -> `monit datasource-list`),
  and lists the level's commands. The check now runs before cobra
  dispatch, so `<group> <typo> --help` errors instead of printing the
  group's help and exiting 0.
- `--version` is accepted and prints the `version` line.

--fields is now a global output flag: json/toon output of any command is
projected to the named top-level fields (each row of a list, or the keys
of a single record), before the 16 KiB bound is applied. A name matching
no key is reported on stderr with the keys that exist; acknowledgements
and raw bodies print unchanged with a note; non-projectable shapes are an
error. Commands with their own --fields keep it.

Windowed queries announce on stderr the window they sent, in the local
timezone, with its span and its end relative to now
(`note: window ..(1h, ends in 7h)`), which makes a wrong default or a
timezone slip visible. This covers the curated windowed verbs,
monit-query's execution window, and generated --start-time/--end-time
pairs. `incident list` with only open --progress states and
`alert list --active` default to 30d instead of 24h when --since is not
given, since open records are often older than a day.

skilldoc: the dump now carries the root's flags; the validator checks the
flags of a bare `fduty --flag` example against them and takes global
flags from the dump instead of a hand-kept list.
A bare `fduty --bogus` reports an unknown flag; `fduty --json` is valid.
@ysyneu

ysyneu commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Checked against a live test environment with read-only commands only, comparing this branch to main, with TZ=Asia/Shanghai. IDs, titles and hosts are omitted below.

1. incident list --progress Triggered,Processing --limit 100

  • main: 30 rows. These are the open incidents from the last 24h, all on one page, and nothing says a window was applied.
  • branch: 100 rows on page 1 of 725 open incidents. stderr:
note: window 2026-09-09T19:42:28+08:00..2026-10-09T19:42:28+08:00 (30d, ended now; default --since 30d because --progress lists only open states)
note: showing 100 of 725 total results (page 1); raise --limit or use --page for the rest

2. alert list --active --limit 100

  • main: 33 rows.
  • branch: 100 rows on page 1 of 1000 total. stderr:
note: window 2026-09-09T19:42:19+08:00..2026-10-09T19:42:19+08:00 (30d, ended now; default --since 30d because of --active)

3. monit-query <prometheus-ds> --tool prometheus.query, range params

  • from_ms/to_ms = 18:00..19:00 +08:00:
note: window 2026-10-09T18:00:00+08:00..2026-10-09T19:00:00+08:00 (1h, ended 43m ago)

It returned 4 frames.

  • The same wall-clock times misread as UTC, which is the common slip:
note: window 2026-10-10T02:00:00+08:00..2026-10-10T03:00:00+08:00 (1h, ends in 7h17m)

It returned frames: []. The note shows the window is in the future, which explains the empty result.

  • main prints no window line for either query.

4. Suggestions

  • main prints Error: unknown flag: --channel-id and Error: unknown command "silence" for "fduty", with no candidates.
  • branch:
Error: unknown flag: --channel-id for "fduty alert list"

Did you mean this?
	--channel
Flags: --active, --alert-ids, --alert-keys, --asc, --by-updated-at, --channel, --fields, --integration, --limit, --muted, --page, --recovered, --severity, --since, --until
Global flags: --base-url, --json, --no-trunc, --output-format

Error: unknown command "silence" for "fduty"

Did you mean this?
	fduty channel silence-rule-list
	fduty channel silence-rule-create
	fduty channel silence-rule-delete
	fduty channel silence-rule-enable
	fduty channel silence-rule-update
	fduty channel silence-rule-disable
Available commands: account, alert, alert-event, audit, ...

5. alert get <id from (2)> --fields ...

  • main: Error: unknown flag: --fields.
  • branch, --fields alert_id,title: stdout is exactly {"alert_id": ..., "title": ...}.
  • branch, --fields id,title: stdout is {"title": ...}. stderr:
note: --fields id matched no key in the output (keys present: account_id, alert_id, alert_key, alert_severity, alert_status, channel_id, ...)
  • Also, monit datasource-list --fields id,name,type_ident --output-format toon (a generated command) returned a 3-column toon table.

In every case above, stdout stayed valid JSON/toon, and all the notes went to stderr.

…re it does not apply

A defaulted lookback --since (24h, or 30d for open-only incident and
active alert lists) now spans back from the parsed --until instead of
from now. With --until +1d the 30d default used to produce a 31-day
window, which the list endpoints reject; a past --until with the 24h
default produced an inverted window. timeutil.Lookback recognizes the
past-relative duration form (and rejects "+" offsets, which the day
shorthand expansion would otherwise accept).

The "--fields does not apply" note moves to the root PersistentPostRun,
driven by whether a projection ran. Commands that print their own
output (version, config show) and table mode now get the note too,
instead of ignoring --fields silently.
@ysyneu
ysyneu merged commit bfc6738 into main Oct 9, 2026
12 checks passed
@ysyneu
ysyneu deleted the feat/self-correcting-errors-and-window-notes branch October 9, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant