Skip to content

Triage CLI - #717

Open
jpodivin wants to merge 2 commits into
packit:mainfrom
jpodivin:CLI
Open

Triage CLI#717
jpodivin wants to merge 2 commits into
packit:mainfrom
jpodivin:CLI

Conversation

@jpodivin

@jpodivin jpodivin commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Triage can now be executed using CLI command ymir. The command verifies credentials, spins up containers, and starts the workflow. Once it's done, the containers are stopped. The sole exception is the trace-server, so that developers can access the collected trace information and see how the agent behaved. The dotenv package was introduced to handle env vars, as most of the credentials are managed this way, and they have to be verified and synced across containers and host.

When the workflow is executed, the given JIRA is marked with ymir_manual_trigger label. This blocks the the JIRA from being picked up by the fetcher. The intent here is to clearly mark this workflow as somewhat irregular. The CLI is not intended to be a primary way for anyone to use Ymir. This way we can also see how often people invoke Ymir manually, by running as simple query in Jira.

This PR is also a base for possible expansion of CLI to other parts of the workflow.

Trace server

I didn't want to run more containers than necessary, so to skip otel-collector, I've implemented protobuf parsing in the trace-server. This does require two new dependencies, opentelemetry-proto and protobuf. The image size is comfortable 265 MB.

But I understand that this is a matter of opinion.

Changes to ymir/common/pyproject.toml

This code was developed in parallel to ymir/common/pyproject.toml so I had put the merge_queue.py on the explicit list to get the package working. Depending on the merge order, this part can be dropped.

RHEL_CONFIG_PATH env var

To make sure that all workflows load the config pointed to by --secrets the RHEL_CONFIG_PATH is set by the CLI, and later used by the load_rhel_config function.

RELEASE NOTES BEGIN

Triage workflow can now be triggered using CLI.

RELEASE NOTES END

@jpodivin
jpodivin requested review from TomasKorbar and nforro and removed request for nforro July 27, 2026 06:58
@jpodivin
jpodivin marked this pull request as ready for review July 27, 2026 11:37
@qodo-for-packit

Copy link
Copy Markdown

PR Summary by Qodo

Add ymir CLI to run triage locally via compose-managed containers

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Introduce ymir triage CLI to run triage in containers with local infra startup/teardown.
• Mark CLI runs with ymir_manual_trigger Jira label to prevent parallel fetcher processing.
• Improve trace capture by letting trace-server ingest OTLP protobuf directly (no collector).
Diagram

graph TD
  A[/"ymir CLI"/] --> B[["compose.yaml (cli profile)"]] --> C("triage-cli container") --> H[("Work dir (GIT_REPOS_HOST)")]
  B --> D("mcp-gateway-cli") --> E{{"Jira API"}}
  B --> F[("valkey")]
  B --> G("trace-server")
  C --> F --> H
  C --> G

  subgraph Legend
    direction LR
    _cli[/"CLI"/] ~~~ _cfg[["Compose config"]] ~~~ _svc("Container") ~~~ _db[("Store")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep OTEL collector and remove protobuf parsing from trace-server
  • ➕ Uses standard OTLP pipeline with fewer custom ingestion concerns
  • ➕ Avoids adding protobuf/opentelemetry-proto dependencies to trace-server image
  • ➖ Runs an extra container for local CLI usage
  • ➖ More moving parts for developers and higher orchestration overhead
2. Use a Python compose/containers library instead of subprocess calls
  • ➕ Stronger typing and error handling than shelling out
  • ➕ Potentially easier to test lifecycle logic without heavy mocking
  • ➖ Extra dependency footprint and varying support across podman/docker
  • ➖ Harder to mirror existing Makefile compose detection behavior exactly
3. Move direct-mode Jira label lifecycle into a dedicated helper/service
  • ➕ Centralizes label semantics (manual trigger, stale label cleanup) for reuse
  • ➕ Could be shared by future CLI commands beyond triage
  • ➖ More refactor/churn now for a feature that currently targets triage only
  • ➖ Risk of changing existing queue-mode behavior if not carefully isolated

Recommendation: The PR’s approach is reasonable for a developer-focused CLI: using compose subprocess calls aligns with existing tooling, and leaving trace-server running improves debugging ergonomics. The main tradeoff is custom protobuf ingestion in trace-server; if maintenance burden grows, reverting to an OTEL collector container would be the cleanest fallback.

Files changed (23) +1744 / -35 · 3 not counted

Enhancement (5) +402 / -15
server.pyAccept OTLP protobuf payloads in trace-server +48/-7

Accept OTLP protobuf payloads in trace-server

• Extends '/v1/traces' to handle 'application/x-protobuf' by decoding 'ExportTraceServiceRequest' and converting IDs from base64 to hex for UI routing compatibility. Adds strict content-type handling with 415 responses for unsupported types.

trace_server/server.py

triage_agent.pyAdd USER_TRIGGERED direct-mode label lifecycle + result export +76/-8

Add USER_TRIGGERED direct-mode label lifecycle + result export

• Introduces 'USER_TRIGGERED' direct mode behavior: add 'ymir_manual_trigger' at start (non-dry-run), remove it on crash, and on success add the resolution label while stripping stale 'ymir_*' labels (except manual trigger). Also writes 'triage_result.json' under 'GIT_REPO_BASEPATH/<issue>/' for CLI consumption.

ymir/agents/triage_agent.py

compose.pyImplement compose orchestration helpers +95/-0

Implement compose orchestration helpers

• Adds compose command detection (podman/docker + standalone variants) and helper functions to start infra services, stop selected services (leaving trace-server running), and run the triage agent with forwarded env vars.

ymir/cli/compose.py

main.pyAdd Typer CLI for running triage via containers +180/-0

Add Typer CLI for running triage via containers

• Implements 'ymir triage' with secrets/env loading, credential validation, work directory management, compose lifecycle, and result printing. Ensures 'RHEL_CONFIG_PATH' and other env vars are set for the run and restored afterwards.

ymir/cli/main.py

constants.pyAdd JiraLabels.MANUAL_TRIGGER +3/-0

Add JiraLabels.MANUAL_TRIGGER

• Introduces 'ymir_manual_trigger' as a first-class Jira label constant for CLI-triggered runs.

ymir/common/constants.py

Bug fix (1) +1 / -1
utils.pyTreat httpx.ReadError as connection error +1/-1

Treat httpx.ReadError as connection error

• Expands '_is_connection_error()' to include 'httpx.ReadError', improving retry/backoff classification for transient read failures.

ymir/common/utils.py

Refactor (1)
__init__.pyCreate CLI package namespace not counted

Create CLI package namespace

• Adds the 'ymir.cli' package marker for the new CLI implementation.

ymir/cli/init.py

Tests (5) +1170 / -0
__init__.pyAdd CLI test package not counted

Add CLI test package

• Introduces the CLI test package root to organize unit tests under 'ymir/cli/tests/'.

ymir/cli/tests/init.py

__init__.pyAdd CLI unit test module not counted

Add CLI unit test module

• Adds unit test module initialization for CLI tests.

ymir/cli/tests/unit/init.py

test_compose.pyUnit test compose detection and lifecycle commands +207/-0

Unit test compose detection and lifecycle commands

• Covers compose command detection order and verifies generated 'compose up/stop/run' invocations include the 'cli' profile, correct cwd, and expected service selections.

ymir/cli/tests/unit/test_compose.py

test_main.pyComprehensively test CLI env handling and failure modes +773/-0

Comprehensively test CLI env handling and failure modes

• Adds extensive tests for credential validation, secrets/rhel-config presence checks, env var forwarding to containers, lifecycle robustness on failures, work-dir behavior, result-file echoing, and environment restoration.

ymir/cli/tests/unit/test_main.py

test_observability.pyTest exporter/ingestion payload compatibility for traces +190/-0

Test exporter/ingestion payload compatibility for traces

• Validates that OpenTelemetry protobuf-to-dict conversion produces the camelCase structure expected by trace-server '_extract_spans'. Provides regression coverage for serialization format mismatches.

ymir/cli/tests/unit/test_observability.py

Documentation (2) +97 / -2
README-agents.mdDocument CLI workflow, secrets, and trace viewer +96/-2

Document CLI workflow, secrets, and trace viewer

• Documents 'ymir' CLI setup/usage, the 'cli' compose profile services, and how mock Jira runs work. Also adds 'RHEL_CONFIG_PATH' guidance and points to the trace viewer URL.

README-agents.md

jira_label_workflow_routing.mdDocument 'ymir_manual_trigger' label semantics +1/-0

Document 'ymir_manual_trigger' label semantics

• Adds the CLI-triggered label as a dedup anchor to prevent fetcher parallelism and clarifies that it persists after completion.

jira_label_workflow_routing.md

Other (9) +74 / -17
Containerfile.c9s-testsAdd CLI deps to test container image +3/-1

Add CLI deps to test container image

• Installs 'typer' and 'python-dotenv' in the c9s test image so the new CLI and its tests can run in CI containers.

Containerfile.c9s-tests

Containerfile.trace-serverInstall protobuf tooling for OTLP ingestion +2/-1

Install protobuf tooling for OTLP ingestion

• Adds 'python3-pip' and installs 'opentelemetry-proto' + 'protobuf' so the trace-server can parse OTLP protobuf payloads directly.

Containerfile.trace-server

MakefileAdd check-cli container test target +3/-1

Add check-cli container test target

• Introduces 'check-cli-in-container' as a first-class Makefile target and wires it into the phony list.

Makefile

Makefile.testsRun CLI tests in host and container test suites +10/-4

Run CLI tests in host and container test suites

• Adds 'check-cli' and 'check-cli-in-container' targets and includes them in the overall 'check' and 'check-in-container' pipelines.

Makefile.tests

compose.yamlAdd 'cli' compose profile and one-shot triage container +40/-6

Add 'cli' compose profile and one-shot triage container

• Adds 'mcp-gateway-cli' (keep-id + localhost port), exposes valkey to localhost for CLI usage, and includes trace-server in the 'cli' profile. Introduces 'triage-cli' service for 'compose run --rm' execution and parameterizes git repo mount paths.

compose.yaml

pyproject.tomlRegister 'ymir' console script entrypoint +3/-0

Register 'ymir' console script entrypoint

• Adds '[project.scripts] ymir = ymir.cli.main:app' so the CLI can be invoked as 'ymir' after installation.

pyproject.toml

requirements.txtAdd python-dotenv runtime dependency +1/-0

Add python-dotenv runtime dependency

• Adds 'python-dotenv' to support loading credentials from '.secrets/*.env' for CLI runs.

requirements.txt

config.pyLoad RHEL config from RHEL_CONFIG_PATH +11/-4

Load RHEL config from RHEL_CONFIG_PATH

• Updates 'load_rhel_config()' to read the config path from 'RHEL_CONFIG_PATH' (defaulting to 'rhel-config.json') and clarifies error behavior in the docstring.

ymir/common/config.py

pyproject.tomlInclude merge_queue module in ymir-common build mapping +1/-0

Include merge_queue module in ymir-common build mapping

• Adds 'merge_queue.py' to the explicit file mapping to keep the common package build working across merge ordering.

ymir/common/pyproject.toml

@qodo-for-packit

qodo-for-packit Bot commented Jul 27, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 7 rules

Grey Divider


Action required

1. Docker CLI runtime mismatch ✗ Dismissed 🐞 Bug ≡ Correctness
Description
detect_compose_cmd() can select Docker Compose, but the CLI compose profile requires `userns_mode:
keep-id for mcp-gateway-cli and triage-cli`, which will cause container creation/startup to fail
before triage can run. This makes the CLI unusable on Docker-only setups despite advertising Docker
fallback behavior.
Code

ymir/cli/compose.py[R20-46]

+def detect_compose_cmd() -> list[str]:
+    """Detect and return available compose command.
+
+    Mirrors the detection logic in the Makefile:
+    podman compose > podman-compose > docker compose > docker-compose
+    """
+    for runtime in ("podman", "docker"):
+        runtime_path = shutil.which(runtime)
+        if not runtime_path:
+            continue
+        try:
+            subprocess.run(  # noqa: S603
+                [runtime_path, "compose", "version"],
+                capture_output=True,
+                check=True,
+            )
+            return [runtime_path, "compose"]
+        except (subprocess.CalledProcessError, FileNotFoundError):
+            pass
+
+        if standalone := shutil.which(f"{runtime}-compose"):
+            return [standalone]
+
+    raise RuntimeError(
+        "No compose tool found. Install one of: "
+        "podman compose, podman-compose, docker compose, docker-compose"
+    )
Relevance

●● Moderate

Docker-compatibility fixes have been rejected before; unclear if team will change runtime
detection/constraints for keep-id.

PR-#516

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI runtime detection explicitly falls back to Docker, while the CLI compose profile configures
userns_mode: keep-id on the services the CLI must run, so selecting Docker will break the core
execution path.

ymir/cli/compose.py[20-46]
compose.yaml[128-141]
compose.yaml[233-248]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ymir` CLI's compose detection can return a Docker-based compose command, but the `cli` profile in `compose.yaml` requires Podman-specific `userns_mode: keep-id`. When Docker is selected, `compose up/run` will fail.

### Issue Context
The code explicitly tries Podman first, then Docker. The compose file explicitly configures `keep-id` to map the host UID.

### Fix Focus Areas
- ymir/cli/compose.py[20-46]
- compose.yaml[128-137]
- compose.yaml[233-248]

### Recommended fix
- In `detect_compose_cmd()`, if the discovered runtime is Docker (either `docker compose` or `docker-compose`), fail fast with a clear error like: "ymir CLI requires Podman Compose because compose.yaml uses userns_mode: keep-id".
- Alternatively, introduce a Docker-compatible compose path (e.g., a separate profile or a CLI flag) that omits `userns_mode: keep-id` and updates the volume strategy accordingly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. valkey uses docker.io image ✗ Dismissed 📘 Rule violation § Compliance
Description
The PR adds the cli profile to the valkey service, which uses docker.io/valkey/valkey:8
instead of the required quay.io/jotnar/ namespace. This violates the container image namespace
requirement for changed container build specs/manifests.
Code

compose.yaml[R144-150]

+    ports:
+      # Exposed for ymir CLI — localhost only, does not affect inter-container traffic
+      - "127.0.0.1:6379:6379"
    volumes:
      - valkey-data:/data
    restart: unless-stopped
-    profiles: ["agents", "supervisor", "e2e-test"]
+    profiles: ["agents", "supervisor", "e2e-test", "cli"]
Relevance

●●● Strong

Compliance namespace rules are typically enforced; change expands valkey usage into new cli profile.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1591 requires all image references in changed container specs to start with
quay.io/jotnar/ unless an explicit exception comment is present. In compose.yaml, the valkey
service uses docker.io/valkey/valkey:8, and this PR changes profiles to include cli, making
this non-compliant image part of the new CLI profile.

Rule 1591: Enforce quay.io/jotnar namespace for container images
compose.yaml[142-150]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`compose.yaml` enables the `valkey` service for the `cli` profile, but the service image is `docker.io/valkey/valkey:8`, which violates the requirement that image references start with `quay.io/jotnar/` (or have an explicitly documented exception).

## Issue Context
The `cli` workflow introduced by this PR starts `valkey` automatically, so this non-compliant image reference becomes part of the new CLI execution path.

## Fix Focus Areas
- compose.yaml[142-150]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Jira key path traversal 🐞 Bug ⛨ Security
Description
Direct-mode triage writes triage_result.json to Path(GIT_REPO_BASEPATH) / jira_issue and the CLI
reads from Path(GIT_REPOS_HOST) / issue, but neither validates that the Jira key is not absolute
or contains .., allowing reads/writes outside the intended work directory. This can corrupt or
disclose local files when the issue string comes from untrusted automation or accidental misuse.
Code

ymir/agents/triage_agent.py[R926-928]

+            result_dir = Path(os.environ.get("GIT_REPO_BASEPATH", "/git-repos")) / jira_issue
+            result_dir.mkdir(parents=True, exist_ok=True)
+            (result_dir / "triage_result.json").write_text(output.model_dump_json(indent=2), encoding="utf-8")
Relevance

●●● Strong

Strong precedent: team accepts Jira key/path validation to prevent traversal when using filesystem
paths.

PR-#571
PR-#670

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new direct-mode code uses the environment-provided Jira issue key directly in a
Path(...)/jira_issue join and writes a file, and the CLI similarly constructs a host path from the
user-provided issue; meanwhile, a nearby code path demonstrates the expected validation pattern for
Jira keys used in filesystem paths.

ymir/agents/triage_agent.py[865-929]
ymir/cli/main.py[120-159]
ymir/agents/tasks.py[111-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`jira_issue` / `issue` is used as a filesystem path segment in both the CLI and direct-mode agent. Without validation, values like `../../tmp/x` or `/etc` can escape the intended base directory.

### Issue Context
Other code paths already validate Jira keys before using them in filesystem operations (rejecting absolute paths and `..`). The new direct-mode result persistence and CLI result reading should follow the same pattern.

### Fix Focus Areas
- ymir/agents/triage_agent.py[865-929]
- ymir/cli/main.py[120-159]
- ymir/agents/tasks.py[111-116]

### Recommended fix
- Add a shared validation helper (or inline checks) that rejects:
 - empty strings
 - absolute paths (`Path(x).is_absolute()`)
 - any segment containing `..`
 - (optionally) enforce a Jira-key regex like `^[A-Z][A-Z0-9]+-\d+$`
- Apply it:
 - in `ymir.cli.main.triage()` before constructing `issue_upper` paths
 - in `ymir.agents.triage_agent.main()` direct-mode branch before creating `result_dir`
- Use the validated/normalized Jira key (e.g., uppercased) consistently for directory naming.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread compose.yaml Outdated
Comment thread ymir/cli/compose.py
Comment on lines +926 to +928
result_dir = Path(os.environ.get("GIT_REPO_BASEPATH", "/git-repos")) / jira_issue
result_dir.mkdir(parents=True, exist_ok=True)
(result_dir / "triage_result.json").write_text(output.model_dump_json(indent=2), encoding="utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Jira key path traversal 🐞 Bug ⛨ Security

Direct-mode triage writes triage_result.json to Path(GIT_REPO_BASEPATH) / jira_issue and the CLI
reads from Path(GIT_REPOS_HOST) / issue, but neither validates that the Jira key is not absolute
or contains .., allowing reads/writes outside the intended work directory. This can corrupt or
disclose local files when the issue string comes from untrusted automation or accidental misuse.
Agent Prompt
### Issue description
`jira_issue` / `issue` is used as a filesystem path segment in both the CLI and direct-mode agent. Without validation, values like `../../tmp/x` or `/etc` can escape the intended base directory.

### Issue Context
Other code paths already validate Jira keys before using them in filesystem operations (rejecting absolute paths and `..`). The new direct-mode result persistence and CLI result reading should follow the same pattern.

### Fix Focus Areas
- ymir/agents/triage_agent.py[865-929]
- ymir/cli/main.py[120-159]
- ymir/agents/tasks.py[111-116]

### Recommended fix
- Add a shared validation helper (or inline checks) that rejects:
  - empty strings
  - absolute paths (`Path(x).is_absolute()`)
  - any segment containing `..`
  - (optionally) enforce a Jira-key regex like `^[A-Z][A-Z0-9]+-\d+$`
- Apply it:
  - in `ymir.cli.main.triage()` before constructing `issue_upper` paths
  - in `ymir.agents.triage_agent.main()` direct-mode branch before creating `result_dir`
- Use the validated/normalized Jira key (e.g., uppercased) consistently for directory naming.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically true, but to exploit this, you would need an access to the host, and have permissions to access the file. And why would you then try to exfiltrate it in this convoluted way.

Comment thread jira_label_workflow_routing.md Outdated
| `ymir_todo` | Maintainer-facing trigger for an e2e run | Fetcher swaps it for `ymir_triage_in_progress` on enqueue; only honored when the changelog shows the label was added by a member of the `Red Hat Employee` Jira group (verified per-issue, not via JQL). The triage run posts an ack comment and a result comment so the requester gets feedback. Default is silent — without `ymir_todo`, no comments are posted. |
| `ymir_consolidate_base` | Mark a backport MR for consolidation (base) | Paired with `ymir_consolidate_next` on another issue for the same package/branch. The fetcher matches the pair, submits a targeted consolidation job, removes both labels, and posts comments. |
| `ymir_consolidate_next` | Mark a backport MR for consolidation (next) | Must be on a different issue than `ymir_consolidate_base`, for the same package/branch. |
| `ymir_manual_trigger` | Dedup anchor for CLI-triggered runs | Set by the CLI at the start of a user-triggered triage run, before the workflow executes. Prevents the fetcher from picking up the same issue in parallel. Persists after completion alongside the terminal resolution label. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this that we'd be able to track how many people will use the CLI

Comment thread ymir/cli/main.py Outdated
_ENV_KEYS = ("RHEL_CONFIG_PATH", "MOCK_JIRA", "JIRA_MOCK_FILES_HOST", "GIT_REPOS_HOST")


def check_credentials() -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function brings a ton of value and should help the adoption a lot

nforro
nforro previously approved these changes Jul 28, 2026

@nforro nforro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but Claude has some remarks (nothing major):

1. Compose changes to mcp-gateway affect all profiles, not just CLI

The diff parameterizes GIT_REPO_BASEPATH, GIT_CONFIG_GLOBAL, and the git-repos volume mount in the *mcp-gateway anchor:

# Before (hardcoded)
- GIT_REPO_BASEPATH=/git-repos
- git-repos:/git-repos

# After (env-var-driven)
- GIT_REPO_BASEPATH=${GIT_REPO_BASEPATH:-/git-repos}
- ${GIT_REPOS_HOST:-git-repos}:${GIT_REPO_BASEPATH:-/git-repos}:z

Defaults preserve existing behavior, but these apply to agents, supervisor, and e2e-test profiles too (via the anchor). If someone accidentally exports GIT_REPOS_HOST or GIT_REPO_BASEPATH in a prod-like environment, the gateway's volume mount silently changes. The :z SELinux label addition is actually a consistency fix (other volumes already had it), but it's still a behavioral change for non-CLI profiles.

Consider documenting as intentional scope-widening, or limiting the override to mcp-gateway-cli only.

2. Resolution.ERROR leaves the issue stuck with only ymir_manual_trigger

The label lifecycle:

Outcome ymir_manual_trigger Resolution label
Success stays added
Crash (exception) removed
Resolution.ERROR stays skipped

On Resolution.ERROR, the issue keeps ymir_manual_trigger (blocking the fetcher) but gets no resolution label. The issue is effectively claimed with no indication of what happened — diagnosis requires checking container logs. The crash path removes ymir_manual_trigger (allowing fetcher retry), suggesting transient failures should be retryable — Resolution.ERROR is arguably closer to that category.

Consider writing TRIAGE_ERRORED even in direct mode, or documenting this stuck state in the CLI help.

3. stale_labels list is a snapshot from before the workflow runs

current_labels, _ = await tasks.get_jira_issue_metadata(jira_issue)
# ...workflow runs (can take minutes)...
stale_labels = [
    label for label in current_labels
    if label.startswith("ymir_") and label != JiraLabels.MANUAL_TRIGGER.value
]

If another process adds or removes ymir_* labels while the workflow runs, stale_labels would miss them or try to remove labels that are already gone. Low risk in practice (CLI issues shouldn't be fetcher-processed concurrently), but worth documenting the assumption.

4. _normalize_protobuf_ids doesn't handle link IDs

The function only normalizes span-level traceId, spanId, parentSpanId. Span links also carry these fields in base64 after MessageToDict. The trace server doesn't currently process link data, so no runtime impact — but if it ever does, base64 IDs containing / would break URL routing.

5. f-string in logger.warning — style inconsistency

trace_server/server.py line 429:

logger.warning(f"POST /v1/traces rejected: invalid protobuf: {e}")

Other logging calls in the same file use %s-style lazy formatting. The f-string eagerly evaluates str(e) even when WARNING is disabled.

6. functools.cache on _compose_base_cmd never cleared in tests

The cache persists for the entire test process. Each test gets a unique tmp_path so there are no cache hits across tests, but within a single test that calls both start_services and stop_services on the same path (e.g. TestTriageContainerLifecycle), the second call hits the cache and skips the mocked detect_compose_cmd(). Works by accident since the mock returns the same value — but the cache masks the second call.

7. load_dotenv side effects leak between tests

triage() calls load_dotenv(override=False) which sets env vars like CHAT_MODEL. These aren't in _ENV_KEYS and aren't restored in the finally block. Tests that don't use patch.dict(os.environ, clear=True) (e.g. TestEnvVarRestoration) leak these values to subsequent tests. Not currently causing failures, but a latent ordering dependency.

Comment thread ymir/agents/triage_agent.py
if label.startswith("ymir_") and label != JiraLabels.MANUAL_TRIGGER.value
]
try:
await tasks.set_jira_labels(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should try to minimise labelling of Jiras during the CLI triggered runs. The labels are mainly for coordinating what's happening in the service (dedup, routing, status tracking), they don't add much value when triggered locally where the user sees what's happening in their terminal. A lot of service logic relies on these and this could cause some unexpected behaviours (e.g. if the CLI run fails, the label set before the workflow stays and blocks the fetcher from picking up the issue in the service). Maybe we could stick to adding the label to be able to see issues that were invoked from CLI after the run finishes successfully?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking the fetcher is actually what I was going for. Running Ymir through CLI is a valid, but a fundamentally irregular operation. I'd rather avoid possible race between fetcher and CLI. If user wants to triage something outside of the regular workflow, we should trust them that they don't want the regular workflow to touch it, whatever reasons they may have.

That being said. I do understand why do you want the label removed on a failed run.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarification, blocking the fetcher with the label makes sense, agreed.

My concern was also about error comments to be posted to Jira (because of the user_triggered=True). For CLI, the user sees errors in their terminal, so posting them to Jira too feels unnecessary. This behavior was designed for ymir_todo where the user only has Jira as feedback channel.

Comment thread compose.yaml Outdated
@jpodivin

Copy link
Copy Markdown
Collaborator Author

LGTM, but Claude has some remarks (nothing major):

1. Compose changes to mcp-gateway affect all profiles, not just CLI

The diff parameterizes GIT_REPO_BASEPATH, GIT_CONFIG_GLOBAL, and the git-repos volume mount in the *mcp-gateway anchor:

# Before (hardcoded)
- GIT_REPO_BASEPATH=/git-repos
- git-repos:/git-repos

# After (env-var-driven)
- GIT_REPO_BASEPATH=${GIT_REPO_BASEPATH:-/git-repos}
- ${GIT_REPOS_HOST:-git-repos}:${GIT_REPO_BASEPATH:-/git-repos}:z

Defaults preserve existing behavior, but these apply to agents, supervisor, and e2e-test profiles too (via the anchor). If someone accidentally exports GIT_REPOS_HOST or GIT_REPO_BASEPATH in a prod-like environment, the gateway's volume mount silently changes. The :z SELinux label addition is actually a consistency fix (other volumes already had it), but it's still a behavioral change for non-CLI profiles.

Consider documenting as intentional scope-widening, or limiting the override to mcp-gateway-cli only.

2. Resolution.ERROR leaves the issue stuck with only ymir_manual_trigger

The label lifecycle:
Outcome ymir_manual_trigger Resolution label
Success stays added
Crash (exception) removed —
Resolution.ERROR stays skipped

On Resolution.ERROR, the issue keeps ymir_manual_trigger (blocking the fetcher) but gets no resolution label. The issue is effectively claimed with no indication of what happened — diagnosis requires checking container logs. The crash path removes ymir_manual_trigger (allowing fetcher retry), suggesting transient failures should be retryable — Resolution.ERROR is arguably closer to that category.

Consider writing TRIAGE_ERRORED even in direct mode, or documenting this stuck state in the CLI help.

3. stale_labels list is a snapshot from before the workflow runs

current_labels, _ = await tasks.get_jira_issue_metadata(jira_issue)
# ...workflow runs (can take minutes)...
stale_labels = [
    label for label in current_labels
    if label.startswith("ymir_") and label != JiraLabels.MANUAL_TRIGGER.value
]

If another process adds or removes ymir_* labels while the workflow runs, stale_labels would miss them or try to remove labels that are already gone. Low risk in practice (CLI issues shouldn't be fetcher-processed concurrently), but worth documenting the assumption.

4. _normalize_protobuf_ids doesn't handle link IDs

The function only normalizes span-level traceId, spanId, parentSpanId. Span links also carry these fields in base64 after MessageToDict. The trace server doesn't currently process link data, so no runtime impact — but if it ever does, base64 IDs containing / would break URL routing.

5. f-string in logger.warning — style inconsistency

trace_server/server.py line 429:

logger.warning(f"POST /v1/traces rejected: invalid protobuf: {e}")

Other logging calls in the same file use %s-style lazy formatting. The f-string eagerly evaluates str(e) even when WARNING is disabled.

6. functools.cache on _compose_base_cmd never cleared in tests

The cache persists for the entire test process. Each test gets a unique tmp_path so there are no cache hits across tests, but within a single test that calls both start_services and stop_services on the same path (e.g. TestTriageContainerLifecycle), the second call hits the cache and skips the mocked detect_compose_cmd(). Works by accident since the mock returns the same value — but the cache masks the second call.

7. load_dotenv side effects leak between tests

triage() calls load_dotenv(override=False) which sets env vars like CHAT_MODEL. These aren't in _ENV_KEYS and aren't restored in the finally block. Tests that don't use patch.dict(os.environ, clear=True) (e.g. TestEnvVarRestoration) leak these values to subsequent tests. Not currently causing failures, but a latent ordering dependency.

Most of these seem valid. And implementing adjustments should be very easy.

@jpodivin

Copy link
Copy Markdown
Collaborator Author

Looks like something is wrong with the gateway.

@nforro

nforro commented Jul 28, 2026

Copy link
Copy Markdown
Member

Looks like something is wrong with the gateway.

Should be fixed by f96cd96.

Signed-off-by: Jiri Podivin <jpodivin@redhat.com>
lbarcziova
lbarcziova previously approved these changes Aug 3, 2026

@lbarcziova lbarcziova left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just one note, otherwise LGTM, thank you

Comment thread ymir/cli/main.py Outdated
Comment on lines +80 to +82
no_auto_chain: bool = typer.Option(
False, "--no-auto-chain", help="Do not push results to downstream agent queues"
),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole functionality can be removed here, as we don't have the valkey queue and propagation to downstream agents/queues

Signed-off-by: Jiri Podivin <jpodivin@redhat.com>
Assisted-by: Claude Opus 4.6
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.

4 participants