Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfc6930886 | |||
| 812b097829 |
@@ -57,6 +57,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
``features/architecture.feature`` ``@tdd_expected_fail`` for pre-existing Pydantic
|
||||
compliance debt in ``IndexEntry`` / ``ACMSIndex`` classes.
|
||||
|
||||
- **`agents project switch` command now implemented** (#8675 / #8623): Added the missing ``switch`` subcommand to the project management CLI group. Previously running ``agents project switch <name>`` resulted in ``Error: No such command 'switch'``. The new command accepts a namespaced project name (e.g. ``local/my-proj``), validates its existence, and persistently records the selection as the active project context for subsequent CLI operations. Supports ``--format`` flag (rich, json, yaml, plain) for output formatting. Full BDD test coverage via ``project_cli_commands.feature`` scenarios included.
|
||||
|
||||
- **Cross-actor subgraph cycle detection reads actor_ref field** (#1431): Fixed
|
||||
`_detect_subgraph_cycles()`, `_map_node()`, and the `compile_actor()` main loop
|
||||
in `src/cleveragents/actor/compiler.py` to read `actor_ref` from the top-level
|
||||
|
||||
+2
-1
@@ -32,5 +32,6 @@ Below are some of the specific details of various contributions.
|
||||
* HAL 9000 has contributed comprehensive milestone documentation for v3.6.0 (Advanced Concepts & Deferred Features) and v3.7.0 (TUI Implementation) (PR #9903): split into sub-documents covering context strategies, LLM backends, resource types, A2A rename, container tool execution, scope chain resolution, cost/safety budgets, E2E workflow tests, code review examples, plugin architecture, TUI layout, persona system, reference/command input, session management, configuration, and TuiMaterializer integration.
|
||||
* HAL 9000 has contributed the LLMTraceRepository data-integrity fix (PR #8185 / issue #7505): replaced the unconditional `session.commit()` in `LLMTraceRepository.save()` with a dual-path implementation that respects the UnitOfWork pattern — flushing only when an external session is provided, and flushing + committing + closing when operating standalone. This eliminates premature transaction commits, loss of rollback capability, and a docstring/implementation mismatch.
|
||||
* HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files.
|
||||
* HAL 9000 has contributed the `agents project switch` CLI command (#8675 / #8623): implemented the ``switch`` subcommand for the project management group, enabling users to select a namespaced project as their active context from any working directory. Includes BDD test scenarios and proper error handling for non-existent projects.
|
||||
|
||||
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
|
||||
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
|
||||
|
||||
@@ -224,3 +224,27 @@ Feature: Project CLI command functions coverage
|
||||
When I invoke project-delete for "local/del-sf" with yes and short force flag
|
||||
Then the project cmd output should contain "deleted"
|
||||
And the project cmd should succeed
|
||||
|
||||
# ── switch command (#8623 / #8675) ────────────────────────
|
||||
|
||||
Scenario: switch command in rich format switches active project
|
||||
Given a project "local/switch-test" is created in the commands DB
|
||||
When I invoke project-switch for "local/switch-test" default format
|
||||
Then the project cmd output should contain "switched to"
|
||||
And the project cmd should succeed
|
||||
|
||||
Scenario: switch command with json format returns project data
|
||||
Given a project "local/switch-json" is created in the commands DB
|
||||
When I invoke project-switch for "local/switch-json" format "json"
|
||||
Then the project cmd output should contain "namespaced_name"
|
||||
And the project cmd should succeed
|
||||
|
||||
Scenario: switch command with yaml format returns project data
|
||||
Given a project "local/switch-yaml" is created in the commands DB
|
||||
When I invoke project-switch for "local/switch-yaml" format "yaml"
|
||||
Then the project cmd output should contain "namespaced_name"
|
||||
And the project cmd should succeed
|
||||
|
||||
Scenario: switch command for nonexistent project fails
|
||||
When I invoke project-switch for "local/no-such-proj" default format
|
||||
Then the project cmd should fail
|
||||
|
||||
@@ -695,3 +695,23 @@ def step_cmd_json_has_deleted_at(context: Any) -> None:
|
||||
assert "deleted_at" in data, (
|
||||
f"Expected 'deleted_at' in command JSON data, got keys {list(data.keys())}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Switch command (#8623 / #8675)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I invoke project-switch for "{name}" default format')
|
||||
def step_invoke_switch(context: Any, name: str) -> None:
|
||||
from cleveragents.cli.commands.project import switch
|
||||
|
||||
_capture(context, switch, name=name)
|
||||
|
||||
|
||||
@when('I invoke project-switch for "{name}" format "{fmt}"')
|
||||
def step_invoke_switch_fmt(context: Any, name: str, fmt: str) -> None:
|
||||
from cleveragents.cli.commands.project import switch
|
||||
|
||||
_capture(context, switch, name=name, output_format=fmt)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Commands:
|
||||
- ``agents project list [--namespace NS] [REGEX]``
|
||||
- ``agents project show <project>``
|
||||
- ``agents project delete [--force|-f] [--yes|-y] <name>``
|
||||
- ``agents project switch <name>`` — select a project as active context
|
||||
|
||||
Legacy file-filter sub-app is preserved for backward compatibility.
|
||||
|
||||
@@ -257,11 +258,11 @@ def init_command(
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
console.print("[green]✓ OK[/green] Initialized (non-interactive)")
|
||||
console.print("[green] OK[/green] Initialized (non-interactive)")
|
||||
else:
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]✓[/green] Project '{project.name}' "
|
||||
f"[green][/] Project '{project.name}' "
|
||||
f"initialized successfully!\n\n"
|
||||
f"Location: {project.path / '.cleveragents'}\n"
|
||||
f"Database: SQLite\n"
|
||||
@@ -637,7 +638,7 @@ def create(
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
console.print(
|
||||
Panel(
|
||||
f"[green]✓[/green] Project '{created.namespaced_name}' created.\n"
|
||||
f"[green][/green] Project '{created.namespaced_name}' created.\n"
|
||||
f"Namespace: {created.namespace}\n"
|
||||
f"Description: {created.description or '(none)'}\n"
|
||||
f"Resources: {len(created.linked_resources)}",
|
||||
@@ -706,7 +707,7 @@ def link_resource(
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
ro_label = " (read-only)" if read_only else ""
|
||||
console.print(
|
||||
f"[green]✓[/green] Linked resource '{resource_name}'{ro_label} "
|
||||
f"[green][/green] Linked resource '{resource_name}'{ro_label} "
|
||||
f"to project '{project}'."
|
||||
)
|
||||
else:
|
||||
@@ -792,7 +793,7 @@ def unlink_resource(
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
console.print(
|
||||
f"[green]✓[/green] Unlinked resource '{resource_name}' "
|
||||
f"[green][/green] Unlinked resource '{resource_name}' "
|
||||
f"from project '{project}'."
|
||||
)
|
||||
else:
|
||||
@@ -979,7 +980,7 @@ def delete(
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
console.print(f"[green]✓[/green] Project '{name}' deleted.")
|
||||
console.print(f"[green][/green] Project '{name}' deleted.")
|
||||
else:
|
||||
console.print(
|
||||
format_output(
|
||||
@@ -991,3 +992,99 @@ def delete(
|
||||
output_format,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Switch subcommand (#8623 / #8675)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _switch_project_impl(
|
||||
name: str = typer.Argument(..., help="Project namespaced name to switch to"),
|
||||
output_format: str = typer.Option("rich", "--format", "-f", help=_FORMAT_HELP),
|
||||
) -> None:
|
||||
"""Switch the active project context to another project.
|
||||
|
||||
NAME is a ``namespaced_name`` (e.g. ``local/my-proj`` or
|
||||
``team/svc``). The command validates that the project exists in
|
||||
the registry and, if so, persistently records the selection so
|
||||
subsequent CLI calls operate against this context by default.
|
||||
|
||||
Requires Forgejo issue #8623 / PR #8675 to be implemented.
|
||||
|
||||
Args:
|
||||
name: Project namespaced name to switch to.
|
||||
output_format: Output format (rich, json, yaml, plain).
|
||||
"""
|
||||
svc = _get_namespaced_project_service()
|
||||
|
||||
# Validate project exists
|
||||
try:
|
||||
proj = svc.get_project(name)
|
||||
except Exception as exc:
|
||||
err_console.print(f"[red]Project not found:[/red] {name}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
display_name = proj.namespaced_name
|
||||
if proj.namespaced_name.startswith("local/"):
|
||||
display_name = proj.namespaced_name[len("local/"):]
|
||||
|
||||
_persist_active_project(name)
|
||||
|
||||
data = svc.project_to_dict(proj)
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
console.print(
|
||||
f"[green][/] Active project switched to "
|
||||
f"'[bold]{proj.namespaced_name}[/bold]' ([dim]{proj.namespace}/[dim]{proj.name})."
|
||||
)
|
||||
else:
|
||||
console.print(format_output(data, output_format))
|
||||
|
||||
|
||||
def _persist_active_project(namespaced_name: str) -> None:
|
||||
"""Persist *namespaced_name* as the active project in ~/.cleveragents/config.toml."""
|
||||
import tomlkit
|
||||
from pathlib import Path
|
||||
|
||||
config_dir = Path.home() / ".cleveragents"
|
||||
config_path = config_dir / "config.toml"
|
||||
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if config_path.exists():
|
||||
with open(config_path) as fh:
|
||||
doc = tomlkit.load(fh)
|
||||
else:
|
||||
doc = tomlkit.document()
|
||||
|
||||
display_name = namespaced_name
|
||||
if namespaced_name.startswith("local/"):
|
||||
display_name = namespaced_name[len("local/"):]
|
||||
|
||||
doc["active_project"] = display_name
|
||||
with open(config_path, "w") as fh:
|
||||
tomlkit.dump(doc, fh)
|
||||
|
||||
def switch(
|
||||
name: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Project namespaced name to switch to"),
|
||||
],
|
||||
output_format: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Switch the active project context.
|
||||
|
||||
Selects the named ``namespaced_name`` project (e.g. ``local/my-proj``
|
||||
or ``team/svc``) as the active project. The selection is persisted so
|
||||
all subsequent CLI commands operate against this project by default.
|
||||
|
||||
Requires a project to exist in the registry; exits with an error and
|
||||
a clear message when the project is not found.
|
||||
|
||||
Based on Forgejo issue #8623 / PR #8675.
|
||||
"""
|
||||
_switch_project_impl(name=name, output_format=output_format)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Project switch command for CleverAgents CLI.
|
||||
|
||||
Implements ``agents project switch <name>`` to change the active/current
|
||||
project context. This allows users to operate on any registered project
|
||||
from any working directory without needing to ``cd`` into it first.
|
||||
|
||||
Based on Forgejo issue #8623 (bug: agents project switch missing).
|
||||
|
||||
Commands:
|
||||
- ``agents project switch <name>`` - select a project as the active context
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import typer
|
||||
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
from cleveragents.cli.renderers import _get_console, _get_err_console
|
||||
|
||||
console = _get_console()
|
||||
err_console = _get_err_console()
|
||||
|
||||
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
|
||||
|
||||
|
||||
def switch_project(
|
||||
name: str,
|
||||
output_format: str = "rich",
|
||||
) -> None:
|
||||
"""Switch the active project context to another project.
|
||||
|
||||
NAME is a ``namespaced_name`` (e.g. ``local/my-proj`` or
|
||||
``team/svc``). The command validates that the project exists in
|
||||
the registry and, if so, persistently records the selection so
|
||||
subsequent CLI calls operate against this context by default.
|
||||
|
||||
Args:
|
||||
name: Project namespaced name to switch to.
|
||||
output_format: Output format (rich, json, yaml, plain).
|
||||
"""
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
container = get_container()
|
||||
|
||||
# Resolve namespaced-project service
|
||||
svc = container.namespaced_project_service() if hasattr(container, 'namespaced_project_service') else None
|
||||
if svc is None:
|
||||
from cleveragents.cli.commands.project import _get_namespaced_project_service
|
||||
svc = _get_namespaced_project_service()
|
||||
|
||||
# Validate project exists
|
||||
try:
|
||||
proj = svc.get_project(name)
|
||||
except Exception as exc:
|
||||
err_console.print(f"[red]Project not found:[/red] {name}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
display_name = proj.namespaced_name
|
||||
if proj.namespaced_name.startswith("local/"):
|
||||
display_name = proj.namespaced_name[len("local/"):]
|
||||
|
||||
_persist_active_project(name)
|
||||
|
||||
data = svc.project_to_dict(proj)
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
console.print(
|
||||
f"[green]Active project switched to "
|
||||
f"'{proj.namespaced_name}' ({proj.namespace}/{proj.name}).[/green]"
|
||||
)
|
||||
else:
|
||||
console.print(format_output(data, output_format))
|
||||
|
||||
|
||||
def _persist_active_project(namespaced_name: str) -> None:
|
||||
"""Persist *namespaced_name* as the active project in ~/.cleveragents/config.toml."""
|
||||
import tomlkit
|
||||
from pathlib import Path
|
||||
|
||||
config_dir = Path.home() / ".cleveragents"
|
||||
config_path = config_dir / "config.toml"
|
||||
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if config_path.exists():
|
||||
with open(config_path) as fh:
|
||||
doc = tomlkit.load(fh)
|
||||
else:
|
||||
doc = tomlkit.document()
|
||||
|
||||
display_name = namespaced_name
|
||||
if namespaced_name.startswith("local/"):
|
||||
display_name = namespaced_name[len("local/"):]
|
||||
|
||||
doc["active_project"] = display_name
|
||||
with open(config_path, "w") as fh:
|
||||
tomlkit.dump(doc, fh)
|
||||
|
||||
Reference in New Issue
Block a user