Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ src/keboola_agent_cli/
constants.py # Shared constants + dynamic APP_NAME resolution (retry params, timeouts, defaults)
json_utils.py # Deep-merge, set_nested_value, compute_diff utilities
models.py # Pydantic models shared across layers
effective_branch.py # resolve_branch(): the ONLY code that applies the `branch use` active branch;
# records project + branch for the `Target:` line / `targets` key (#766)
output.py # OutputFormatter: JSON vs Rich dual-mode output
errors.py # KeboolaApiError, ConfigError, ErrorCode enum, mask_token()
config_store.py # JSON persistence for config.json (0600 permissions)
Expand Down Expand Up @@ -293,6 +295,14 @@ Full author checklist: see `CONTRIBUTING.md` > "Releasing a beta (pre-release) v
`auth register-projects` disclose and every doc surface defers to. Run
`python scripts/check_sentinel_guards.py --list` to see the inventory.

19. **Only `effective_branch.resolve_branch()` applies the active branch.**
Commands and services get the branch ID from it and never read
`ProjectConfig.active_branch_id` themselves: the function records the
project and branch, and `OutputFormatter` reports them (a `Target:` line
on stderr, `targets` in the `--json` envelope, also for `--dry-run`).
`tests/test_effective_branch.py` fails on a new direct read. A read that
only shows or manages the active branch must be on its list, with a reason.

## Claude Code Plugin

The plugin lives here in `plugins/kbagent/` and is **published through `keboola/ai-kit`**. It exposes: a CLI (`kbagent`), three skills (`kbagent`, `kbagent-cicd-migration`, `kbagent-promotion-pipeline`), three slash commands (`/kbagent:setup`, `/keboola`, `/kbagent:review`), and two specialist subagents (`keboola-expert`, `kbagent-pr-reviewer`). All are namespaced under `kbagent:`. `/kbagent:setup` is the documented one-command first-run path (install CLI -> connect project -> `doctor`); it runs in the main context and spawns no subagent.
Expand Down Expand Up @@ -760,6 +770,8 @@ kbagent permissions check OPERATION
kbagent branch list [--project NAME]
kbagent branch create --project ALIAS --name "..." [--description "..."]
kbagent branch use --project ALIAS --branch ID
# branch use: every command that then picks a branch names it -- `Target:` on stderr, `targets` in
# --json (branch_source explicit|active_branch|git_mapping|manifest|merge_request|production). Version gate in gotchas.md (#766).
kbagent branch reset --project ALIAS
kbagent branch delete --project ALIAS --branch ID
kbagent branch merge --project ALIAS [--branch ID]
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ When adding a new command (e.g., `kbagent storage create-foo`), you must update
- [ ] **Service method** in `services/` -- business logic, validation, orchestration
- [ ] **Command function** in `commands/` -- Typer options, formatter, error handling
- [ ] **Permission registration** in `permissions.py` (`OPERATION_REGISTRY` dict)
- [ ] **Branch choice** through `resolve_branch()` in `effective_branch.py` when the command takes `--branch` or uses the active branch -- never read `ProjectConfig.active_branch_id` directly. The function reports the branch (`Target:` line, `targets` in `--json`); `tests/test_effective_branch.py` fails on a new direct read.
- [ ] **Service wiring** in `cli.py` if adding a new service class
- [ ] **HTTP API endpoint** in `src/keboola_agent_cli/server/routers/<group>.py` -- `kbagent serve` exposes the CLI as a REST API so external applications (Web UI, scheduled AI agents, Slack bots, Streamlit dashboards, CI pipelines) can call the platform without forking CLI subprocesses. The current convention is **1:1**: every command in a group has a matching endpoint in that group's router (e.g. `commands/flow.py` has 8 commands, `server/routers/flows.py` has 8 routes). If you add a new command, add the corresponding route. **Skip allowed** only for genuinely terminal-only commands (interactive prompts, Rich-rendered output that has no useful JSON shape, `doctor`/`init`/`update`-style infrastructure that manages kbagent itself rather than Keboola). Document any skip in the PR description with a one-line reason so reviewers don't flag it.

Expand Down
2 changes: 1 addition & 1 deletion docs/merge-requests-layer1.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ decisions, derive facts.
Every implicit resolution is reported, on stderr in human mode and in the payload always:

```
Info: Using active branch (ID: 123) for project 'acme'
Source: project 'acme', branch 123 (from 'kbagent branch use')
Info: Resolved merge request #7 from branch 123
```

Expand Down
9 changes: 9 additions & 0 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,15 @@ its absence is NOT a promise the entry is version-independent (see §1 Rule 6).
or run `project use`. On <= 0.90.1 the same commands silently used the FIRST
registered project and ignored the pin (issue #684). gotchas.md.

**Which branch did a command use? (vNEXT+)**
- Every command that picks a branch names it: `Target: project 'P', branch ID
(from 'kbagent branch use')` on stderr, `targets` in `--json`
(`branch_source` `active_branch` = chosen by `branch use`). Read it before
you report a write as done on production. No `targets` key = no branch was
chosen, NOT production. Below vNEXT, `workspace create` and most config /
flow writes applied the active branch silently: check `branch list` (Active
column) first. gotchas.md (#766).

**Recurring `ext.keboola.cli.` events in a project's own event log (0.93.0+)**
- kbagent posts one best-effort usage event per command to the acting project's
Storage events. An event audit then shows one `ext.keboola.cli.` (CLI/REPL) or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ kbagent --json branch merge --project ALIAS
- **Async operations**: `branch create` and `branch delete` are async on the API. kbagent waits for completion (typically 1-3s). No need to poll.
- **Merge from the CLI needs the merge-request group** *(since 0.94.0)*: `branch merge` only returns a URL for the Keboola UI (and is deprecated). On a project with the `branches-merge-requests` feature, `kbagent merge-request create` + `merge-request merge` merge via the API with review and conflict resolution -- see [merge-request-workflow.md](merge-request-workflow.md).
- **Active branch persistence**: stored in kbagent config. Survives between sessions.
- **See which branch a command used** (since vNEXT, #766): every command that picks a branch prints `Target: project 'P', branch ID 'NAME' (from 'kbagent branch use')` on stderr, and `--json` output carries `targets` (`branch_source`: `explicit`, `active_branch`, `git_mapping`, `manifest`, `merge_request` or `production`). `--dry-run` reports the same target as the real run. Check it before a write.
- **Config commands respect active branch**: `config list`, `config detail`, and `config search` auto-scope to the active branch. Use `--branch ID` to override.
- **Workspaces respect active branch**: `workspace create` and `workspace delete` operate in the active branch context.
- **Workspaces respect active branch**: `workspace create`, `workspace list`, `workspace detail` and `workspace delete` operate in the active branch context.
- **Sync respects active branch**: `sync pull` writes dev branch configs into a separate directory (e.g. `fix-etl/` instead of `main/`). `sync diff` and `sync push` also auto-scope to the active branch. See [sync-workflow.md](sync-workflow.md) for details.
- **`branch reset` alone does not re-target the sync manifest** *(since v0.89.0)*: a dev-branch `sync pull` re-points every `manifest.configurations` entry at that branch, so after resetting to production the `main/` tree is an orphan the manifest no longer tracks. A production `sync diff` / `sync push` reports those configs under `orphaned` and excludes them (they are never pushed as new configs — issue #649); run `kbagent sync pull --project ALIAS` to re-target the manifest to production first. Configs that exist only on the dev branch are promoted with `branch merge`, never by pushing them to production.
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ Non-SOX Branches 2.0: merge a dev branch into production with review. Alias `mr`

## Workspaces (SQL Debugging)
- `workspace create --project ALIAS [--name NAME] [--ui] [--read-only]` -- create workspace (headless ~1s, `--ui` ~15s). Since v0.47.1: Snowflake headless workspaces return a `private_key` PEM field; `password` is empty. BigQuery workspaces keep the default password credential shape.
- `workspace list [--project NAME ...] [--orphaned] [--branch ID] [--qs-compatible]` -- list workspaces. `--project` repeatable; `--orphaned` filters to workspaces whose backing `keboola.sandboxes` config is missing. **Since v0.42.0 (#304)**: each entry carries `login_type`, `read_only`, `qs_compatible`, `database`, `warehouse`. New `Login Type` / `RO` / `QS` columns in human mode. `--qs-compatible` pre-filters to RO + whitelisted-loginType workspaces (the canonical data-app shape). **Updated v0.58.0**: `qs_compatible` is keyed by `(backend, loginType)` -- BigQuery workspaces (loginType `default`) now report `qs_compatible: true` and pass `--qs-compatible`; pre-0.58.0 every BigQuery workspace was wrongly excluded (Snowflake's own legacy `default` stays `false`). `--branch` requires exactly one `--project`; without `--branch`, the command behaves like `storage buckets` and uses production with an `Info: Using production branch for read (active dev branch X ignored; pass --branch X to override)` banner when an alias is pinned to a dev branch
- `workspace list [--project NAME ...] [--orphaned] [--branch ID] [--qs-compatible]` -- list workspaces. `--project` repeatable; `--orphaned` filters to workspaces whose backing `keboola.sandboxes` config is missing. **Since v0.42.0 (#304)**: each entry carries `login_type`, `read_only`, `qs_compatible`, `database`, `warehouse`. New `Login Type` / `RO` / `QS` columns in human mode. `--qs-compatible` pre-filters to RO + whitelisted-loginType workspaces (the canonical data-app shape). **Updated v0.58.0**: `qs_compatible` is keyed by `(backend, loginType)` -- BigQuery workspaces (loginType `default`) now report `qs_compatible: true` and pass `--qs-compatible`; pre-0.58.0 every BigQuery workspace was wrongly excluded (Snowflake's own legacy `default` stays `false`). `--branch` requires exactly one `--project`; without `--branch`, it uses each alias's active branch (`branch use`), else production. **Since vNEXT (#766)** the `Target:` line / `targets` key names that branch; the earlier `Info: Using production branch for read` line was wrong
- `workspace detail --project ALIAS --workspace-id ID [--branch ID]` -- show connection details. **Since v0.42.0 (#304)**: response carries `login_type`, `read_only`, `qs_compatible`; human mode adds `Login type:` / `Read-only:` / `Query Service compatible:` rows. **Updated v0.58.0**: BigQuery `default` workspaces now report `qs_compatible: true` (was `false`). `--branch` opt-in mirrors `workspace list`
- `workspace delete --project ALIAS --workspace-id ID` -- delete workspace
- `workspace password --project ALIAS --workspace-id ID` -- reset and return new password
Expand Down
88 changes: 77 additions & 11 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,73 @@ Versioning convention:
behavior; the inline `(updated vX.Y.Z)` records when the refinement landed.
-->

## Every command that picks a branch names it: `Target:` line and `targets` key

*(since vNEXT, #766)*

- **`kbagent branch use` sets an active branch per project, and commands
apply it when `--branch` is omitted.** Before, some commands printed an
`Info:` line in human mode only, and others applied the active branch with
no notice (for example `workspace create`). A write could land on a
development branch and the caller did not know.
- **Human mode prints one line on stderr** before the first API call to the
project and before any confirmation prompt:
- `Target: project 'prod', branch 456 'feature-x' (from 'kbagent branch use')`
- `Target: project 'prod', branch 789 (from the command line)`
- `Target: project 'prod', production (active branch 456 'feature-x' not
used; pass --branch 456 to use it)` -- storage reads, Data Streams,
data apps, branch metadata and git-branching `sync` do not use the
active branch.
- `Target: project 'prod', branch 388 (from .keboola/branch-mapping.json)`
or `(from .keboola/manifest.json)` -- `sync` without an active branch.
- `Source:` names the branch that a command reads from or merges from:
the source project of `config clone`, the branch of a merge request
(`merge-request *`, `branch merge`), the branch notification commands
read config names from, and the source table branch of `storage
create-table --source-branch-id` / `storage clone-table` (production).
`merge-request merge` and an armed `merge-request auto-merge` also print
`Target: project 'prod', production`.
- A command that uses two branches prints a line for each:
- `workspace create --ui` creates the config in the active branch but
runs its job on production.
- `workspace from-transformation` reads the transformation from
production.
- `sync clone` creates the buckets in production.
- **`--json` adds `targets` to the success AND the error envelope**, before
`data` / `error`, which do not change. Each entry is
`{role, project_alias, branch_id, branch_name, branch_source, active_branch}`:
- `branch_source`: `explicit` (`--branch` or `--target-branch`),
`active_branch` (`branch use`), `git_mapping`
(`.keboola/branch-mapping.json`), `manifest` (the first branch of
`.keboola/manifest.json`), `merge_request` (the branch of the merge
request given by `--merge-request-id`) or `production`.
- `production` can carry a numeric `branch_id` when the command read the
default branch ID from the API (workspaces, config metadata).
- `active_branch` is the project's `branch use` choice
(`{branch_id, branch_name}` or null), also when the command did not use it.
- `role` is `source` for the `Source:` cases above, else `target`.
- **No `targets` key means the command chose no branch**: it has no
`--branch` and never uses the active branch (`project list`, `token list`,
storage listings without `--project`), it failed on an unknown alias, or it
refused to run because it needs a branch and got none. It does not mean
production.
- **`--branch 0` means production.** The API clients always sent 0 to the
production endpoint. Before vNEXT the config, flow, schedule and
notification commands used the active branch for `--branch 0` instead.
- **`--dry-run` reports the same target as the real run.** `flow delete
--dry-run` and `flow schedule-remove --dry-run` now put the resolved branch
in `would_delete.branch_id` (before: the raw `--branch` value, so null under
an active branch).
- **`workspace list` / `workspace detail` use the active branch.** The
`Info: Using production branch for read` line they printed was wrong: the
workspace service has used the active branch since v0.42.0. They now print
`Target:` with the branch they use.
- **`branch use` and `branch create` save the branch name** next to the ID
(`active_branch_name` in `config.json`). It shows in `Target:` and in
`branch_name`. A branch renamed later keeps the saved name until the next
`branch use`. An active branch set by an older version has no name.
- `kbagent serve` responses do not carry `targets` (REST reporting: #791).

## A semantic-layer dataset `fqn` is the table's real warehouse location, not `"KEBOOLA"`

*(since 0.95.0, #761)*
Expand Down Expand Up @@ -1130,17 +1197,16 @@ kbagent --json workspace list --project prod --qs-compatible
# returns only workspaces with login_type ∈ whitelist AND read_only=true
```

**Branch behaviour (read-command parity with `storage buckets`):**

`workspace list` / `workspace detail` now follow the same pattern as
`storage buckets` / `storage tables` / `config list`: when an alias is
pinned to a dev branch via `branch use`, the production endpoint is used
with an `Info: Using production branch for read (active dev branch X
ignored; pass --branch X to override)` banner. Before v0.42.0 these
commands silently scoped to the pinned branch, returning a different
workspace set than the same alias one shell ago. Pass `--branch ID` to
opt back into the dev-branch endpoint. `--branch` requires exactly one
`--project`.
**Branch behaviour:**

`workspace list` / `workspace detail` use the alias's active branch
(`branch use`) when `--branch` is omitted, like `config list`. Up to vNEXT
they printed `Info: Using production branch for read (active dev branch X
ignored; pass --branch X to override)`, but the workspace service used the
active branch: the line was wrong, the listing was not. Since vNEXT they
print `Target:` with the branch they use (see the #766 entry at the top).
`storage buckets` / `storage tables` are the reads that use production
under an active branch. `--branch` requires exactly one `--project`.

## `config detail --component-id keboola.sandboxes` now annotates the misleading `parameters.id` (since v0.42.0, closes #304)

Expand Down
6 changes: 6 additions & 0 deletions src/keboola_agent_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from .commands.workspace import workspace_app
from .config_store import ConfigStore, resolve_config_dir
from .constants import EXIT_PERMISSION_DENIED
from .effective_branch import record_targets
from .errors import ErrorCode, PermissionDeniedError
from .output import OutputFormatter, force_utf8_when_redirected

Expand Down Expand Up @@ -300,6 +301,11 @@ def main(
no_color=effective_no_color,
verbose=verbose,
)
# Record the project and branch of this command for the output (#766). Not
# for the REPL shell (each line records on its own) nor for `serve`, whose
# request threads would all add to one record for the server's lifetime.
if ctx.invoked_subcommand not in (None, "repl", "serve"):
ctx.with_resource(record_targets(formatter.report_target))

resolved_dir, source = resolve_config_dir(cli_config_dir=config_dir)
config_store = ConfigStore(config_dir=resolved_dir, source=source)
Expand Down
5 changes: 5 additions & 0 deletions src/keboola_agent_cli/commands/_data_app_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import typer

from ..effective_branch import resolve_branch
from ..errors import ConfigError, ErrorCode, KeboolaApiError
from ._helpers import get_formatter, get_service, map_error_to_exit_code

Expand Down Expand Up @@ -139,6 +140,7 @@ def data_app_secrets_set(
"""

formatter = get_formatter(ctx)
branch = resolve_branch(ctx.obj["config_store"], project, branch, ignore_active_branch=True)
service = get_service(ctx, "data_app_service")

if secret and secrets_file:
Expand Down Expand Up @@ -256,6 +258,7 @@ def data_app_secrets_list(
"""

formatter = get_formatter(ctx)
branch = resolve_branch(ctx.obj["config_store"], project, branch, ignore_active_branch=True)
service = get_service(ctx, "data_app_service")
try:
result = service.list_data_app_secrets(
Expand Down Expand Up @@ -321,6 +324,7 @@ def data_app_secrets_get(
"""

formatter = get_formatter(ctx)
branch = resolve_branch(ctx.obj["config_store"], project, branch, ignore_active_branch=True)
service = get_service(ctx, "data_app_service")
try:
result = service.get_data_app_secret(
Expand Down Expand Up @@ -396,6 +400,7 @@ def data_app_secrets_remove(
"""

formatter = get_formatter(ctx)
branch = resolve_branch(ctx.obj["config_store"], project, branch, ignore_active_branch=True)
service = get_service(ctx, "data_app_service")

if (
Expand Down
4 changes: 2 additions & 2 deletions src/keboola_agent_cli/commands/_flow_triggers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
from rich.markup import escape
from rich.table import Table

from ..effective_branch import resolve_branch
from ..errors import ConfigError, ErrorCode, KeboolaApiError
from ._helpers import (
get_formatter,
get_service,
map_error_to_exit_code,
resolve_branch,
)

NOT_COVERED_NOTE = (
Expand Down Expand Up @@ -119,7 +119,7 @@ def flow_triggers(
formatter = get_formatter(ctx)
service = get_service(ctx, "flow_service")
config_store = ctx.obj["config_store"]
_, effective_branch = resolve_branch(config_store, formatter, project, branch)
effective_branch = resolve_branch(config_store, project, branch)

try:
result = service.get_flow_triggers(
Expand Down
Loading
Loading