diff --git a/.forgejo/workflows/master.yml b/.forgejo/workflows/master.yml index 7c959ba40..ccdede22d 100644 --- a/.forgejo/workflows/master.yml +++ b/.forgejo/workflows/master.yml @@ -3,8 +3,6 @@ name: CI on: push: branches: [master, develop] - pull_request: - branches: [master, develop] vars: docker_prefix: "http://harbor.cleverthis.com/docker/" diff --git a/CHANGELOG.md b/CHANGELOG.md index d098d8464..4fa2ac4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -343,6 +343,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). had `@tdd_expected_fail` removed and now run as permanent regression guards. Net result: 629 features active in CI (up from ~545), zero `@skip` tags remain. +- **Actor Context List Regex Filter** (#6500): `agents actor context list` now + accepts an optional `[REGEX]` positional argument to filter listed named + contexts by a regular expression pattern. Invalid patterns are reported with + a clear error message. The deprecated `agents context list` alias receives + the same filtering capability. + - **Git Worktree Sandbox Apply** (#4454): The `plan apply` command now merges LLM-generated changes via `git merge` from an isolated worktree branch instead of flat `shutil.copy2`. Displays spec-aligned Apply Summary diff --git a/features/actor_context_cmds.feature b/features/actor_context_cmds.feature index 2e716f9bd..69f5ee28c 100644 --- a/features/actor_context_cmds.feature +++ b/features/actor_context_cmds.feature @@ -6,6 +6,15 @@ Feature: Actor context clear, remove, export, and import commands Background: Given a temporary context directory for actor context tests + @actor_context_list_regex + Scenario: List actor contexts filtered by regex + Given an actor context named "docs" exists + And an actor context named "notes" exists + When I run actor context list "docs" + Then the actor context list command should succeed + And the list output should contain "docs" + And the list output should not contain "notes" + # ── context clear ───────────────────────────────────────── Scenario: Clear a named actor context diff --git a/features/steps/actor_context_cmds_steps.py b/features/steps/actor_context_cmds_steps.py index 16925aa17..32547cee7 100644 --- a/features/steps/actor_context_cmds_steps.py +++ b/features/steps/actor_context_cmds_steps.py @@ -105,6 +105,36 @@ def step_create_json_file_with_name(context, name): context.import_file.write_text(json.dumps(data, indent=2), encoding="utf-8") +# --------------------------------------------------------------------------- +# When — list +# --------------------------------------------------------------------------- + + +@when('I run actor context list "{pattern}"') +def step_list_with_pattern(context, pattern): + context.result = context.runner.invoke( + actor_context_app, + [ + "list", + pattern, + "--context-dir", + str(context.context_dir), + ], + ) + + +@when("I run actor context list without a pattern") +def step_list_without_pattern(context): + context.result = context.runner.invoke( + actor_context_app, + [ + "list", + "--context-dir", + str(context.context_dir), + ], + ) + + # --------------------------------------------------------------------------- # When — clear # --------------------------------------------------------------------------- @@ -383,6 +413,15 @@ def step_roundtrip_import(context, name): # --------------------------------------------------------------------------- +@then("the actor context list command should succeed") +def step_list_success(context): + assert context.result.exit_code == 0, ( + f"Expected exit 0, got {context.result.exit_code}.\n" + f"stdout: {context.result.output}\n" + f"stderr: {getattr(context.result, 'stderr', '')}" + ) + + @then("the actor context clear command should succeed") def step_clear_success(context): assert context.result.exit_code == 0, ( @@ -514,6 +553,18 @@ def step_all_contexts_empty(context): # --------------------------------------------------------------------------- +@then('the list output should contain "{text}"') +def step_list_output_contains(context, text): + output = context.result.output + assert text in output, f"Expected '{text}' in list output: {output}" + + +@then('the list output should not contain "{text}"') +def step_list_output_not_contains(context, text): + output = context.result.output + assert text not in output, f"Did not expect '{text}' in list output: {output}" + + @then('the output should contain valid JSON with key "{key}"') def step_output_json_key(context, key): output = context.result.output diff --git a/robot/cli_plan_context_commands.robot b/robot/cli_plan_context_commands.robot index ccc291d17..88a9f7fe6 100644 --- a/robot/cli_plan_context_commands.robot +++ b/robot/cli_plan_context_commands.robot @@ -81,6 +81,38 @@ List Context Files [Teardown] Cleanup Test Directory +Actor Context List Supports Regex Filter + [Documentation] Verify actor context list supports optional regex filtering + ... Uses --context-dir to point at a directory of named actor + ... context subdirectories so the test is self-contained and + ... does not depend on project-context state. + [Setup] Setup Test Directory + + # Create a temporary context directory with two named actor context subdirs + ${ctx_dir}= Set Variable ${TEST_DIR}${/}actor_contexts + Create Directory ${ctx_dir} + Create Directory ${ctx_dir}${/}test-context + Create Directory ${ctx_dir}${/}utils-context + + # Filter by "test" — should match test-context but not utils-context + ${result}= Run Process ${PYTHON} -m cleveragents actor context list test + ... --context-dir ${ctx_dir} + ... cwd=${TEST_DIR} timeout=120s on_timeout=kill + + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} test-context + Should Not Contain ${result.stdout} utils-context + + # Filter by "nomatch" — no contexts match, should print "No contexts found." + ${result}= Run Process ${PYTHON} -m cleveragents actor context list nomatch + ... --context-dir ${ctx_dir} + ... cwd=${TEST_DIR} timeout=120s on_timeout=kill + + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} No contexts found. + + [Teardown] Cleanup Test Directory + Show Context Content [Documentation] Test context show command [Setup] Initialize Test Project With Context diff --git a/src/cleveragents/cli/commands/actor_context.py b/src/cleveragents/cli/commands/actor_context.py index c90d33e49..c6b8f66f9 100644 --- a/src/cleveragents/cli/commands/actor_context.py +++ b/src/cleveragents/cli/commands/actor_context.py @@ -12,6 +12,7 @@ from __future__ import annotations import hashlib import json +import re import shutil from pathlib import Path from typing import Annotated, Any @@ -31,6 +32,46 @@ console = Console() _FORMAT_HELP = "Output format: json, yaml, plain, table, rich, or color (default: rich)" + +@app.command("list") +def context_list( + regex: Annotated[ + str | None, + typer.Argument(help="Optional regex filter for context names"), + ] = None, + context_dir: Annotated[ + Path | None, + typer.Option( + "--context-dir", + help="Directory where contexts are stored", + resolve_path=True, + ), + ] = None, +) -> None: + """List named actor contexts, optionally filtered by a regular expression.""" + + pattern: re.Pattern[str] | None = None + if regex is not None: + try: + pattern = re.compile(regex) + except re.error as exc: + console.print(f"[red]Error:[/red] Invalid regex pattern: {exc}") + raise typer.Abort() from exc + + base = _default_context_base(context_dir) + context_names = _list_context_names(base) + + if pattern is not None: + context_names = [name for name in context_names if pattern.search(name)] + + if not context_names: + typer.echo("No contexts found.") + return + + for name in context_names: + typer.echo(name) + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/src/cleveragents/cli/commands/context.py b/src/cleveragents/cli/commands/context.py index 50959c1d9..f0a5771ab 100644 --- a/src/cleveragents/cli/commands/context.py +++ b/src/cleveragents/cli/commands/context.py @@ -9,6 +9,7 @@ Deprecated alias: ``agents context `` (emits deprecation warning) from __future__ import annotations +import re from pathlib import Path from typing import TYPE_CHECKING, Annotated, Any @@ -396,6 +397,10 @@ def context_rm_deprecated( @app.command("list") def context_list( + regex: Annotated[ + str | None, + typer.Argument(help="Optional regex filter for context names"), + ] = None, context_dir: Annotated[ Path | None, typer.Option( @@ -409,6 +414,14 @@ def context_list( Or list named contexts in a directory. """ + pattern: re.Pattern[str] | None = None + if regex is not None: + try: + pattern = re.compile(regex) + except re.error as exc: + console.print(f"[red]Error:[/red] Invalid regex pattern: {exc}") + raise typer.Abort() from exc + if context_dir is not None: # List named contexts in the given directory ctx_base = context_dir @@ -417,6 +430,8 @@ def context_list( return context_dirs = sorted([d.name for d in ctx_base.iterdir() if d.is_dir()]) + if pattern is not None: + context_dirs = [name for name in context_dirs if pattern.search(name)] if not context_dirs: typer.echo("No contexts found.") @@ -451,24 +466,36 @@ def context_list( console.print("Use 'agents actor context add ' to add files.") return - # Display context files - table = Table(title=f"Context Files ({len(context_files)} total)") - table.add_column("File Path", style="cyan") - table.add_column("Type", style="green") - table.add_column("Size", style="magenta") - table.add_column("Added", style="yellow") - + # Filter and normalize entries + filtered_entries: list[tuple[str, str, str, str]] = [] for file_info in context_files: path_text, type_label, size, added_at, _ = _normalize_context_entry( file_info ) display_name = Path(path_text).name if path_text else "" + if pattern is not None and not pattern.search(display_name): + continue + if isinstance(size, str): size_str = size else: size_str = f"{size:,} bytes" if size > 0 else "0 bytes" + filtered_entries.append((display_name, type_label, size_str, added_at)) + + if not filtered_entries: + console.print("[yellow]No files matched the pattern.[/yellow]") + return + + # Display context files + table = Table(title=f"Context Files ({len(filtered_entries)} total)") + table.add_column("File Path", style="cyan") + table.add_column("Type", style="green") + table.add_column("Size", style="magenta") + table.add_column("Added", style="yellow") + + for display_name, type_label, size_str, added_at in filtered_entries: table.add_row( display_name, type_label,