From af9349779088a748687410a73ffed00a7f627ac0 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:01:45 -0700 Subject: [PATCH 1/3] docs(skills): describe streamed results, the table cap, and exit code 3 The core skill still told agents that hotdata query prints the complete result in every format. Since #300, csv and json stream the persisted result while -o table fetches a 10,000-row window, prints an INCOMPLETE PREVIEW footer, and exits 3. Both skills now say so, and the analytics skill lists exit code 3 alongside 0/1/2. Also: wide decimals print at full precision and list/struct cells print on one line in -o json (#299); databases queries and results accept -d/--database and -w/--workspace-id, which both skills denied; two duplicated phrases in the geospatial skill's discovery notes. --- skills/hotdata/SKILL.md | 5 ++--- skills/hotdata/subskills/analytics/SKILL.md | 15 ++++++++------- skills/hotdata/subskills/geospatial/SKILL.md | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/skills/hotdata/SKILL.md b/skills/hotdata/SKILL.md index 5ebe584..7e23ed8 100644 --- a/skills/hotdata/SKILL.md +++ b/skills/hotdata/SKILL.md @@ -43,8 +43,6 @@ Optional: pass **`--debug`** on any command to print verbose HTTP request/respon Commands that accept `--workspace-id` default to the active workspace from config when omitted. Use `hotdata workspaces use` to switch interactively, or `hotdata workspaces use ` for a direct choice. In `hotdata workspaces list`, the `*` marker labels the **default** workspace the CLI resolves to. -**`hotdata databases queries` does not accept `--workspace-id`:** query run history always uses the active workspace—set it with `workspaces use` first if needed. - If **`HOTDATA_WORKSPACE`** is set in the environment, the workspace is **locked** to that value: passing a different `--workspace-id` is an error, and **`hotdata workspaces use` fails** (“workspace is locked”). **Omit `--workspace-id` unless you need to target a specific workspace** (and it is not locked by env or session). @@ -258,7 +256,8 @@ hotdata query status - Use `hotdata databases tables list` and `hotdata databases tables show` for discovery — not `information_schema` via `query`. (Discovery lists every workspace table; queryability still requires the table's catalog to be in the active database's scope.) - **PostgreSQL dialect.** Quote non-lowercase columns with double quotes. To write DuckDB/Postgres/Snowflake SQL instead, pass `--dialect` (server-side transpile, read-only queries) — details in **`hotdata-analytics`**. - Async runs return `query_run_id` → poll with `query status ` (do not re-run the same heavy SQL). `query status` exit codes: `0` succeeded, `1` failed, `2` still running (poll again), `3` succeeded but the result is a truncated/incomplete preview. -- **Large results are complete, not a preview.** The server returns inline rows only up to a bounded cap and persists the full set out-of-band; `hotdata query` transparently fetches the full result, so the printed rows and row count are the complete set. (If the full result can't be retrieved, the CLI prints the preview and a `warning:` to stderr.) +- **Large results: `-o csv` / `-o json` are complete, `-o table` is capped.** The server returns inline rows only up to a bounded cap and persists the full set under a `result_id`. For `csv` and `json` the CLI **streams** that full result batch by batch, so output size is unbounded and memory stays flat — pipe a big result to a file with `-o csv`/`-o json`, never `table`. For `table` the CLI fetches at most **10,000 rows** and the footer says `N of TOTAL rows — INCOMPLETE PREVIEW` (with `?` when the server sent no total); the process exits **3** so a pipeline cannot mistake it for the whole set. Same rules for `hotdata databases results get`. A result the server is still writing is waited for (up to 5 minutes, honoring `Retry-After`) rather than returned partial. If the full result can't be retrieved, the CLI prints the inline preview, a `warning:` to stderr, and exits 3. `-o json` carries `row_count`, `total_row_count`, and `truncated` — branch on `truncated`, not on the row count. +- **Numbers print exactly.** A `DECIMAL(38,2)` or other wide number is printed with every digit the service sent, in every output format; `-o json` emits it as an unquoted JSON number. One consequence: a list or struct cell prints on a single line in `-o json` (re-indenting would mean re-parsing and rounding it); scalars are unaffected. - **Backpressure is handled.** Under heavy concurrent load the server may shed a query with HTTP 429 (`OVERLOADED`); the CLI auto-retries (honoring `Retry-After`) before surfacing an error — no manual retry needed. - **OLAP** (aggregations, history, Chain, sorted indexes): **`hotdata-analytics`** skill. - **Search** (BM25, vector): **`hotdata-search`** skill. diff --git a/skills/hotdata/subskills/analytics/SKILL.md b/skills/hotdata/subskills/analytics/SKILL.md index cc8eca6..d29de33 100644 --- a/skills/hotdata/subskills/analytics/SKILL.md +++ b/skills/hotdata/subskills/analytics/SKILL.md @@ -26,7 +26,8 @@ hotdata query status - Use **`hotdata databases tables list`** for schema discovery — not `information_schema` via `query`. - Fully qualified names: `..`, `..
`. - **Query scope:** every query runs inside one instant database (active or `--database`); it sees that database's own catalog plus whatever is **attached** to it only. To read another database's table, or **join your own table against one**, attach that database first: `hotdata databases attach ` — see **`hotdata`** skill → [Querying across databases](../../SKILL.md#querying-across-databases-attach). No instant database set → *"a database is required."* -- Long-running queries may return `query_run_id` → poll with **`query status`** (exit `2` = still running). Do not re-run identical heavy SQL while polling. +- Long-running queries may return `query_run_id` → poll with **`query status`** — exit `0` succeeded, `1` failed, `2` still running (poll again), `3` succeeded but the printed result is an incomplete/truncated preview. Do not re-run identical heavy SQL while polling. +- **Big result sets:** use `-o csv` or `-o json` — both stream the full persisted result with flat memory. `-o table` is a human view capped at 10,000 rows; past that the footer reads `N of TOTAL rows — INCOMPLETE PREVIEW` and the command exits `3`. Details in the **`hotdata`** skill → Execute SQL Query. - For **workspace-wide** joins and naming, load **context:DATAMODEL** when listed (`hotdata databases context list` → `show DATAMODEL`) — see **`hotdata`** skill. ### OLAP patterns @@ -45,11 +46,11 @@ Column names from CSV uploads may be case-sensitive — use double quotes when n ## Query run history -Uses the **active workspace only** (no `--workspace-id`; set with `hotdata workspaces use`). +Scoped to the **active database** (set with `hotdata databases use`); pass `-d/--database ` to target another, `-w/--workspace-id` for another workspace. ```bash -hotdata databases queries list [--limit ] [--cursor ] [--status ] [--output table|json|yaml] -hotdata databases queries [--output table|json|yaml] +hotdata databases queries list [-d ] [--workspace-id ] [--limit ] [--cursor ] [--status ] [--output table|json|yaml] +hotdata databases queries [-d ] [--workspace-id ] [--output table|json|yaml] ``` - `list` — status, duration, row count, SQL preview (default limit 20). Filter: `--status running,failed`. @@ -61,11 +62,11 @@ hotdata databases queries [--output table|json|yaml] ## Stored results ```bash -hotdata databases results list [--workspace-id ] [--limit ] [--offset ] [--output table|json|yaml] -hotdata databases results get [--workspace-id ] [--output table|json|csv] +hotdata databases results list [-d ] [--workspace-id ] [--limit ] [--offset ] [--output table|json|yaml] +hotdata databases results get [-d ] [--workspace-id ] [--output table|json|csv] ``` -- Prefer **`databases results get `** over re-running identical heavy queries. +- Prefer **`databases results get `** over re-running identical heavy queries. It renders like `query`: `csv`/`json` stream the whole result, `table` is capped at 10,000 rows and exits `3` when the result is larger. Results and query runs scope to the active database; pass `-d/--database ` to target another. - Query footers may include `[result-id: rslt...]`; also available from `databases queries `. - `databases results list --limit` defaults to **100** (max **1000**) — unlike `databases queries list`, which defaults to **20**. diff --git a/skills/hotdata/subskills/geospatial/SKILL.md b/skills/hotdata/subskills/geospatial/SKILL.md index cc021bf..56eb79b 100644 --- a/skills/hotdata/subskills/geospatial/SKILL.md +++ b/skills/hotdata/subskills/geospatial/SKILL.md @@ -18,9 +18,9 @@ All SQL below runs through the core CLI: hotdata query "" [--workspace-id ] [--database ] [--output table|json|csv] ``` -- **Fully qualify tables** as `..
` (or `..
` for an instant database) — every `
` placeholder below means a qualified name. +- **Fully qualify tables** as `..
`, where the catalog is the instant database's `--catalog` alias (or the alias of an attached database) — every `
` placeholder below means a qualified name. - **PostgreSQL dialect:** double-quote any non-lowercase identifier (e.g. `"GeoID"`). -- Discover candidate tables/columns with **`hotdata databases tables list`** (filter with `--schema`/`--table`) or **`hotdata databases tables list`** (tables inside an instant database) — see core skill. +- Discover candidate tables with **`hotdata databases tables list`** (filter with `--schema`/`--table`) and columns with **`hotdata databases tables show `** — see core skill. --- From 8b085c37feb23f6467be70788816d19a9662eb43 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:04:02 -0700 Subject: [PATCH 2/3] build(release): gate every release and tag on the agent skills scripts/check-skills.sh runs at the start of release.sh prepare and again before finish pushes the tag. It fails when src/ changed since the last v* tag but skills/ did not (SKIP_SKILL_DRIFT=1 to override a verified skill-neutral release), when the built binary exposes a subcommand no skill names, and on finish when a SKILL.md version disagrees with Cargo.toml. The coverage check found two commands documented only under an alias: databases show and databases query status now appear in the core skill. --- docs/RELEASING.md | 14 +++++- scripts/check-skills.sh | 107 ++++++++++++++++++++++++++++++++++++++++ scripts/release.sh | 10 +++- skills/hotdata/SKILL.md | 8 ++- 4 files changed, 133 insertions(+), 6 deletions(-) create mode 100755 scripts/check-skills.sh diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 9ee16f1..8a5e5c4 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -8,7 +8,7 @@ Releases use a two-phase workflow wrapping [`cargo-release`](https://github.com/ scripts/release.sh prepare ``` -Creates a `release/` branch, bumps the version, updates `CHANGELOG.md`, pushes the branch, and opens a pull request. +Runs the skill check (below), then creates a `release/` branch, bumps the version (including the `version:` line in every `SKILL.md`), updates `CHANGELOG.md`, pushes the branch, and opens a pull request. **Phase 2 — finish** @@ -16,4 +16,14 @@ Creates a `release/` branch, bumps the version, updates `CHANGELOG.md`, scripts/release.sh finish ``` -Switches to `main`, pulls latest, tags the release, and triggers the dist workflow. +Switches to `main`, pulls latest, runs the skill check with `--require-version`, tags the release, and triggers the dist workflow. + +## Skill check + +Both phases run `scripts/check-skills.sh`. The agent skills under `skills/` ship in every release (`skills.tar.gz`, `SKILL.md`), so the script refuses to release when they lag the code: + +- **Drift** — if `src/` or `README.md` changed since the last `v*` tag and `skills/` did not, the run fails and lists the commits to review. Update the skills and commit, or set `SKIP_SKILL_DRIFT=1` when the release is verified to be skill-neutral. +- **Coverage** — every subcommand the built binary exposes must appear as `hotdata ` somewhere in `skills/**/*.md`. +- **Version** (`finish` only) — every `SKILL.md` frontmatter `version:` must equal the crate version. + +The drift check is a reminder, not a judge of prose. Before `prepare`, read the commits since the last tag and update `skills/hotdata/SKILL.md` and the subskills to match the current `--help` output and behavior. diff --git a/scripts/check-skills.sh b/scripts/check-skills.sh new file mode 100755 index 0000000..ca8688b --- /dev/null +++ b/scripts/check-skills.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# check-skills.sh — refuse to release with agent skills that lag the code. +# +# Runs before a release branch is cut and again before the tag is pushed. +# Three checks, all mechanical: +# +# 1. Drift: if src/ changed since the last release tag, skills/ must have +# changed too. The script cannot judge prose, so this is the reminder +# that a code change needs its SKILL.md follow-up. Override with +# SKIP_SKILL_DRIFT=1 when a release is verified to be skill-neutral. +# 2. Coverage: every subcommand the built binary exposes must be named in +# skills/**/*.md, so a new command cannot ship undocumented. +# 3. Version: every SKILL.md frontmatter `version:` must equal Cargo.toml +# (finish phase only — prepare runs before cargo-release bumps them). +# +# Usage: +# scripts/check-skills.sh [--require-version] + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +REQUIRE_VERSION=0 +[ "${1:-}" = "--require-version" ] && REQUIRE_VERSION=1 + +BIN="${HOTDATA_BIN:-target/debug/hotdata}" +SKILL_FILES=(skills/hotdata/SKILL.md skills/hotdata/subskills/*/SKILL.md) +fail=0 + +# --- 1. drift --------------------------------------------------------------- +LAST_TAG="$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || true)" +if [ -z "$LAST_TAG" ]; then + echo "→ skills: no previous v* tag; skipping drift check" +else + src_changes="$(git log --oneline "$LAST_TAG"..HEAD -- src/ README.md | grep -v -E 'chore\(deps\)|chore: Release' || true)" + skill_changes="$(git log --oneline "$LAST_TAG"..HEAD -- skills/ || true)" + if [ -n "$src_changes" ] && [ -z "$skill_changes" ]; then + if [ "${SKIP_SKILL_DRIFT:-}" = "1" ]; then + echo "→ skills: code changed since $LAST_TAG without a skills change (SKIP_SKILL_DRIFT=1, continuing)" + else + echo "error: code changed since $LAST_TAG but skills/ did not." >&2 + echo "" >&2 + echo "$src_changes" | sed 's/^/ /' >&2 + echo "" >&2 + echo "Review each commit against skills/hotdata/SKILL.md and the subskills, update them," >&2 + echo "and commit. If none of these change agent-visible behavior, rerun with SKIP_SKILL_DRIFT=1." >&2 + fail=1 + fi + else + echo "→ skills: drift check ok (since $LAST_TAG)" + fi +fi + +# --- 2. coverage ------------------------------------------------------------ +if [ ! -x "$BIN" ]; then + echo "→ skills: building $BIN for the command inventory..." + cargo build -q +fi + +# Walk the clap tree: " []", leaf commands only. +list_subcommands() { + "$BIN" "$@" --help 2>/dev/null \ + | awk '/^Commands:/{f=1;next} /^$/{f=0} f && $1!="help" {print $1}' +} +leaves=() +walk() { + local path=("$@") + local subs + subs="$(list_subcommands "${path[@]}")" + if [ -z "$subs" ]; then + [ ${#path[@]} -gt 0 ] && leaves+=("${path[*]}") + return + fi + local s + for s in $subs; do walk "${path[@]}" "$s"; done +} +walk + +skill_text="$(cat "${SKILL_FILES[@]}" skills/hotdata/references/*.md skills/hotdata/subskills/*/references/*.md)" +missing=() +for leaf in "${leaves[@]}"; do + if ! grep -qF "hotdata $leaf" <<<"$skill_text"; then + missing+=("$leaf") + fi +done +if [ ${#missing[@]} -gt 0 ]; then + echo "error: commands the CLI exposes but no skill mentions:" >&2 + printf ' hotdata %s\n' "${missing[@]}" >&2 + fail=1 +else + echo "→ skills: coverage ok (${#leaves[@]} commands documented)" +fi + +# --- 3. version ------------------------------------------------------------- +if [ "$REQUIRE_VERSION" = 1 ]; then + crate="$(grep -E '^version = ' Cargo.toml | head -1 | sed -E 's/^version = "([^"]+)".*/\1/')" + for f in "${SKILL_FILES[@]}"; do + v="$(sed -n 's/^version: //p' "$f" | head -1)" + if [ "$v" != "$crate" ]; then + echo "error: $f declares version $v, Cargo.toml is $crate" >&2 + fail=1 + fi + done + [ "$fail" = 0 ] && echo "→ skills: version ok ($crate)" +fi + +exit $fail diff --git a/scripts/release.sh b/scripts/release.sh index fdd3215..cd90c5f 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -2,8 +2,8 @@ # release.sh — two-phase release wrapper around cargo-release # # Usage: -# scripts/release.sh prepare # branch, bump, changelog PR -# scripts/release.sh finish # tag only (main is branch-protected) +# scripts/release.sh prepare # skill check, branch, bump, changelog PR +# scripts/release.sh finish # skill check, tag only (main is branch-protected) set -euo pipefail @@ -45,6 +45,9 @@ case "$COMMAND" in require_clean_tree + echo "→ Checking agent skills against the code..." + scripts/check-skills.sh + echo "→ Creating branch $BRANCH" git checkout -b "$BRANCH" @@ -99,6 +102,9 @@ case "$COMMAND" in echo "" echo "→ Release version from Cargo.toml: $VERSION (tag $TAG)" + echo "→ Checking agent skills against the code and version..." + scripts/check-skills.sh --require-version + if git rev-parse "$TAG" >/dev/null 2>&1; then echo "error: tag $TAG already exists locally. Delete it or pick a new version." >&2 exit 1 diff --git a/skills/hotdata/SKILL.md b/skills/hotdata/SKILL.md index 7e23ed8..adb6ece 100644 --- a/skills/hotdata/SKILL.md +++ b/skills/hotdata/SKILL.md @@ -105,7 +105,7 @@ hotdata databases fork [] [--name ] [--expires-at ] [--forks-limit ] [--workspace-id ] [--output table|json|yaml] hotdata databases use hotdata databases unset -hotdata databases [--workspace-id ] [--output table|json|yaml] +hotdata databases show [--workspace-id ] [--output table|json|yaml] # or the shorthand: hotdata databases hotdata databases remove [--workspace-id ] # Attach another database so its tables are queryable (enables cross-database queries — see below) @@ -130,7 +130,7 @@ hotdata databases tables remove
[--database ] [--schema public] [--w - `lineage` — renders a database's **whole fork family tree**, walked from the root down (defaults to the active database; one lineage request per database, fine for the small families forks produce in practice): every reachable generation, with the queried database marked `← this database`. Lineage is a historical record, not a live link — the databases stay independent, and a **deleted** generation stays in the chain (marked `deleted`), though its own fork list can't be enumerated: such branches end with `⋯ forks unknown`. Forks made before the server recorded lineage carry none. `--forks-limit ` pages each database's direct-fork list (server clamps to 1–100); a truncated branch closes with `⋯ N more`. `-o json`/`yaml` return `{database_id, root_id, tree}` with a recursive `tree` node. - `use` — saves the database **id** as the active database. Subsequent `databases tables` and `databases context` commands use it automatically. Note that a successful `fork` also updates this: the fork becomes the active database. - `unset` — clears the active database from config. -- `` — inspect one database (returns id, catalog, name, expires_at; a fork also shows its `forked_from` record). +- `show ` / `` — inspect one database (returns id, catalog, name, expires_at, attached databases; a fork also shows its `forked_from` record). - `remove` — removes the instant database; clears the active-database config if it matched. - `load` (top-level shorthand) — loads a file into `--catalog.--schema.--table`. Accepts `--file`, `--url`, `--upload-id`, or `--result-id` (load a saved query result by id — from `hotdata databases results` or a query's `[result-id: …]` footer — instead of a file; the result must belong to the target database). **Formats:** csv, json (`.json`/`.jsonl`/`.ndjson`), and parquet; the format comes from the file's extension, and `--format` overrides it (needed when the extension is absent or misleading). An unrecognised extension is not rejected — the server reads the bytes, and a file that plainly opens a json array is taken as json even without an extension. A json source in any shape (array, pretty-printed, one object per line) is reshaped locally to newline-delimited json before upload; an already-newline-delimited file is uploaded untouched. A table or schema that was never declared is declared by the server as part of the load, so no up-front `--table` is required. - **Load modes** (`--mode`, default `replace`) — `replace` supersedes the table's contents; `append` adds rows; `delete`, `update`, and `upsert` match existing rows **by key**. `--append` is the old shorthand for `--mode append` and still works, but the two cannot be combined. The keyed modes need a key: declare one with `databases tables add --key`, or name it per-load with `--key` (repeat for a composite key). `delete` uploads only the key columns; `update` replaces matching rows and ignores unmatched ones; `upsert` inserts the unmatched instead. Keyed modes are not available with `--result-id`. @@ -249,6 +249,10 @@ hotdata databases context push [--database ] [--dry-run] ``` hotdata query "" [--workspace-id ] [--database ] [--dialect hotsql|duckdb|postgres|snowflake] [--output table|json|csv] hotdata query status + +# Same commands under the databases group (identical flags and exit codes) +hotdata databases query "" [-d ] [--output table|json|csv] +hotdata databases query status ``` - Default output is `table` (row count and execution time). From f4f640b8c5dc5369794c28942589f91081d6aba9 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:19:26 -0700 Subject: [PATCH 3/3] build(ci): check flag coverage too, and run the skill gate on every PR The command-coverage check let a flag ship undocumented; it now also requires every long flag in a leaf command's --help to be named in a skill. That found eight: the Kafka/Delta/continuous shorthands on ingest create (--topic, --table-path, --stream, --dest-table-prefix), --include-deleted on both ingest listings, --ingest-id on ingest logs, and --message on support report. All are documented now. CI runs the gate on pull requests with the drift check skipped, so a new command or flag cannot merge without its skill text; the drift check stays in release.sh where it judges a whole release. --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ docs/RELEASING.md | 2 +- scripts/check-skills.sh | 30 +++++++++++++++++++++++++++--- skills/hotdata/SKILL.md | 25 ++++++++++++++++++------- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d889f9..f3cd255 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,36 @@ jobs: git fetch origin "$BASE_REF" python3 scripts/validate-changelog.py "origin/$BASE_REF" + skills: + # The agent skills ship in every release. Fail a PR that adds a command or + # flag without documenting it. The drift check (code changed since the last + # tag, skills did not) is left to release.sh, where it has a whole release + # to judge rather than one PR. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Install Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + + - name: Cache cargo + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Check skills cover every command and flag + env: + SKIP_SKILL_DRIFT: "1" + run: scripts/check-skills.sh --require-version + fmt: runs-on: ubuntu-latest steps: diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 8a5e5c4..c6f2d03 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -23,7 +23,7 @@ Switches to `main`, pulls latest, runs the skill check with `--require-version`, Both phases run `scripts/check-skills.sh`. The agent skills under `skills/` ship in every release (`skills.tar.gz`, `SKILL.md`), so the script refuses to release when they lag the code: - **Drift** — if `src/` or `README.md` changed since the last `v*` tag and `skills/` did not, the run fails and lists the commits to review. Update the skills and commit, or set `SKIP_SKILL_DRIFT=1` when the release is verified to be skill-neutral. -- **Coverage** — every subcommand the built binary exposes must appear as `hotdata ` somewhere in `skills/**/*.md`. +- **Coverage** — every subcommand the built binary exposes must appear as `hotdata ` somewhere in `skills/**/*.md`, and every long flag in its `--help` must be mentioned too. CI runs this check on every pull request (with the drift check skipped), so a new command or flag cannot merge undocumented. - **Version** (`finish` only) — every `SKILL.md` frontmatter `version:` must equal the crate version. The drift check is a reminder, not a judge of prose. Before `prepare`, read the commits since the last tag and update `skills/hotdata/SKILL.md` and the subskills to match the current `--help` output and behavior. diff --git a/scripts/check-skills.sh b/scripts/check-skills.sh index ca8688b..de84697 100755 --- a/scripts/check-skills.sh +++ b/scripts/check-skills.sh @@ -8,8 +8,10 @@ # changed too. The script cannot judge prose, so this is the reminder # that a code change needs its SKILL.md follow-up. Override with # SKIP_SKILL_DRIFT=1 when a release is verified to be skill-neutral. -# 2. Coverage: every subcommand the built binary exposes must be named in -# skills/**/*.md, so a new command cannot ship undocumented. +# 2. Coverage: every subcommand the built binary exposes, and every long +# flag in its --help, must be named in skills/**/*.md, so a new command +# or flag cannot ship undocumented. Hidden flags are not in --help and +# are not checked. # 3. Version: every SKILL.md frontmatter `version:` must equal Cargo.toml # (finish phase only — prepare runs before cargo-release bumps them). # @@ -88,7 +90,29 @@ if [ ${#missing[@]} -gt 0 ]; then printf ' hotdata %s\n' "${missing[@]}" >&2 fail=1 else - echo "→ skills: coverage ok (${#leaves[@]} commands documented)" + echo "→ skills: command coverage ok (${#leaves[@]} commands documented)" +fi + +# Long flags, per leaf, minus the globals every command carries. +GLOBAL_FLAGS='^--(api-key|no-input|help|output|workspace-id)$' +missing_flags=() +flag_count=0 +for leaf in "${leaves[@]}"; do + # shellcheck disable=SC2086 + flags="$("$BIN" $leaf --help 2>/dev/null \ + | grep -oE '^\s+(-[a-zA-Z], )?--[a-z][a-z0-9-]+' \ + | grep -oE -- '--[a-z][a-z0-9-]+' | sort -u | grep -vE "$GLOBAL_FLAGS" || true)" + for f in $flags; do + flag_count=$((flag_count + 1)) + grep -qF -- "$f" <<<"$skill_text" || missing_flags+=("hotdata $leaf $f") + done +done +if [ ${#missing_flags[@]} -gt 0 ]; then + echo "error: flags in --help that no skill mentions:" >&2 + printf ' %s\n' "${missing_flags[@]}" >&2 + fail=1 +else + echo "→ skills: flag coverage ok ($flag_count flags documented)" fi # --- 3. version ------------------------------------------------------------- diff --git a/skills/hotdata/SKILL.md b/skills/hotdata/SKILL.md index adb6ece..bf4d62e 100644 --- a/skills/hotdata/SKILL.md +++ b/skills/hotdata/SKILL.md @@ -330,7 +330,7 @@ hotdata ingest sources add --family filesystem --bucket-url s3://events-prod # They merge with --config, flag last. --no-wait returns without watching the # new source settle; the wait is a poll and starts nothing. -hotdata ingest sources list [--family sql] [--state active] # ids, families, states +hotdata ingest sources list [--family sql] [--state active] [--include-deleted] # ids, families, states hotdata ingest sources show # state, config version, discovery hotdata ingest sources update-config --config @source.json # Appends an immutable config version under the SAME id and moves the pointer. @@ -369,11 +369,22 @@ hotdata ingest create --source "prod postgres" --table orders --schema public \ # --schema source schema (sql) # --format csv|jsonl|parquet, --glob "**/*.parquet" (bucket sources) # --record-shape otel_traces|mqtt_observations (bucket sources) -# --all everything under a bucket root (needs --format) +# --topic Kafka topic, REPEATABLE — topics live on the ingest, +# not the datasource, which is the cluster (kafka) +# --table-path ONE Delta table under the datasource root, e.g. +# warehouse/orders — name it with --dest-table (delta) +# --all everything the datasource exposes (buckets need --format) # --limit N stop after N source rows +# --stream shorthand for --type continuous (still needs --every) # Destination flags instead of --destination: -# --database-id (required) --dest-table (defaults to the single --table) -# --dest-schema (default public) --write-mode (default replace) +# --database-id (required) --dest-schema (default public) --write-mode (default replace) +# --dest-table for sources that land ONE table: buckets, delta, +# --raw-sql (defaults to --table there) +# --dest-table-prefix

for sources that land SEVERAL (sql --table/--sql, +# iceberg, ducklake, kafka, rest): `orders` lands as +# `

_orders`. Optional, but only one prefix-less +# ingest can own a database+schema — a second one +# must pick a prefix or it overwrites the first. hotdata ingest create --datasource-id ds_01J --database-id db_123 \ --sql "SELECT id, status FROM public.orders WHERE status = 'open' LIMIT 1000" @@ -387,7 +398,7 @@ hotdata ingest create --datasource-id ds_01J --database-id db_123 \ # CTEs, window functions. Only the result set transfers, into --table. (A query # has no source table, so --table names where the result lands.) -hotdata ingest list [--datasource-id ds_01J] [--type continuous] [--state active] +hotdata ingest list [--datasource-id ds_01J] [--type continuous] [--state active] [--include-deleted] hotdata ingest show hotdata ingest pause # stops the active run AND future runs hotdata ingest resume # clears the stop; starts NOTHING immediately @@ -395,7 +406,7 @@ hotdata ingest schedule --every 5m [--next now] hotdata ingest remove # releases the destination table; data untouched # --- runs -------------------------------------------------------------------- -hotdata ingest logs [--status failed] # every attempt, newest first +hotdata ingest logs [--status failed] # every attempt, newest first (--ingest-id is the flag form) hotdata ingest run # exits 0 succeeded / 1 failed|cancelled / 2 in flight # --wait on either polls to a terminal status (--wait-timeout, default 300s; # exit 2 on timeout). It WATCHES: the scheduler owns dispatch, so waiting cannot @@ -473,7 +484,7 @@ hotdata auth logout # Remove saved auth for the default profile hotdata support report -m "" --subject "" [--kind bug|question|billing|feature|account|other] [--severity urgent|high|medium|low] [-w | --no-workspace] [--logs |-] [--context KEY=VALUE ...] [-o table|json|yaml] ``` -Files a support ticket via the API — no browser needed. `-m`/`--subject` are required together for non-interactive use (agents: always pass both); omit both in an interactive terminal to compose in `$EDITOR` instead. Attaches the active workspace by default (`--no-workspace` to omit, `-w` for a specific one); `--logs` reads a file or `-` for stdin (cap 256 KiB); `--context key=value` adds extra diagnostic pairs (repeatable, max 20). Prints the ticket's `public_id` on success — replies go to the email on the HotData account, not to the CLI. +Files a support ticket via the API — no browser needed. `-m`/`--message` and `--subject` are required together for non-interactive use (agents: always pass both); omit both in an interactive terminal to compose in `$EDITOR` instead. Attaches the active workspace by default (`--no-workspace` to omit, `-w` for a specific one); `--logs` reads a file or `-` for stdin (cap 256 KiB); `--context key=value` adds extra diagnostic pairs (repeatable, max 20). Prints the ticket's `public_id` on success — replies go to the email on the HotData account, not to the CLI. ## Workflows