Skip to content
Draft
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
52 changes: 52 additions & 0 deletions .github/workflows/issue-filters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Issue filters

# The existing CI and parity workflows do not target this feature's stacked
# base. Keep verification separate from the workflow edit owned by feat/otel.
on:
pull_request:
branches: [feat/fp-cli-parity]
paths:
- "fp-cloud-cli/**"
- ".github/workflows/issue-filters.yml"
- "CHANGELOG.md"

permissions:
contents: read

concurrency:
group: issue-filters-${{ github.ref }}
cancel-in-progress: true

jobs:
cloud-cli:
name: Issue filters (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
defaults:
run:
working-directory: fp-cloud-cli
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: fp-cloud-cli/uv.lock
- run: uv sync --locked --extra dev --python ${{ matrix.python-version }}
- name: Test all CLI behaviour
env:
FP_CLI_REQUIRE_CONTRACT: "1"
run: uv run --extra dev pytest -q
- run: uv build
- name: Clean wheel install and user-level issue-filter smoke
run: |
uv venv /tmp/fp-issue-smoke --python ${{ matrix.python-version }}
uv pip install --python /tmp/fp-issue-smoke/bin/python dist/*.whl
/tmp/fp-issue-smoke/bin/fp --version
/tmp/fp-issue-smoke/bin/python tests/issue_filters_smoke.py \
--fp /tmp/fp-issue-smoke/bin/fp --artifacts /tmp/fp-issue-artifacts
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Changelog

## 1.0.11-beta.1 — 2026-10-13
## 1.0.11-beta.1 — 2026-10-14

### Features

- Add Cloud CLI issue filters and facets for agent, environment, machine, source/alert/audit, assignee, opened dates and title/summary text (#900).

- Cloud CLI parity: hosted code, LLM-judge and Jev evaluator authoring/deployment, dashboards, sentiment, assistant artifacts/approvals, org controls and the shared mint/pink screen language (#885)

- New terminal look for the CLI, step one: the palette gains the redesign's label grey, progress-track grey and a red for failures. Descriptions keep the terminal's own colour, and the greys fall back to the dim attribute below 24-bit, so every screen stays readable on light themes (#881)
Expand Down Expand Up @@ -31,6 +33,8 @@

### Fixes

- Align Cloud CLI issue tags, JSON and alert/audit facets with the server's top-level scope contract, without extra requests on normal filtered calls (#900).

- `failproofai policies` showed nothing for a machine still on the pre-pack builtins, while those builtins were enforcing. It now says how many under BUILT IN, with the command that moves them into a pack (#881)
- `failproofai config --disconnect` told you to restart the daemon to stop it sending. It stops on its own within a few seconds, because it polls the credential, and now says so (#881)
- A config change that landed while the daemon was restarting its collector after a backfill was recorded as already running and never applied. `failproofai config` re-adding an agent hits exactly that, writing its backfill request and then the selection, so the agent stayed off until some unrelated edit (#881)
Expand Down
36 changes: 36 additions & 0 deletions __tests__/ci/issue-filters-workflow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @vitest-environment node
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";

const workflow = parse(readFileSync(
resolve(process.cwd(), ".github/workflows/issue-filters.yml"), "utf8",
));
const scripts = (job: { steps: { run?: string }[] }) =>
job.steps.map((step) => step.run ?? "").join("\n");

describe("issue-filter stacked verification", () => {
it("tests the actual PR base without publishing or release credentials", () => {
expect(workflow.on.pull_request.branches).toContain("feat/fp-cli-parity");
expect(workflow.on.workflow_dispatch).toBeUndefined();
expect(workflow.permissions).toEqual({ contents: "read" });
expect(workflow.on.pull_request.paths).toContain("fp-cloud-cli/**");
});

it("keeps this client-only feature's verification scoped to the CLI", () => {
expect(Object.keys(workflow.jobs)).toEqual(["cloud-cli"]);
expect(workflow.on.pull_request.paths).not.toContain("crates/fpai-collect/**");
});

it("tests both auth modes across supported Python versions and a clean wheel", () => {
const job = workflow.jobs["cloud-cli"];
expect(job.strategy.matrix["python-version"]).toEqual(["3.10", "3.11", "3.12", "3.13"]);
const run = scripts(job);
expect(run).toContain("uv sync --locked --extra dev");
expect(run).toContain("pytest -q");
expect(run).toContain("uv build");
expect(run).toContain("uv pip install --python /tmp/fp-issue-smoke/bin/python dist/*.whl");
expect(run).toContain("tests/issue_filters_smoke.py");
});
});
6 changes: 5 additions & 1 deletion fp-cloud-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Changelog — `fp` CLI

## 0.0.2b1 — 2026-10-13
## 0.0.2b1 — 2026-10-14

### Added

- Filter issues list/count and discover facets by agent, environment, machine, source/alert/audit, assignee, opened dates and title/summary text in session or key mode; show agent tags and warn when an older Cloud may ignore filters (#900).

- Hosted evaluator authoring for code, LLM judge and Jev: create/deploy, AI draft specs, dry-run, labels, toggles, instances, schema, jobs and code-only backfill.
- Dashboards and tiles; session overview/export/history/re-evaluate; latency and sentiment; query composition/export; alert recipients/issues and agent contracts.
- Policy author/backtest/candidacy, assistant artifact tables, fail-closed interactive write approvals, and agent-curated home briefs.
Expand All @@ -17,6 +19,8 @@

### Fixes

- Align issue scope JSON and named alert/audit facets with agenteye #1074; avoid capability requests on normal filtered calls and limit empty-result probes and older-Cloud warnings to once per process (#900).

- Remove the unused colour-keyed permission rank, document full-id rendering, emit UTC `Z` timestamps for deploy backfills, and report approval-answer failures without a traceback while restoring the terminal (#885).
- Reject natural-language binary judge steps, distinguish unknown routes from missing records on 404, normalize standalone backfill ranges to UTC `Z`, expose all subcommands in help, and render artifact headers before titles with consistent session-only wording (#885).
- New Cloud commands use deliberate human summaries/tables and actionable empty states at 80/140 columns without raw response blobs. Evaluator previews display before writes and require explicit `--force` after failures; plan/claim deny key mode, deploy specs can omit the positional key, and older settings-route 404s degrade clearly. Existing JSON payloads stay unchanged (#885).
Expand Down
28 changes: 28 additions & 0 deletions fp-cloud-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,34 @@ fp --json events --session-id run-001 --all | jq '.events[].payload'
fp --json sessions --since 7d --fields session_id,status,scores
```

### Filter the failures inbox

Issue filters work with either a signed-in session or an API key carrying `issues:read`:

```bash
fp --json issues facets --env prod
fp issues list --agent claude-repo --agent codex-repo --env prod --assignee me --since 7d
fp --json issues count --source audit --audit weekly-audit --machine ci-runner-01
fp issues list --agent claude-checkout --assignee unassigned --search "tool timeout" \
--since 2026-10-01T00:00:00Z --until 2026-10-14T00:00:00Z
```

`issues list`, `count` and `facets` share `--agent`, `--env`,
`--machine`, `--source alert|audit|manual`, `--alert`, `--audit` and
`--assignee EMAIL|me|unassigned`. These accept repeated flags or comma-separated
values (OR within a filter, AND between filters). `--since` accepts the existing
`all|15m|1h|6h|24h|7d` presets or RFC3339; `--until` accepts RFC3339. Dates apply
to issue opening, and `--search` searches title + summary. Human rows show the
stored agent scope; facets show available values and counts.

Older Clouds may ignore the new query parameters. A returned issue's top-level
`agent_ids` key confirms filter support without another request. Non-empty
legacy rows warn once; empty lists or zero counts probe facets at most once
per process, and a missing route warns once that results may be unfiltered.
Positive counts make no extra request. Warnings leave stdout JSON usable. Calling
missing `facets` directly exits 6 with “not available on this FailproofAI Cloud yet”.
Upgrade Cloud for filtering. Existing `list --alert-id` remains supported.

### Cloud-managed policies

Three commands for the three jobs the dashboard splits across three pages —
Expand Down
79 changes: 72 additions & 7 deletions fp-cloud-cli/fp_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ def _cloud_not_found(exc: FpCliError, path: str, *, body=None, params=None) -> N
or family == "queries" and segments[1:] in (["run"], ["schema"])
or family == "audits" and segments == ["audits", "contracts"]
or family == "alerts" and segments == ["alerts", "recipients"]
or family == "issues" and segments in (["issues", "count"], ["issues", "facets"])
or family == "users" and segments == ["users", "defaults"]
or family == "usage" and segments == ["usage", "windows"]
or family == "settings" and segments == ["settings", "model-context-windows"]
Expand Down Expand Up @@ -1293,27 +1294,45 @@ def test_alert(ctx: ClientContext, alert_id: str, channels: Optional[List[Dict[s

# --- Incidents --------------------------------------------------------------

# Capability detection is only a fallback for empty results. Keep both the
# probe and the compatibility warning to at most once per CLI process.
_issue_facets_probed = False
_issue_facets_available: Optional[bool] = None
_issue_filters_warned = False


def list_incidents(
ctx: ClientContext,
*,
state: Optional[str] = None,
alert_id: Optional[str] = None,
limit: Optional[int] = None,
filters: Optional[Dict[str, str]] = None,
on_filters_unavailable: Optional[Callable[[], None]] = None,
) -> List[Incident]:
"""GET /api/issues (or /api/alerts/{id}/issues when alert_id given). The
alert-scoped path honours ``state``/``limit`` too — pass them so the filters aren't silently
dropped on that path (``_get_json`` omits None params)."""
if alert_id:
if alert_id and not filters:
data = _get_json(ctx, f"/api/alerts/{alert_id}/issues", {"state": state, "limit": limit})
else:
data = _get_json(ctx, "/api/issues", {"state": state, "limit": limit})
return [Incident.from_dict(i) for i in (data if isinstance(data, list) else [])]


def count_incidents(ctx: ClientContext, *, state: Optional[str] = None) -> int:
params = {"state": state, "limit": limit, **(filters or {})}
if alert_id:
params["alert_id"] = alert_id
data = cloud_request(ctx, "GET", "/api/issues", params=params)
items = data.get("issues", []) if isinstance(data, dict) else data
_check_issue_filters(ctx, items, filters, on_filters_unavailable)
return [Incident.from_dict(i) for i in (items if isinstance(items, list) else [])]


def count_incidents(
ctx: ClientContext, *, state: Optional[str] = None,
filters: Optional[Dict[str, str]] = None,
on_filters_unavailable: Optional[Callable[[], None]] = None,
) -> int:
"""GET /api/issues/count — {count}."""
data = _get_json(ctx, "/api/issues/count", {"state": state})
data = cloud_request(ctx, "GET", "/api/issues/count", params={"state": state, **(filters or {})})
_check_issue_filters(ctx, data, filters, on_filters_unavailable)
if not isinstance(data, dict):
return 0
try:
Expand All @@ -1322,6 +1341,52 @@ def count_incidents(ctx: ClientContext, *, state: Optional[str] = None) -> int:
return 0


def issue_facets(
ctx: ClientContext, *, state: Optional[str] = None,
filters: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
"""GET /api/issues/facets — available scope/source/assignee values with counts."""
data = cloud_request(ctx, "GET", "/api/issues/facets", params={"state": state, **(filters or {})})
if not isinstance(data, dict):
raise ApiError("Cloud returned an invalid issues facets response")
return data


def _check_issue_filters(
ctx: ClientContext, data: Any, filters: Optional[Dict[str, str]],
on_unavailable: Optional[Callable[[], None]],
) -> None:
"""New Cloud always sends `agent_ids`, even when its array is empty.

Non-empty lists confirm support from the raw rows (before model defaults
fill missing keys), or warn for legacy rows without making another request.
Empty lists/zero counts probe facets once per process. Positive counts have
no scope marker and make no extra request; a cached missing route still
warrants a warning. Probe auth/network/server errors remain visible.
"""
global _issue_facets_probed, _issue_facets_available, _issue_filters_warned
if not filters:
return
if isinstance(data, list) and data:
if any(isinstance(row, dict) and "agent_ids" in row for row in data):
return
unavailable = True
else:
empty = data == [] or isinstance(data, dict) and data.get("count") == 0
if empty and not _issue_facets_probed:
_issue_facets_probed = True
try:
issue_facets(ctx, filters=filters)
except NotFoundError:
_issue_facets_available = False
else:
_issue_facets_available = True
unavailable = _issue_facets_available is False
if unavailable and on_unavailable and not _issue_filters_warned:
_issue_filters_warned = True
on_unavailable()


def get_incident(ctx: ClientContext, incident_id: str) -> Incident:
"""GET /api/issues/{id} — full detail (comments, subscribers, activity)."""
return Incident.from_dict(_get_json(ctx, f"/api/issues/{incident_id}"))
Expand Down
Loading
Loading