* Capture session paths via SessionStart hook + lock parallel pushes Replace the encoding-based scan of ~/.claude/projects/<encoded-cwd>/ with a queue file populated by a new `agnes capture-session` SessionStart hook. The hook reads the documented `transcript_path` field from Claude Code's hook stdin JSON, sidestepping the cwd-to-folder encoding (which is an internal implementation detail and varies by Claude Code version). - New `agnes capture-session` subcommand appends transcript_path to <workspace>/.claude/agnes-sessions.txt. Silent on all malformed input so a hook chain failure doesn't clutter Claude Code startup. - `agnes push` now consumes the queue: atomic snapshot rename guards against hooks writing during the push window, successful uploads land in agnes-sessions-uploaded.txt (TSV: timestamp + path), failed paths are requeued. - Cross-platform single-instance lock via the filelock package (fcntl on POSIX, msvcrt on Windows). Concurrent SessionEnd hooks — common when the user closes several sessions at once — silent-exit on the losing side instead of all racing the upload. - Recovery: pre-existing snapshot files from a crashed push are picked up and processed before the live queue. - The SessionStart `agnes push` self-heal entry is dropped — it became redundant once the queue persists across runs (orphans from headless / crashed sessions ship out on the next interactive SessionEnd push). Existing workspaces auto-migrate via the marker-based replace logic. - Legacy encoding scan stays available behind `--legacy-scan` for one- off backfills of sessions predating the queue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add /agnes-private + statusLine indicator for private sessions Users handling sensitive data inside Claude Code can now opt a session out of the Agnes upload pipeline, either proactively (right after session start) or reactively (mid-session). The `/agnes-private` slash command runs `agnes mark-private` deterministically via `!`-prefix direct bash — no AI in the loop. A workspace-installed statusLine surfaces a `🔒 agnes-private` indicator in Claude Code's status bar so the user sees the state at a glance. Authoritative source of "do not upload" is a separate file `<workspace>/.claude/agnes-sessions-private.txt` (one session_id per line). Both `capture-session` (queue writer) and `push` (queue reader) consult the list. This makes the slash-command / SessionStart-hook race impossible by construction: whichever runs first, the session is correctly filtered out. - `agnes mark-private` reads `CLAUDE_CODE_SESSION_ID` from env (set by Claude Code in every bash subprocess it spawns — stable documented API) and appends to the private list. - `agnes statusline` reads the session JSON Claude Code pipes on stdin, checks the private list, and emits the indicator or nothing. Optimized for the high call frequency of statusLine renders. - `capture-session` extracts session_id from hook stdin and skips queue write when the ID is already on the private list (race protection). - `push` filters snapshot entries by the private list and appends to a per-workspace audit log `agnes-sessions-private-skipped.txt`. - Queue format migrated from `<path>` to `<session_id>\t<path>`; legacy one-column lines still parse (empty session_id, still upload, can't be marked private retroactively — fine, they pre-date the feature). - `install_claude_hooks` writes a workspace statusLine unless the user already has a custom one (warn + preserve). Idempotent re-init. - `install_claude_commands` ships `agnes-private.md` alongside `update-agnes-plugins.md`. Per-template fallback so a missing template doesn't get clobbered with the wrong content. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix setup-prompt + CLAUDE.md marketplace copy + drop skills step Three issues against the post-PR-#240 / post-PR-#237 state: 1. Setup prompt's marketplace block trailer (both has-stack and empty-stack variants) claimed the SessionStart hook keeps the marketplace clone in sync via `agnes refresh-marketplace --quiet` on every session and that admin grants land automatically — both false since PR #237 (0.47.x) moved the install/update path out of the hook into the `/update-agnes-plugins` slash command. The hook is `--check`-only: detects server-side changes, prompts the user to run the slash command, which does the full reconcile interactively with output visible in the transcript. 2. The empty-stack variant framed composition as "admin grants only", missing the actual three-source served stack: (admin RBAC ∩ /marketplace subscriptions) ∪ system-mandatory plugins (admin-pinned, auto-applied) ∪ Flea market installs (skills/agents bundled, plugins standalone) Updated copy spells out all three sources so analysts know where their stack picks live, and what the SessionStart hook actually does on change detection. 3. CLAUDE.md template's "Agnes Marketplace" section conflated eligibility (`resolve_allowed_plugins` — what's listed) with served stack (`resolve_user_marketplace` — what actually reaches Claude Code). The two are different: a user can be RBAC-eligible for a plugin without having subscribed to it on /marketplace. Rewrote the section to distinguish the eligibility set from the served stack and to describe the `--check`-only hook accurately. Plus: deleted the setup prompt's interactive Skills step (final step before Confirm). The named-opinion question — "do you want me to bulk-copy every skill into ~/.claude/skills/agnes/ or pull on-demand via `agnes skills show <name>`?" — had no obvious right answer for new users at the tail end of a wall of technical steps. On-demand lookup is the one-size-fits-all default; `agnes skills list/show` remain discoverable and the CLAUDE.md template references specific skills inline (e.g. agnes-data-querying in the BigQuery section) where they're relevant. Layout: Confirm shifts from step 9 to step 8. Tests updated, full setup/marketplace/welcome surface green (115 passed). Remaining full-suite failures are pre-existing (BQ/Keboola fixtures, Windows charmap collection error in test_v26_keboola_e2e) — verified against a clean stash, unrelated to this diff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix session-queue race + snapshot PID-reuse data loss Two blocker fixes from the PR #242 review: 1. Concurrent SessionStart hooks could corrupt the queue file on Windows. Python's `open(path, "a")` is not atomic there — the CRT does not pass FILE_APPEND_DATA to CreateFile, so concurrent appenders (user opening several Claude Code windows simultaneously) could interleave bytes mid-line. The malformed lines then silently fail the parser and the entries are dropped. Fix: wrap append_to_queue, requeue_failed, and snapshot_queue in a short-lived FileLock on a dedicated `agnes-queue.lock`. Separate from `agnes-push.lock` so capture-session hooks don't block on the push command. New test_append_concurrent_threads_no_corruption reproduces the race with 4 threads x 50 appends. 2. Snapshot filenames embedded only the PID (`agnes-sessions.snapshot. <PID>.txt`). After a crashed push left a snapshot on disk and the OS recycled the PID for a new push, `os.rename` would atomically overwrite the recovery snapshot — every entry in it lost, silently. Fix: append a uuid8 hex tail (`agnes-sessions.snapshot.<PID>. <uuid8>.txt`). find_recovery_snapshots already globs the prefix so it picks up both old and new format. New test_snapshot_filename_is_unique_per_call asserts two consecutive snapshots under the same PID don't collide. Targeted tests green (47/47 in session_queue/capture_session/cli_push). Full suite failures unchanged from baseline (pre-existing BQ/Keboola fixture issues per CLAUDE.md). * Auto-refresh workspace hooks + bash-wrap all hook entries (Windows) Fixes from PR #242 second review (ZdenekSrotyr): 1. `uv.lock` regenerated to include `filelock 3.29.0` (declared in pyproject.toml but missing from the lock file — CI's lockfile-consistency check would fail; `uv pip install` on a clean cache would silently miss the dep). 2. `agnes self-upgrade` now auto-refreshes the workspace Claude Code hooks via the new `cli.lib.hooks.maybe_refresh_claude_hooks`. Closes the silent-stop migration gap: a v0.48 workspace would auto-upgrade the CLI from its existing SessionStart self-upgrade entry but never pick up the new `agnes capture-session` SessionStart hook, leaving the queue empty and `agnes push` uploading nothing. The refresh fires on both the "info is None" fast path (CLI already current — catches the second SessionStart after a prior upgrade) and the install-success path. Guarded by `workspace_has_agnes_hooks` so it never writes `.claude/settings.json` into directories that aren't Agnes workspaces (e.g. `agnes self-upgrade` invoked from `~/`). Errors are surfaced on stderr but never flip the upgrade exit code. 3. All Agnes-managed hooks are now wrapped in `bash -c "..."`. The self-upgrade+pull chained SessionStart entry was the only one still shipping unwrapped — Claude Code on Windows runs hook commands directly without a shell, so the `;` chain + `2>/dev/null` + `|| true` shell syntax silently no-op'd on native Windows installs without Git Bash on PATH. Workspaces still on the old form auto-upgrade via the refresh path above. Tests: +12 in test_lib_hooks.py (guard semantics, v0.48→v0.49 migration end-to-end, third-party-hook preservation, bash-wrap invariant). +5 in test_self_upgrade.py (refresh fires on info=None, fires on install success, skipped on failure, skipped on --check-only, refresh failure never flips exit code). 130 targeted tests green. The 2 pre-existing Windows path-separator failures in `test_smoke_test_detects_version_mismatch[uv|pip]` are unrelated (path mismatch `\fake\uv\bin\agnes` vs `/fake/uv/bin/agnes` in test asserts, pre-PR baseline). * CHANGELOG: document PR-242 main features Closes ZdenekSrotyr #4: the [Unreleased] block was missing entries for the PR's primary surface — only the post-merge fix bullets and the unrelated setup-prompt copy change were captured. Adds: - ### Added: 6 bullets covering the session capture queue + new `agnes capture-session` subcommand, `/agnes-private` slash + `agnes mark-private`, `agnes statusline` + statusLine wiring, `--legacy-scan` opt-in fallback, single-instance push lock, and the new `filelock` runtime dep. - ### Changed: BREAKING bullet on the SessionStart / SessionEnd hook wire format change (capture-session as first SessionStart entry, push self-heal removed, SessionEnd push detached via nohup, all entries bash-wrapped). Folds the prior standalone bash-wrap bullet into this consolidated entry — Z's review flagged the layout shift as BREAKING, and grouping the related sub-changes makes the migration story readable in one place. - Operator migration is auto-handled by `maybe_refresh_claude_hooks` invoked from `agnes self-upgrade` (separate Changed entry below). No `agnes init` re-run required. Pre-queue session jsonls on upgrading workspaces still need a one-off `agnes push --legacy-scan` — flagged in the BREAKING bullet. No code change; doc only. * Drop permanent 4xx uploads instead of requeueing forever Closes ZdenekSrotyr #5. Previously the push retry path requeued any non-200 response except the literal "file not found on disk", so 401 (token expired), 403 (RBAC denial), 413 (payload too large), 400 (server-side validation) cycled through every push run forever — the queue grew without bound and each run re-bombarded the server with the same deterministically-failing upload. Now 4xx (except 408 Request Timeout + 429 Too Many Requests, which the HTTP spec marks as transient) is dropped and audit-logged to `<workspace>/.claude/agnes-sessions-failed.txt`: <iso_ts>\t<session_id>\t<status>\t<transcript_path> 5xx and network errors continue to requeue — those reflect server / transport state that can change between runs, so retry is the right behavior. The audit log piggybacks on the push single-instance lock (agnes-push.lock) — push is the only writer to this file, same as the existing `mark_uploaded` and `mark_private_skipped` paths, so no separate filelock is needed. `agnes push --json` surfaces a new `dropped_permanent` counter; non- quiet stdout mentions the audit-log path so operators tailing the output have a pointer to the forensic trail. Tests: +7 in test_cli_push.py (401/400/403/413 → drop; 408/429 → requeue; 500/502/503 → requeue; network exception → requeue; --json `dropped_permanent` counter; stdout audit-log pointer). +1 in test_session_queue.py (mark_failed_permanent TSV format). 127/129 targeted tests green. The 2 pre-existing Windows path-separator failures in `test_smoke_test_detects_version_mismatch [uv|pip]` are unrelated (path mismatch `\fake\uv\bin\agnes` vs `/fake/uv/bin/agnes` in test asserts, pre-PR baseline). * Catch OSError in push lock acquisition Closes ZdenekSrotyr #8. `acquire_or_skip` in `cli/lib/push_lock.py` previously caught only `filelock.Timeout`. Any `OSError` from `FileLock.acquire` — read-only filesystem, permission denied on `.claude/`, disk full, hardware I/O failure — propagated as an unhandled traceback. Two visible failure modes: - SessionEnd hook: `|| true` in the wrapper swallowed the error, so daily pushes silently never ran. Operator had no signal. - Manual `agnes push`: ugly Python traceback dumped to the terminal instead of a clean exit. Now `OSError` is treated the same as `Timeout` — yield `None`, caller returns cleanly with rc=0. The operator's environment in these scenarios has bigger problems than missing session uploads, so we swallow rather than retry-loop or surface a noisy warning. Test: `test_push_silent_exit_when_filelock_raises_oserror` patches the `FileLock` used inside `push_lock` to raise OSError on acquire, verifies push exits 0 with no traceback and the queue is preserved for the next attempt. * Address remaining S2 items from PR-242 review Four items from ZdenekSrotyr's S2 list: S2.10 — `_install_statusline` truthy check (cli/lib/hooks.py): replace `if existing:` with explicit `if existing is None or existing == "":`. Documents and tests the behavior for both edge cases (explicit-null and empty-string `statusLine`) — both treated as "not configured" rather than "explicit user opt-out", so we install ours. Two new tests in test_lib_hooks.py pin the contract. S2.6 — onboarding docs for /agnes-private. New "Private sessions" subsection in `config/claude_md_template.txt` (next to Data Sync) covering the slash command, statusbar indicator, and audit-log location. One-line tip in `app/web/setup_instructions.py` so the feature is discoverable at onboarding. S2.9 — e2e privacy test (tests/test_e2e_privacy.py). Wires capture_session → mark_private → push against a recording fake api_post and asserts zero session uploads for the marked one. Three cases: mark-before-capture (queue write skipped), mark-after-capture (push-side filter catches it + audit-logs), control (unmarked sessions upload normally). David #8 — `--legacy-scan` help text now documents the private-list gap (legacy entries carry empty session_id, so the filter is not consulted). The practical impact is bounded — pre-queue sessions cannot have been marked private since the private list is a queue-era feature — but the disclaimer in the help text means an operator running a backfill is not surprised. 68 targeted tests green (3 new e2e + 2 new truthy edge tests + existing). 2 pre-existing Windows path-separator failures in test_smoke_test_detects_version_mismatch[uv|pip] unchanged. Remaining S2 items (statusline mkdir push-back, capture-session silent-fail follow-up) handled in PR comment + follow-up issue respectively. * Address remaining S2 follow-ups (David #8, S2.7, David #11) Three items left over from Mina's bbf63472 batch — that commit addressed S2.6/S2.9/S2.10 + documented David #8 in help text but deferred the actual implementations of S2.7, David #11, and the real David #8 fix to follow-ups. This commit closes them. David #8 — `agnes push --legacy-scan` now consults the private list. Claude Code names jsonls `<session-id>.jsonl`, so the file stem IS the session id; the legacy-scan path can apply the same private filter the queue path uses. Both the dry-run and live-upload code paths fixed. Help text updated (no longer warns the filter is bypassed). Two new tests in test_cli_push.py cover the upload-skip path + the dry-run `would_skip_private` segregation. S2.7 — `statusline`/`is_private` no longer mkdir-pollutes arbitrary workdirs. Split `_claude_dir` into `_claude_dir_writable` (used only from `add_private`) and `_claude_dir_readonly` (no mkdir). The read-only public helpers (`private_list_path`, `read_all_private`, `is_private`) compose the no-mkdir variant by default; `add_private` opts in via `writable=True`. Added a process-local mtime-keyed cache around `read_all_private` so in-process callers (push doing one stat per upload candidate, future `agnes diagnose`) don't re-parse the file on every check. Cache eviction on `add_private` so a sub-second write+read sequence doesn't see stale data even on coarse-mtime filesystems. Two new tests pin the no-mkdir contract + the in-same-second add+read consistency. David #11 — `agnes capture-session` writes a breadcrumb log on every invocation. New `<workspace>/.claude/agnes-capture-session.log` TSV: `<iso_ts>\t<outcome>\t<detail>` where outcome covers every silent- exit path (`ok`, `private_skip`, `empty_stdin`, `bad_json`, `not_object`, `no_transcript_path`, `stdin_read_error`, `write_error`). Gives operators a signal to detect "hook fires but queue stays empty" — without it, an upstream Claude Code stdin- contract change is invisible because the hook always exits 0. Log rolls at 256 KiB so it doesn't grow unbounded on long-lived workspaces. Best-effort: a breadcrumb-write failure is itself swallowed so the hook contract stays "exit 0 always". Skipped in non-Agnes workdirs (no `.claude/` exists) so opening Claude Code in `~/` doesn't pollute it. Five new tests in test_capture_session.py cover the success / bad_json / no_transcript_path / private_skip / no-pollute paths. 115 targeted tests green (test_cli_push, test_capture_session, test_private_list, test_session_queue, test_e2e_privacy, test_lib_hooks, test_statusline, test_mark_private). --------- Co-authored-by: Minas Arustamyan <arustamyan.minas@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: ZdenekSrotyr <zdenek.srotyr@keboola.com>
297 lines
17 KiB
Text
297 lines
17 KiB
Text
{# Default analyst-onboarding workspace prompt for "agnes init".
|
||
Rendered server-side by src/claude_md.py. Edit this file to change
|
||
the OSS default; admins override per-instance via /admin/workspace-prompt.
|
||
|
||
Available context (see docs/agent-workspace-prompt.md for the full reference):
|
||
instance.name, instance.subtitle
|
||
server.url, server.hostname
|
||
sync_interval — string from instance.yaml
|
||
data_source.type — keboola | bigquery | local
|
||
tables — list of {name, description, query_mode}
|
||
metrics.count, metrics.categories
|
||
marketplaces — list of {slug, name, plugins:[{name}]}
|
||
user.id, user.email, user.name, user.is_admin, user.groups
|
||
now, today — datetime / date string
|
||
#}
|
||
# {{ instance.name }} — AI Data Analyst
|
||
|
||
This workspace is connected to {{ server.url }}.
|
||
{% if instance.subtitle %}Operated by **{{ instance.subtitle }}**.{% endif %}
|
||
|
||
> Looking for human-readable workspace docs? Open `AGNES_WORKSPACE.md` in this directory — that file documents what `agnes init` installed, where files live, and how to uninstall.
|
||
|
||
## Rules
|
||
- Before computing any business metric: run `agnes catalog --metrics --show <category>/<name>`
|
||
- **For canonical table list with query modes: `agnes catalog`.** Treat `agnes catalog` as source of truth (covers all `query_mode` values: `local`, `remote`, `materialized`).
|
||
- Do not use DESCRIBE/SHOW COLUMNS — use `agnes schema <table>` instead
|
||
- Sync data regularly with `agnes pull`
|
||
- **Personal customizations go in `.claude/CLAUDE.local.md`, NOT here.** This file is regenerated by `agnes init --force`; edits here will be lost. CLAUDE.local.md is preserved across regeneration and uploaded on `agnes push`.
|
||
|
||
## Metrics Workflow
|
||
1. `agnes catalog --metrics` — list registered metrics + categories
|
||
2. `agnes catalog --metrics --show <category>/<name>` — read the canonical SQL + business rules
|
||
3. Adapt the canonical SQL; never invent metric calculations
|
||
|
||
## Data Sync
|
||
- `agnes pull` — download current data from server
|
||
- `agnes push` — upload sessions and local notes to server
|
||
- Data on the server refreshes every {{ sync_interval }}
|
||
|
||
## Private sessions
|
||
|
||
If a Claude Code session covers something you do **not** want uploaded
|
||
to the Agnes server (sensitive PII walk-through, exploratory work that
|
||
shouldn't shape org-wide context, etc.), mark it private:
|
||
|
||
- Run `/agnes-private` from inside the session — the deterministic
|
||
`!`-prefix bash adds the current `CLAUDE_CODE_SESSION_ID` to
|
||
`<workspace>/.claude/agnes-sessions-private.txt`. Subsequent
|
||
`agnes push` runs skip its transcript and audit-log the skip to
|
||
`<workspace>/.claude/agnes-sessions-private-skipped.txt`.
|
||
- The Claude Code statusbar shows `🔒 agnes-private` while you are
|
||
in a private session, so the marking is visible at a glance.
|
||
- Marking before `capture-session` fires (the SessionStart hook that
|
||
queues the transcript) means the session never enters the upload
|
||
queue at all; marking after it fires is also safe — the push-side
|
||
re-check on the private list is the authoritative filter.
|
||
|
||
## Discovering tables — never enumerate from memory
|
||
|
||
Tables, columns, sizes, descriptions, and `query_mode` change as admins
|
||
register / migrate / drop entries. Always re-discover from the live server,
|
||
never from this file or your training data:
|
||
|
||
```
|
||
agnes catalog --json # all tables: id, query_mode, sql_flavor,
|
||
# where_examples, fetch_via, rough_size_hint, description
|
||
agnes catalog --json | jq '.tables[] | select(.id=="<id>")' # single table — read its description in full BEFORE writing any SQL
|
||
agnes schema <table> # columns + types in the right SQL dialect
|
||
agnes describe <table> -n 5 # sample rows (local + materialized only)
|
||
```
|
||
|
||
The `description` field on each catalog row is the **authoritative
|
||
business-rules text** for that table — it carries grain, partition
|
||
column, join contracts, and column-level gotchas. Re-read it from the
|
||
live `agnes catalog` for every cross-table decision; do **not** copy
|
||
it into this workspace `CLAUDE.md` (it's a snapshot that goes stale,
|
||
and `agnes init` will overwrite local edits — put personal notes into
|
||
`.claude/CLAUDE.local.md` instead). The CLI is the source of truth.
|
||
|
||
`rough_size_hint` is server-populated for `local` and `materialized` tables
|
||
(`small` ≤100 MiB, `medium` ≤1 GiB, `large` ≤10 GiB, `very_large` >10 GiB) and
|
||
`null` for `remote` rows. When `null`, treat the table as potentially large
|
||
and use `agnes snapshot create --estimate` to size-check before fetching.
|
||
|
||
{% if marketplaces -%}
|
||
## Agnes Marketplace — plugins available to you
|
||
|
||
The Agnes server publishes a per-user **`agnes`** Claude Code marketplace,
|
||
aggregated from upstream marketplaces and RBAC-filtered. The list below is
|
||
your **eligibility set** — plugins your account is allowed to add to its
|
||
stack on the `/marketplace` page. Eligibility ≠ installed.
|
||
|
||
What actually reaches Claude Code is your **served stack**, which is:
|
||
- plugins you opted in to on `/marketplace` (from the eligibility set above),
|
||
- plus any plugin your admin pinned as system-mandatory (auto-applied to every user),
|
||
- plus skills / agents / plugins you installed from the **Flea market** tab on `/marketplace`.
|
||
|
||
When you install or invoke a stack plugin inside Claude Code, address it
|
||
as `<plugin>@agnes` regardless of which upstream marketplace it came from
|
||
— e.g. `claude plugin install <plugin>@agnes`.
|
||
|
||
Updates: the SessionStart hook runs `agnes refresh-marketplace --check`
|
||
on every Claude Code session — it only **detects** server-side changes
|
||
(new admin grants, new system plugins, your own `/marketplace` picks) and
|
||
prompts you to run `/update-agnes-plugins` inside Claude Code to install
|
||
the diff. The slash command does the full reconcile with output visible
|
||
in the transcript; no silent auto-install at session start.
|
||
|
||
Upstream marketplaces folded into your `agnes` view (eligibility):
|
||
{% for mp in marketplaces -%}
|
||
- **{{ mp.name }}** ({{ mp.slug }}): {{ mp.plugins | map(attribute="name") | join(", ") }}
|
||
{% endfor %}
|
||
{% endif -%}
|
||
|
||
## Remote Queries (BigQuery) — when data isn't on the laptop
|
||
|
||
Not every table is synced. Tables registered with `query_mode: "remote"` live in
|
||
BigQuery, accessed server-side via DuckDB's BQ extension — no parquet on disk.
|
||
Tables you don't see in `server/parquet/` may still be queryable.
|
||
|
||
### Discovery first — read `agnes catalog --json` BEFORE every cross-table decision
|
||
|
||
`agnes catalog --json` returns one row per table with these fields. Use them; don't guess:
|
||
|
||
| Field | What it tells you | How to use it |
|
||
|---|---|---|
|
||
| `query_mode` | `local` (parquet on laptop) / `remote` (BQ on demand) / `materialized` (synced parquet of a BQ result) | Picks the tool — see decision tree below |
|
||
| `source_type` | `keboola` / `bigquery` / `jira` | Determines SQL dialect |
|
||
| `sql_flavor` | `duckdb` for local sources, `bigquery` for `--remote` queries on BQ rows | What syntax `--where` expects |
|
||
| `where_examples` | 1–3 example WHERE predicates that are valid for this table's dialect | Copy as starting point for `--where` |
|
||
| `fetch_via` | Pre-formatted `agnes snapshot create …` template for this table | The canonical "how do I get a slice of this table" command |
|
||
| `rough_size_hint` | Coarse size hint (`small` / `medium` / `large` or null when unknown) | Bigger than `medium` → never `agnes query --remote` without a tight `--where`; use `agnes snapshot create` |
|
||
|
||
```
|
||
agnes catalog --json # full structured view (use this in scripts)
|
||
agnes catalog # human-readable summary
|
||
agnes schema <table> # columns + types (BIGQUERY/DUCKDB dialect printed in header)
|
||
agnes describe <table> -n 5 # sample rows (works on local & materialized only)
|
||
```
|
||
|
||
### Decision tree — pick the right tool BEFORE writing SQL
|
||
|
||
```
|
||
┌─ local → agnes query "SELECT ..."
|
||
agnes catalog → ─────┤
|
||
query_mode of <table> ├─ materialized → agnes query (parquet was synced by agnes pull)
|
||
│ (if missing locally, run `agnes pull` first)
|
||
│
|
||
└─ remote → choose by table size + query shape:
|
||
- one cheap probe (COUNT, schema-confirm, single agg ≤200s)
|
||
→ agnes query --remote "..."
|
||
- repeated questions on same slice / large scan
|
||
→ agnes snapshot create <table> --select ... --where ... --as <name>
|
||
then agnes query "SELECT ... FROM <name>"
|
||
- join with a local table
|
||
→ agnes query --register-bq "alias=BQ_SQL" --sql "..."
|
||
```
|
||
|
||
### Three patterns for `query_mode: "remote"` tables
|
||
|
||
| Pattern | Tool | Use when |
|
||
|---------|------|----------|
|
||
| **`agnes snapshot create`** (preferred) | materializes a filtered subset locally → query the snapshot | repeated questions on same slice |
|
||
| **`agnes query --remote`** | one-shot, server-side execution against BigQuery (works for BASE TABLE rows directly + VIEW/MATERIALIZED_VIEW rows via the BQ jobs API; cost-guarded by a 5 GiB scan cap configurable in /admin/server-config) | single aggregate / cheap probe |
|
||
| **`agnes query --register-bq`** | hybrid joins between local snapshots and ad-hoc BQ subqueries | crossing local + remote |
|
||
|
||
### Common mistakes — avoid on first try
|
||
|
||
- **`--estimate` is on `agnes snapshot create` ONLY.** Do NOT pass it to `agnes query` — fails with `No such option: --estimate`. The estimate flow is a snapshot-creation cost gate, not a query primitive.
|
||
- **Old `agnes fetch` / `da fetch` / `da query` references in stale docs** — the CLI is `agnes`; `agnes fetch` was renamed to `agnes snapshot create`. If you see those names, translate before running.
|
||
- **Don't attempt personal GCP auth** if a BQ query fails with permission errors. BQ access uses the **server's service account**, not your Google identity — escalate to admin instead.
|
||
- **Don't `agnes query --remote "SELECT * FROM <large_table>"`** without a `--where`. Even if the scan-byte gate refuses, you've wasted the round-trip; gate yourself first by reading `rough_size_hint` and `where_examples` from `agnes catalog --json`.
|
||
|
||
### Failure-mode dictionary — what each error means + the right response
|
||
|
||
| Error wording (substring) | Cause | Response |
|
||
|---|---|---|
|
||
| `Binder Error: Query execution exceeded the timeout. Job ID: ...` | BQ-side query took >~200 s wall-clock; the DuckDB BQ extension's `bq_query_timeout_ms` (default 90 s, server may bump to 600 s) elapsed | Narrow `--where` (especially partition column), drop unused columns from `--select`, or switch to `agnes snapshot create` to materialise once + query locally |
|
||
| `HTTP 400: remote_scan_too_large` | Server's `bq_max_scan_bytes` cost gate refused the query (default 5 GiB) | Tighten `--where`; consider `agnes snapshot create` so the cost is paid once, then local queries are free |
|
||
| `HTTP 401: ... unauthorized` | PAT expired or wrong | `agnes init --server-url ... --token <new-PAT>`; re-mint via the dashboard's "Personal Access Tokens" page |
|
||
| `HTTP 403: cross_project_forbidden` (with `serviceusage` mention) | Server SA lacks `serviceusage.services.use` on the BQ data project | Escalate to admin to set `data_source.bigquery.billing_project`; do NOT try personal auth |
|
||
| `ReadTimeout` (client-side) on `agnes query --remote` | CLI is older than 0.35.1 (had 30 s default) | `agnes --version`; if <0.35.1, upgrade with `uv tool install --force <wheel-from-server>` (the URL is in the `[update]` banner that prints on every command). Then retry. |
|
||
| `unknown columns: [...]` from `agnes snapshot create` | `--select` lists columns that don't exist | Run `agnes schema <table>` and copy column names verbatim |
|
||
|
||
### Cost discipline — every BQ query bills bytes scanned
|
||
|
||
A naive `SELECT * FROM <large_table>` can cost real money. ALWAYS:
|
||
- filter via `--where` on the partition column (typically a date) — read `where_examples` in `agnes catalog --json`
|
||
- list specific columns in `--select` — column-store BQ skips the rest
|
||
- run `--estimate` first (only valid on `agnes snapshot create`) when the table is partitioned/clustered or when `rough_size_hint` is unknown
|
||
|
||
### `agnes snapshot create` discipline
|
||
|
||
```
|
||
# 1. ESTIMATE first — refuses to fetch without knowing the cost
|
||
agnes snapshot create <table> --select col1,col2 --where "date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)" --estimate
|
||
|
||
# 2. If reasonable, fetch as a named snapshot
|
||
agnes snapshot create <table> --select col1,col2 --where "..." --as my_recent
|
||
|
||
# 3. Query the local snapshot
|
||
agnes query "SELECT col1, COUNT(*) FROM my_recent GROUP BY 1"
|
||
|
||
# 4. List + drop snapshots when done
|
||
agnes snapshot list
|
||
agnes snapshot drop my_recent
|
||
```
|
||
|
||
Rules of thumb:
|
||
- ALWAYS list specific columns in `--select`. Avoid implicit SELECT *.
|
||
- ALWAYS include a `--where` for remote tables; otherwise add `--limit`.
|
||
- ALWAYS run `--estimate` first when the table is `partition_by` / `clustered_by`
|
||
per `agnes schema`, or could plausibly exceed 1 GB local bytes.
|
||
- Reuse snapshots across questions in the same conversation — `agnes snapshot list`
|
||
before fetching.
|
||
|
||
### Snapshot freshness — when to refresh
|
||
|
||
Snapshots are point-in-time copies. They go stale as the source data updates (most BQ tables refresh daily; check `sync_schedule` per `agnes catalog`). For each new conversation:
|
||
|
||
```
|
||
agnes snapshot list # see existing snapshots + their ages
|
||
agnes snapshot drop my_recent # drop stale ones
|
||
agnes snapshot create <table> --select ... --where ... --as my_recent # re-fetch
|
||
```
|
||
|
||
If the question is time-sensitive (e.g. "today's orders"), assume any snapshot older than the table's `sync_schedule` is stale and refresh.
|
||
|
||
### Hybrid query example — local + remote in one query
|
||
|
||
`agnes query --register-bq` lets a single SQL statement join a local table with an ad-hoc BQ subquery. The BQ subquery runs first (server-side), result registered as a DuckDB view, then the joined query runs locally.
|
||
|
||
```
|
||
agnes query \
|
||
--register-bq "traffic=SELECT date, country, SUM(views) AS views \
|
||
FROM \`prj.web_analytics.sessions\` \
|
||
WHERE date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY) \
|
||
GROUP BY 1, 2" \
|
||
--sql "SELECT o.date, o.country, o.revenue, t.views, o.revenue / NULLIF(t.views,0) AS rev_per_view \
|
||
FROM orders o \
|
||
JOIN traffic t ON o.date = t.date AND o.country = t.country \
|
||
ORDER BY 1 DESC"
|
||
```
|
||
|
||
The BQ subquery MUST contain `WHERE` and/or `GROUP BY` to keep the registered result manageable (target: under 500K rows, well under 100 MB). Multiple `--register-bq` flags can compose multiple BQ sources. For complex SQL, use `--stdin` mode (`echo '{"register_bq":{...},"sql":"..."}' | agnes query --stdin`).
|
||
|
||
### BigQuery SQL flavor for `--where`
|
||
|
||
Source-typed `bigquery` tables use BigQuery dialect, not DuckDB:
|
||
|
||
- Date literal: `DATE '2026-01-01'`
|
||
- Timestamp literal: `TIMESTAMP '2026-01-01 00:00:00 UTC'`
|
||
- Now: `CURRENT_DATE()`, `CURRENT_TIMESTAMP()`
|
||
- Date arithmetic: `DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)`
|
||
- Regex: `REGEXP_CONTAINS(col, r'pattern')` (raw string!)
|
||
- Cast: `CAST(x AS INT64)` (NOT `INT`)
|
||
|
||
### When the table you want isn't in `agnes catalog`
|
||
|
||
The table may exist in BigQuery but not be registered with Agnes yet. Two options:
|
||
|
||
1. **Ad-hoc one-shot** — register a BQ subquery as a view inline, no admin needed
|
||
if the agnes server SA has BQ access:
|
||
```
|
||
agnes query --register-bq "live=SELECT * FROM \`project.dataset.table\` WHERE date >= '...' LIMIT 1000" \
|
||
--sql "SELECT * FROM live"
|
||
```
|
||
2. **Ask admin to register** the table with `query_mode: "remote"` so it shows up
|
||
in `agnes catalog` and supports `agnes snapshot create` / `agnes query --remote`. This is the
|
||
right path for any table you'll query repeatedly.
|
||
|
||
### Deeper guidance
|
||
|
||
For the full protocol, including hybrid-query examples, snapshot hygiene, and
|
||
when NOT to use `agnes snapshot create`, run:
|
||
|
||
```
|
||
agnes skills show agnes-data-querying
|
||
```
|
||
|
||
## Corporate Memory
|
||
|
||
Rules injected by `agnes pull` from the server's corporate knowledge base live in `.claude/rules/km_*.md`. They are automatically loaded by Claude Code on every session start.
|
||
|
||
- `km_<id>.md` — mandatory rules (always enforced)
|
||
- `km_approved.md` — approved guidance (confidence × recency ranked)
|
||
|
||
Run `agnes pull` to refresh. Rules are pruned automatically when items are revoked.
|
||
|
||
## Directory Structure
|
||
- `server/parquet/*.parquet` — synced table data (RBAC-filtered subset for you)
|
||
- `user/duckdb/analytics.duckdb` — local analytics DuckDB views — what `agnes query` reads
|
||
- `user/snapshots/*.parquet` — ad-hoc materialized snapshots from `agnes snapshot create`
|
||
- `user/sessions/*.jsonl` — Claude Code session logs (uploaded on `agnes push`)
|
||
- `.claude/CLAUDE.local.md` — your personal notes + workspace customizations. **Never overwritten by `agnes init --force`.** Uploaded to the server on `agnes push`. Put any local-only Claude instructions, project-specific reminders, or temporary notes here — NOT in CLAUDE.md (this file is regenerated from a template).
|
||
|
||
_Hello {{ user.name or user.email }} — generated {{ today }}._
|