From 9d60b06d0d1928175b4f340b8cb47a1adff1e364 Mon Sep 17 00:00:00 2001 From: Rubens Andrade <38709237+andrader@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:34:44 -0300 Subject: [PATCH 1/6] feat: rename "agent" to "harness" and ".agents" terminology cleanup --- AGENTS.md | 7 +- README.md | 43 +++---- docs/jup-vs-npx-skills.md | 5 +- src/jup/commands/add.py | 10 +- src/jup/commands/list.py | 54 ++++---- src/jup/commands/remove.py | 14 +-- src/jup/commands/sync.py | 38 +++--- src/jup/commands/utils.py | 14 +-- src/jup/config.py | 30 ++--- src/jup/main.py | 116 ++++++++++-------- src/jup/models.py | 25 ++-- tests/e2e/test_workflow.py | 12 +- tests/integration/test_add_unmanaged.py | 48 ++++---- tests/integration/test_cli_commands.py | 12 +- tests/integration/test_cli_config.py | 28 ++--- ...t_commands.py => test_harness_commands.py} | 68 +++++----- tests/integration/test_sync_safety.py | 24 ++-- tests/unit/test_config.py | 18 +-- tests/unit/test_models.py | 14 +-- uv.lock | 2 +- 20 files changed, 306 insertions(+), 276 deletions(-) rename tests/integration/{test_agent_commands.py => test_harness_commands.py} (55%) diff --git a/AGENTS.md b/AGENTS.md index b254c4e..7015bce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ # AGENTS.md - Project Context & Guidelines โœจ -`jup` is a lightweight Python-based command-line tool designed to manage and synchronize "agent skills" across various AI assistant directories (e.g., Gemini, Copilot, Claude). It allows users to install skills from GitHub repositories or local directories, keeping them organized via a lockfile and syncing them to target locations using symlinks or file copies. +`jup` is a lightweight Python-based command-line tool designed to manage and synchronize "agent skills" across various AI assistant directories (e.g., Gemini, Copilot, Claude, Codex). + It allows users to install skills from GitHub repositories or local directories, keeping them organized via a lockfile and syncing them to target locations using symlinks or file copies. ALWAYS use subagents for independent steps. ALWAYS follow the development workflow outlined below to ensure consistency, maintainability, and quality across the codebase. This workflow is designed to encourage thoughtful implementation, thorough testing, and clear documentation. @@ -35,7 +36,7 @@ jup backlog * `src/jup/main.py`: Main entry point and configuration sub-commands (`jup config ...`). * `src/jup/commands/`: Package containing core CLI commands implementation (`add`, `remove`, `sync`, `list`, `show`, `find`). * `src/jup/config.py`: Handles `~/.jup` persistence, config files, and the skills lockfile. - * `src/jup/models.py`: Defines Pydantic models for configuration, skill sources, and the `DEFAULT_AGENTS` registry. + * `src/jup/models.py`: Defines Pydantic models for configuration, skill sources, and the `DEFAULT_HARNESSES` registry. * **Internal Storage:** Skills are cached/stored in `~/.jup/skills/` before being synced. ## Mandates & Core Workflows @@ -62,7 +63,7 @@ jup backlog - **Skill Structure:** `jup` expects a directory containing a `SKILL.md` file. It supports single-skill directories or collections. - **Sync Behavior:** `sync` only manages entries in the lockfile. It does not preserve arbitrary manual edits inside managed target directories. - **Sync Mode:** The `sync-mode` alias in the CLI maps to `sync_mode` in the config model. -- **Agent Registry:** Supported agents and their default paths are defined in `src/jup/models.py`. The `get_scope_dir` function returns the exact path to an agent's skills directory (e.g., `~/.gemini/skills`). Tests frequently patch this registry. +- **Harness Registry:** Supported harnesses and their default paths are defined in `src/jup/models.py`. The `get_scope_dir` function returns the exact path to a harness's skills directory (e.g., `~/.gemini/skills`). Tests frequently patch this registry. - **Path Consistency:** Ensure that `list`, `sync`, and `remove` commands use `get_scope_dir` directly for the default target, avoiding redundant `/skills` nesting. ### Documentation diff --git a/README.md b/README.md index 19e3c60..a68bf29 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ It helps you: - install skills from GitHub repositories that expose a top-level `skills/` folder (or `.claude/skills/` as a fallback) - keep installed skills organized in a lockfile so they can be synced again later -- copy or link skills into the directories used by agents like Gemini, Copilot, and Claude +- copy or link skills into the directories used by harnesses like Gemini, Copilot, and Claude ## Features โœจ -- **Multi-Agent Support**: Sync skills for Gemini, Copilot, and Claude. +- **Multi-Harness Support**: Sync skills for Gemini, Copilot, Claude, and Codex. - **Local-First**: Works with local skill directories and global configurations. - **Git Integration**: Install and update skills directly from GitHub. @@ -51,16 +51,16 @@ jup config show ![jup config show](docs/images/config_show.svg) -### 3. Choose which agent directories should receive synced skills ๐Ÿค– +### 3. Choose which harness directories should receive synced skills ๐Ÿค– ```bash -jup config set agents gemini,copilot,claude +jup config set harnesses gemini,copilot,claude ``` Use `none` to clear the list: ```bash -jup config set agents none +jup config set harnesses none ``` ### 4. Add skills โž• @@ -118,7 +118,7 @@ jup list ![jup list](docs/images/list.svg) -- Shows all installed skills, their source repo (with clickable links in supported terminals), install/update date, and which agent directories they are synced to. +- Shows all installed skills, their source repo (with clickable links in supported terminals), install/update date, and which harness directories they are synced to. #### Check for updates and apply them @@ -130,7 +130,7 @@ jup sync --update - You can also use `jup sync --check` to only check for updates without applying them. - The update status and last checked date are shown in `jup list`. -### 6. Push the managed skills into the configured agent directories ๐Ÿ”„ +### 6. Push the managed skills into the configured harness directories ๐Ÿ”„ ```bash jup sync @@ -138,7 +138,7 @@ jup sync ## Comparison with `npx skills` โš–๏ธ -While Vercel's `npx skills` is a fantastic package manager for AI skills with a built-in search registry, `jup` focuses heavily on **centralized lockfile management** and **local symlink synchronization** across multiple agents. `jup` is ideal if you want to maintain a single source of truth for your skills and automatically symlink them to Gemini, Claude, and Copilot simultaneously, especially when authoring skills locally. +While Vercel's `npx skills` is a fantastic package manager for AI skills with a built-in search registry, `jup` focuses heavily on **centralized lockfile management** and **local symlink synchronization** across multiple harnesses. `jup` is ideal if you want to maintain a single source of truth for your skills and automatically symlink them to Gemini, Claude, and Copilot simultaneously, especially when authoring skills locally. For a full breakdown of commands, pros, and cons, see the [jup vs. npx skills comparison](docs/jup-vs-npx-skills.md). @@ -178,7 +178,7 @@ After that, `jup sync` installs the managed skills into the configured target lo jup config set sync-mode copy ``` -Skills are placed directly into the agent's skill folder (e.g., `~/.gemini/skills/my-skill/`), ensuring they are correctly discovered by the agent. +Skills are placed directly into the harness's skill folder (e.g., `~/.gemini/skills/my-skill/`), ensuring they are correctly discovered by the harness. ### Update and Check Features @@ -189,41 +189,42 @@ Skills are placed directly into the agent's skill folder (e.g., `~/.gemini/skill The main configuration values are: - `scope`: `global` or `local` -- `agents`: a comma-separated list of agent names +- `harnesses`: a comma-separated list of harness names - `sync-mode`: `link` or `copy` -### 7. Manage custom agent providers ๐Ÿค– +### 7. Manage custom harness providers ๐Ÿค– -You can add your own agent providers if they use a standard `skills/` directory structure: +You can add your own harness providers if they use a standard `skills/` directory structure: ```bash # List all providers -jup agent list +jup harness list # Add a new custom provider -jup agent add myagent --global-location ~/.myagent/skills --local-location ./.myagent/skills +jup harness add myharness --global-location ~/.myharness/skills --local-location ./.myharness/skills # Edit an existing custom provider -jup agent edit myagent --local-location ./new-path/skills +jup harness edit myharness --local-location ./new-path/skills # Remove a custom provider -jup agent remove myagent +jup harness remove myharness ``` -Once a custom agent is added, you can activate it in your configuration: +Once a custom harness is added, you can activate it in your configuration: ```bash -jup config set agents gemini,myagent +jup config set harnesses gemini,myharness ``` -## Supported Agents ๐Ÿงฉ +## Supported Harnesses ๐Ÿงฉ -`jup` includes built-in locations for these agent names: +`jup` includes built-in locations for these harness names: - `gemini` - `copilot` - `claude` -- `default` +- `codex` +- `.agents` ## Contributing ๐Ÿค diff --git a/docs/jup-vs-npx-skills.md b/docs/jup-vs-npx-skills.md index dcf7775..0623330 100644 --- a/docs/jup-vs-npx-skills.md +++ b/docs/jup-vs-npx-skills.md @@ -8,7 +8,8 @@ Both `jup` and `npx skills` are CLI tools designed to manage and install Open Ag | :--- | :--- | :--- | | **Language / Runtime** | Python (via `uv` or `pip`) | Node.js (via `npx`) | | **State Management** | Central lockfile (`~/.jup/skills-lock.json`) | Global or per-project installation | -| **Multi-Agent Sync** | **Yes.** Symlinks/copies from a central cache to all configured agents via `jup sync` | **Yes.** Via `-a` flags during install (e.g., `-a gemini-cli -a cursor`) | +| **Multi-Harness Sync** | + **Yes.** Symlinks/copies from a central cache to all configured agents via `jup sync` | **Yes.** Via `-a` flags during install (e.g., `-a gemini-cli -a cursor`) | | **Search / Registry** | **Yes.** Built-in search (`jup find`) backed by `skills.sh` | **Yes.** Built-in search (`npx skills find`) backed by `skills.sh` | | **Local Development** | Excellent. Symlinks local directories directly into agent folders | Good. Includes an `init` command for scaffolding | | **Updates** | `jup sync --update` | `npx skills update` / `npx skills check` | @@ -21,7 +22,7 @@ Both `jup` and `npx skills` are CLI tools designed to manage and install Open Ag * `jup sync`: Syncs managed skills from the central storage into the configured agent directories (e.g., `.gemini/skills`, `.claude/skills`). * `jup list`: Shows all installed skills, locations, and last update times. * `jup remove `: Uninstalls a skill or repository. -* `jup config set agents gemini,claude`: Configures which agents receive the synced skills. +* `jup config set harnesses gemini,claude`: Configures which harnesses receive the synced skills. ### `npx skills` * `npx skills find [query]`: Interactively searches for available skills in the community registry. diff --git a/src/jup/commands/add.py b/src/jup/commands/add.py index 5775844..a76f68f 100644 --- a/src/jup/commands/add.py +++ b/src/jup/commands/add.py @@ -20,7 +20,7 @@ GITHUB_SOURCE_TYPE, rel_home, run_git_clone, - is_path_in_agent_dir, + is_path_in_harness_dir, ) @@ -70,12 +70,12 @@ def add_skill( raise typer.Exit(code=1) resolved_local = local_path.resolve() - - agent_name = is_path_in_agent_dir(resolved_local, config) - if agent_name: + harness_name = is_path_in_harness_dir(resolved_local, config) + if harness_name: print( - f"[yellow]Source is inside an agent directory ({agent_name}).[/yellow]" + f"[yellow]Source is inside a harness directory ({harness_name}).[/yellow]" ) + if typer.confirm( "Move to central storage? (Recommended for management)", default=True ): diff --git a/src/jup/commands/list.py b/src/jup/commands/list.py index c56f67d..c9ad16d 100644 --- a/src/jup/commands/list.py +++ b/src/jup/commands/list.py @@ -7,7 +7,7 @@ from rich.text import Text from ..config import ( - get_all_agents, + get_all_harnesses, get_config, get_scope_dir, get_skills_lock, @@ -38,24 +38,24 @@ def list_skills( lock = get_skills_lock(config) # Determine targets (where skills should be installed) - all_agents = get_all_agents(config) - configured_agents = [] + all_harnesses = get_all_harnesses(config) + configured_harnesses = [] # 1. Default scope directory default_dir = get_scope_dir(config) - configured_agents.append(("default", default_dir)) + configured_harnesses.append((".agents", default_dir)) - # 2. Configured agents - for agent_name in config.agents: - if agent_name in all_agents: - agent = all_agents[agent_name] + # 2. Configured harnesses + for harness_name in config.harnesses: + if harness_name in all_harnesses: + harness = all_harnesses[harness_name] loc = ( - agent.local_location + harness.local_location if config.scope == "local" - else agent.global_location + else harness.global_location ) p = Path(loc).expanduser().resolve() - configured_agents.append((agent_name, p)) + configured_harnesses.append((harness_name, p)) # Filter sources sources_to_show = [] @@ -119,10 +119,10 @@ def format_location_path(p: Path) -> str: source_exists = skill_src_dir.exists() if skill_src_dir else False status = {} - for agent_name, agent_dir in configured_agents: - skill_path = agent_dir / skill_name + for harness_name, harness_dir in configured_harnesses: + skill_path = harness_dir / skill_name info = { - "path": format_location_path(agent_dir), + "path": format_location_path(harness_dir), "exists": False, "is_symlink": False, "is_broken": False, @@ -137,7 +137,7 @@ def format_location_path(p: Path) -> str: elif skill_path.exists(): info["exists"] = True - status[agent_name] = info + status[harness_name] = info installed_skills_data.append( { @@ -155,15 +155,19 @@ def format_location_path(p: Path) -> str: # Scan for unmanaged skills unmanaged_skills_data = [] - for agent_name, agent_dir in configured_agents: - if not agent_dir.exists(): + for harness_name, harness_dir in configured_harnesses: + if not harness_dir.exists(): continue - for item in agent_dir.iterdir(): + for item in harness_dir.iterdir(): if item.is_dir() and item.name not in managed_skill_names: # Check if it has a SKILL.md to be considered a skill if (item / "SKILL.md").exists(): unmanaged_skills_data.append( - {"name": item.name, "agent": agent_name, "path": rel_home(item)} + { + "name": item.name, + "harness": harness_name, + "path": rel_home(item), + } ) if as_json: @@ -187,17 +191,17 @@ def format_location_path(p: Path) -> str: for skill in installed_skills_data: status_lines = [] - for agent_name, info in skill["status"].items(): + for harness_name, info in skill["status"].items(): symbol = "๐Ÿ”—" if info["is_symlink"] else "๐Ÿ“" if info["is_broken"]: - status_lines.append(f"[red]โ›“๏ธโ€๐Ÿ’ฅ {agent_name} ({info['path']})[/red]") + status_lines.append(f"[red]โ›“๏ธโ€๐Ÿ’ฅ {harness_name} ({info['path']})[/red]") elif info["exists"]: status_lines.append( - f"[green]{symbol} {agent_name}[/green] [dim]({info['path']})[/dim]" + f"[green]{symbol} {harness_name}[/green] [dim]({info['path']})[/dim]" ) else: status_lines.append( - f"[red]{symbol} {agent_name} ({info['path']})[/red] [bold red]โŒ[/bold red]" + f"[red]{symbol} {harness_name} ({info['path']})[/red] [bold red]โŒ[/bold red]" ) status_str = "\n".join(status_lines) @@ -247,11 +251,11 @@ def format_location_path(p: Path) -> str: print("\n[yellow]Unmanaged Skills (not in lockfile):[/yellow]") un_table = Table() un_table.add_column("Skill Name", style="magenta") - un_table.add_column("Agent", style="cyan") + un_table.add_column("Harness", style="cyan") un_table.add_column("Path", style="green") for un in unmanaged_skills_data: - un_table.add_row(un["name"], un["agent"], un["path"]) + un_table.add_row(un["name"], un["harness"], un["path"]) print(un_table) # Render Tips diff --git a/src/jup/commands/remove.py b/src/jup/commands/remove.py index 8aad1dc..1a73d22 100644 --- a/src/jup/commands/remove.py +++ b/src/jup/commands/remove.py @@ -5,7 +5,7 @@ from rich import print from ..config import ( - get_all_agents, + get_all_harnesses, get_config, get_scope_dir, get_skills_lock, @@ -61,14 +61,14 @@ def remove_skill( scope_dir = get_scope_dir(config) default_skills_dir = scope_dir targets.append(default_skills_dir) - all_agents = get_all_agents(config) - for agent_name in config.agents: - if agent_name in all_agents: - agent = all_agents[agent_name] + all_harnesses = get_all_harnesses(config) + for harness_name in config.harnesses: + if harness_name in all_harnesses: + harness = all_harnesses[harness_name] loc = ( - agent.local_location + harness.local_location if config.scope == "local" - else agent.global_location + else harness.global_location ) targets.append(Path(loc).expanduser().resolve()) diff --git a/src/jup/commands/sync.py b/src/jup/commands/sync.py index e22cbbb..5bcb9d7 100644 --- a/src/jup/commands/sync.py +++ b/src/jup/commands/sync.py @@ -8,7 +8,7 @@ from rich import print from ..config import ( - get_all_agents, + get_all_harnesses, get_config, get_scope_dir, get_skills_lock, @@ -39,7 +39,7 @@ def sync_skills( ] = False, verbose: bool = False, ): - """Update all links/copies in default-lib and for other agents.""" + """Update all links/copies in default-lib and for other harnesses.""" verbose_state.verbose = verbose sync_logic(update=update, verbose=verbose, interactive=interactive) @@ -138,26 +138,32 @@ def sync_logic(update: bool = False, verbose: bool = False, interactive: bool = default_skills_dir = scope_dir targets.append(default_skills_dir) - # Agent directories - all_agents = get_all_agents(config) - for agent_name in config.agents: - if agent_name in all_agents: - agent = all_agents[agent_name] + # Harness directories + all_harnesses = get_all_harnesses(config) + for harness_name in config.harnesses: + if harness_name in all_harnesses: + harness = all_harnesses[harness_name] # Use global/local location based on scope loc = ( - agent.local_location + harness.local_location if config.scope == "local" - else agent.global_location + else harness.global_location ) targets.append(Path(loc).expanduser().resolve()) else: - print(f"[yellow]Warning: Unknown agent '{agent_name}'. Skipping.[/yellow]") + print( + f"[yellow]Warning: Unknown harness '{harness_name}'. Skipping.[/yellow]" + ) - # Identify directories of inactive agents to clean up + # Identify directories of inactive harnesses to clean up active_target_paths = {t.resolve() for t in targets} inactive_targets = [] - for agent_name, agent in all_agents.items(): - loc = agent.local_location if config.scope == "local" else agent.global_location + for harness_name, harness in all_harnesses.items(): + loc = ( + harness.local_location + if config.scope == "local" + else harness.global_location + ) p = Path(loc).expanduser().resolve() if p.exists() and p.resolve() not in active_target_paths: inactive_targets.append(p) @@ -169,13 +175,13 @@ def sync_logic(update: bool = False, verbose: bool = False, interactive: bool = # We only overwrite skills managed by our lockfile # to avoid blowing away user's manual skills. - # 1. Clean up managed skills from inactive agents and removed skills + # 1. Clean up managed skills from inactive harnesses and removed skills # Collect all skills that SHOULD exist all_managed_skills = set() for source in lock.sources.values(): all_managed_skills.update(source.skills) - # Clean inactive agents + # Clean inactive harnesses removed_from_inactive = 0 for it in inactive_targets: for skill in all_managed_skills: @@ -191,7 +197,7 @@ def sync_logic(update: bool = False, verbose: bool = False, interactive: bool = if removed_from_inactive > 0 and verbose_state.verbose: print( - f"๐Ÿงน Cleaned {removed_from_inactive} skills from {len(inactive_targets)} inactive agent directories." + f"๐Ÿงน Cleaned {removed_from_inactive} skills from {len(inactive_targets)} inactive harness directories." ) # 2. Process each skill source diff --git a/src/jup/commands/utils.py b/src/jup/commands/utils.py index 8d9d504..818a4ba 100644 --- a/src/jup/commands/utils.py +++ b/src/jup/commands/utils.py @@ -169,14 +169,14 @@ def run_git_pull(repo_dir: Path): raise -def is_path_in_agent_dir(path: Path, config) -> str | None: +def is_path_in_harness_dir(path: Path, config) -> str | None: resolved_path = path.resolve() - from ..config import get_all_agents + from ..config import get_all_harnesses - all_agents = get_all_agents(config) - for name, agent in all_agents.items(): - for loc in [agent.global_location, agent.local_location]: - agent_path = Path(loc).expanduser().resolve() - if resolved_path == agent_path or agent_path in resolved_path.parents: + all_harnesses = get_all_harnesses(config) + for name, harness in all_harnesses.items(): + for loc in [harness.global_location, harness.local_location]: + harness_path = Path(loc).expanduser().resolve() + if resolved_path == harness_path or harness_path in resolved_path.parents: return name return None diff --git a/src/jup/config.py b/src/jup/config.py index ac192d0..9f2b397 100644 --- a/src/jup/config.py +++ b/src/jup/config.py @@ -1,5 +1,5 @@ from pathlib import Path -from .models import DEFAULT_AGENTS, AgentConfig, JupConfig, SkillsLock +from .models import DEFAULT_HARNESSES, HarnessConfig, JupConfig, SkillsLock JUP_CONFIG_DIR = Path.home() / ".jup" CONFIG_FILE = JUP_CONFIG_DIR / "config.json" @@ -22,26 +22,28 @@ def save_config(config: JupConfig): f.write(config.model_dump_json(indent=4, by_alias=True)) -def get_all_agents(config: JupConfig) -> dict[str, AgentConfig]: - """Merge default agents with custom providers from config.""" - all_agents = DEFAULT_AGENTS.copy() - if config.custom_agents: - all_agents.update(config.custom_agents) - return all_agents +def get_all_harnesses(config: JupConfig) -> dict[str, HarnessConfig]: + """Merge default harnesses with custom providers from config.""" + all_harnesses = DEFAULT_HARNESSES.copy() + if config.custom_harnesses: + all_harnesses.update(config.custom_harnesses) + return all_harnesses -def get_scope_dir(config: JupConfig, agent_name: str | None = None) -> Path: +def get_scope_dir(config: JupConfig, harness_name: str | None = None) -> Path: """ - Return the skills directory for the given config and optional agent. + Return the skills directory for the given config and optional harness. """ - all_agents = get_all_agents(config) - agent_key = agent_name if agent_name and agent_name in all_agents else "default" - agent_config = all_agents[agent_key] + all_harnesses = get_all_harnesses(config) + harness_key = ( + harness_name if harness_name and harness_name in all_harnesses else ".agents" + ) + harness_config = all_harnesses[harness_key] if config.scope == "local": - return Path(agent_config.local_location).expanduser().resolve() + return Path(harness_config.local_location).expanduser().resolve() - return Path(agent_config.global_location).expanduser().resolve() + return Path(harness_config.global_location).expanduser().resolve() def get_skills_storage_dir() -> Path: diff --git a/src/jup/main.py b/src/jup/main.py index 14c4945..5f3150a 100644 --- a/src/jup/main.py +++ b/src/jup/main.py @@ -3,8 +3,8 @@ from importlib.metadata import version as get_version from enum import Enum from rich import print -from .config import get_all_agents, get_config, save_config -from .models import DEFAULT_AGENTS, AgentConfig, JupConfig, ScopeType, SyncMode +from .config import get_all_harnesses, get_config, save_config +from .models import DEFAULT_HARNESSES, HarnessConfig, JupConfig, ScopeType, SyncMode class VerboseState: @@ -94,11 +94,13 @@ def config_show(): @config_app.command("get", short_help="Get a config value", no_args_is_help=True) def config_get( - key: str = typer.Argument(..., help="Config key to get (scope, agents, sync-mode)"), + key: str = typer.Argument( + ..., help="Config key to get (scope, harnesses, sync-mode)" + ), ): config = get_config() # Normalize key - normalize_key_map = {"sync-mode": "sync_mode"} + normalize_key_map = {"sync-mode": "sync_mode", "agents": "harnesses"} norm_key = normalize_key_map.get(key, key) if norm_key in JupConfig.model_fields: @@ -119,7 +121,11 @@ def config_set( ): config = get_config() # Normalize key - key_map = {"sync-mode": "sync_mode", "sync_mode": "sync_mode"} + key_map = { + "sync-mode": "sync_mode", + "sync_mode": "sync_mode", + "agents": "harnesses", + } norm_key = key_map.get(key, key) if norm_key not in JupConfig.model_fields: print(f"[red]Unknown config key: {key}[/red]") @@ -128,8 +134,8 @@ def config_set( try: if norm_key == "scope": config.scope = ScopeType(value) - elif norm_key == "agents": - config.agents = ( + elif norm_key == "harnesses": + config.harnesses = ( [v.strip() for v in value.split(",")] if value.lower() != "none" else [] ) elif norm_key == "sync_mode": @@ -149,12 +155,16 @@ def config_set( ) def config_unset(key: str = typer.Argument(..., help="Config key to unset")): config = get_config() - key_map = {"sync-mode": "sync_mode", "sync_mode": "sync_mode"} + key_map = { + "sync-mode": "sync_mode", + "sync_mode": "sync_mode", + "agents": "harnesses", + } norm_key = key_map.get(key, key) if norm_key == "scope": config.scope = ScopeType.GLOBAL - elif norm_key == "agents": - config.agents = [] + elif norm_key == "harnesses": + config.harnesses = [] elif norm_key == "sync_mode": config.sync_mode = SyncMode.LINK else: @@ -164,32 +174,34 @@ def config_unset(key: str = typer.Argument(..., help="Config key to unset")): print(f"Unset {key} (reverted to default)") -agent_app = typer.Typer(help="Manage agent providers", no_args_is_help=True) -app.add_typer(agent_app, name="agent") +harness_app = typer.Typer(help="Manage harness providers", no_args_is_help=True) +app.add_typer(harness_app, name="harness") -@agent_app.command("list") -def agent_list(): - """List all available agent providers.""" +@harness_app.command("list") +def harness_list(): + """List all available harness providers.""" config = get_config() - all_agents = get_all_agents(config) + all_harnesses = get_all_harnesses(config) from rich.table import Table - table = Table(title="Agent Providers") + table = Table(title="Harness Providers") table.add_column("Name", style="magenta") table.add_column("Global Location", style="cyan") table.add_column("Local Location", style="cyan") table.add_column("Type", style="yellow") - for name, agent in all_agents.items(): - agent_type = "default" if name in DEFAULT_AGENTS else "custom" - table.add_row(name, agent.global_location, agent.local_location, agent_type) + for name, harness in all_harnesses.items(): + harness_type = "default" if name in DEFAULT_HARNESSES else "custom" + table.add_row( + name, harness.global_location, harness.local_location, harness_type + ) print(table) -@agent_app.command("add") -def agent_add( - name: str = typer.Argument(..., help="Agent name"), +@harness_app.command("add") +def harness_add( + name: str = typer.Argument(..., help="Harness name"), global_location: str = typer.Option( ..., "--global-location", @@ -205,24 +217,24 @@ def agent_add( prompt="Local skills directory (e.g. ./.gemini/skills)", ), ): - """Add a new custom agent provider.""" + """Add a new custom harness provider.""" config = get_config() - all_agents = get_all_agents(config) - if name in all_agents: - print(f"[red]Agent '{name}' already exists.[/red]") + all_harnesses = get_all_harnesses(config) + if name in all_harnesses: + print(f"[red]Harness '{name}' already exists.[/red]") raise typer.Exit(code=1) - agent = AgentConfig( + harness = HarnessConfig( name=name, global_location=global_location, local_location=local_location ) - config.custom_agents[name] = agent + config.custom_harnesses[name] = harness save_config(config) - print(f"Added custom agent provider: [magenta]{name}[/magenta]") + print(f"Added custom harness provider: [magenta]{name}[/magenta]") -@agent_app.command("edit") -def agent_edit( - name: str = typer.Argument(..., help="Agent name"), +@harness_app.command("edit") +def harness_edit( + name: str = typer.Argument(..., help="Harness name"), global_location: Optional[str] = typer.Option( None, "--global-location", "-g", help="Global skills directory" ), @@ -230,40 +242,40 @@ def agent_edit( None, "--local-location", "-l", help="Local skills directory" ), ): - """Edit an existing custom agent provider.""" + """Edit an existing custom harness provider.""" config = get_config() - if name in DEFAULT_AGENTS: - print(f"[red]Cannot edit default agent '{name}'.[/red]") + if name in DEFAULT_HARNESSES: + print(f"[red]Cannot edit default harness '{name}'.[/red]") raise typer.Exit(code=1) - if name not in config.custom_agents: - print(f"[red]Custom agent '{name}' does not exist.[/red]") + if name not in config.custom_harnesses: + print(f"[red]Custom harness '{name}' does not exist.[/red]") raise typer.Exit(code=1) - agent = config.custom_agents[name] + harness = config.custom_harnesses[name] if global_location is not None: - agent.global_location = global_location + harness.global_location = global_location if local_location is not None: - agent.local_location = local_location + harness.local_location = local_location - config.custom_agents[name] = agent + config.custom_harnesses[name] = harness save_config(config) - print(f"Updated custom agent provider: [magenta]{name}[/magenta]") + print(f"Updated custom harness provider: [magenta]{name}[/magenta]") -@agent_app.command("remove") -def agent_remove(name: str = typer.Argument(..., help="Agent name")): - """Remove a custom agent provider.""" +@harness_app.command("remove") +def harness_remove(name: str = typer.Argument(..., help="Harness name")): + """Remove a custom harness provider.""" config = get_config() - if name in DEFAULT_AGENTS: - print(f"[red]Cannot remove default agent '{name}'.[/red]") + if name in DEFAULT_HARNESSES: + print(f"[red]Cannot remove default harness '{name}'.[/red]") raise typer.Exit(code=1) - if name not in config.custom_agents: - print(f"[red]Custom agent '{name}' does not exist.[/red]") + if name not in config.custom_harnesses: + print(f"[red]Custom harness '{name}' does not exist.[/red]") raise typer.Exit(code=1) - del config.custom_agents[name] + del config.custom_harnesses[name] save_config(config) - print(f"Removed custom agent provider: [magenta]{name}[/magenta]") + print(f"Removed custom harness provider: [magenta]{name}[/magenta]") # Import command registrations after app and shared state are defined. diff --git a/src/jup/models.py b/src/jup/models.py index d96ba68..cf08acf 100644 --- a/src/jup/models.py +++ b/src/jup/models.py @@ -14,7 +14,7 @@ class ScopeType(StrEnum): LOCAL = "local" -class AgentConfig(BaseModel): +class HarnessConfig(BaseModel): name: str global_location: str local_location: str @@ -22,8 +22,8 @@ class AgentConfig(BaseModel): class JupConfig(BaseModel): scope: ScopeType = Field(default=ScopeType.GLOBAL) - agents: List[str] = Field(default_factory=list) - custom_agents: Dict[str, AgentConfig] = Field(default_factory=dict) + harnesses: List[str] = Field(default_factory=list) + custom_harnesses: Dict[str, HarnessConfig] = Field(default_factory=dict) sync_mode: SyncMode = Field(default=SyncMode.LINK, alias="sync-mode") model_config = {"populate_by_name": True} @@ -45,25 +45,30 @@ class SkillsLock(BaseModel): sources: Dict[str, SkillSource] = Field(default_factory=dict) -# Pre-defined registry of agents based on known paths, extensible later. -DEFAULT_AGENTS: Dict[str, AgentConfig] = { - "gemini": AgentConfig( +# Pre-defined registry of harnesses based on known paths, extensible later. +DEFAULT_HARNESSES: Dict[str, HarnessConfig] = { + "gemini": HarnessConfig( name="gemini", global_location="~/.gemini/skills", local_location="./.gemini/skills", ), - "copilot": AgentConfig( + "copilot": HarnessConfig( name="copilot", global_location="~/.copilot/skills", local_location="./.copilot/skills", ), - "claude": AgentConfig( + "claude": HarnessConfig( name="claude", global_location="~/.claude/skills", local_location="./.claude/skills", ), - "default": AgentConfig( - name="default", + "codex": HarnessConfig( + name="codex", + global_location="~/.agents/skills", + local_location="./.agents/skills", + ), + ".agents": HarnessConfig( + name=".agents", global_location="~/.agents/skills", local_location="./.agents/skills", ), diff --git a/tests/e2e/test_workflow.py b/tests/e2e/test_workflow.py index cb16085..c7bdc62 100644 --- a/tests/e2e/test_workflow.py +++ b/tests/e2e/test_workflow.py @@ -3,7 +3,7 @@ from typer.testing import CliRunner from unittest.mock import patch from jup.main import app -from jup.models import AgentConfig +from jup.models import HarnessConfig runner = CliRunner() @@ -13,9 +13,9 @@ def mock_env(tmp_path): jup_dir = tmp_path / ".jup" jup_dir.mkdir() - mock_agents = { - "default": AgentConfig( - name="default", + mock_harnesses = { + ".agents": HarnessConfig( + name=".agents", global_location=str(tmp_path / "global"), local_location=str(tmp_path / "local"), ) @@ -31,8 +31,8 @@ def mock_env(tmp_path): with patch("jup.config.JUP_CONFIG_DIR", jup_dir): with patch("jup.config.CONFIG_FILE", jup_dir / "config.json"): - with patch("jup.config.DEFAULT_AGENTS", mock_agents): - with patch("jup.models.DEFAULT_AGENTS", mock_agents): + with patch("jup.config.DEFAULT_HARNESSES", mock_harnesses): + with patch("jup.models.DEFAULT_HARNESSES", mock_harnesses): with patch("jup.commands.add.run_git_clone") as mock_clone: def side_effect(repo_url, dest_dir, **kwargs): diff --git a/tests/integration/test_add_unmanaged.py b/tests/integration/test_add_unmanaged.py index edca9b0..c0d3348 100644 --- a/tests/integration/test_add_unmanaged.py +++ b/tests/integration/test_add_unmanaged.py @@ -6,15 +6,15 @@ runner = CliRunner() -def test_add_detects_and_moves_agent_dir_source(tmp_path, monkeypatch): - # Setup mock home and agent dir +def test_add_detects_and_moves_harness_dir_source(tmp_path, monkeypatch): + # Setup mock home and harness dir fake_home = tmp_path / "home" fake_home.mkdir() - agent_dir = fake_home / ".agents" / "skills" - agent_dir.mkdir(parents=True) + harness_dir = fake_home / ".agents" / "skills" + harness_dir.mkdir(parents=True) - # Create a skill inside the agent dir - skill_dir = agent_dir / "unmanaged-skill" + # Create a skill inside the harness dir + skill_dir = harness_dir / "unmanaged-skill" skill_dir.mkdir() (skill_dir / "SKILL.md").write_text("info") @@ -22,7 +22,7 @@ def test_add_detects_and_moves_agent_dir_source(tmp_path, monkeypatch): # We also need to point JUP_CONFIG_DIR if it's based on Path.home() # In src/jup/config.py: JUP_CONFIG_DIR = Path.home() / ".jup" - # Mock agent location to our tmp path + # Mock harness location to our tmp path # We need to ensure the config uses our fake_home # Since jup.config.JUP_CONFIG_DIR is defined at module level, # we might need to monkeypatch it if it was already imported. @@ -30,13 +30,13 @@ def test_add_detects_and_moves_agent_dir_source(tmp_path, monkeypatch): monkeypatch.setattr(jup.config, "JUP_CONFIG_DIR", fake_home / ".jup") - # Set up config with the agent dir + # Set up config with the harness dir config = get_config() - from jup.models import AgentConfig + from jup.models import HarnessConfig - config.custom_agents["default"] = AgentConfig( - name="default", - global_location=str(agent_dir), + config.custom_harnesses[".agents"] = HarnessConfig( + name=".agents", + global_location=str(harness_dir), local_location="./.agents/skills", ) jup.config.save_config(config) @@ -47,7 +47,7 @@ def test_add_detects_and_moves_agent_dir_source(tmp_path, monkeypatch): result = runner.invoke(app, ["add", str(skill_dir)], input="y\n") assert result.exit_code == 0 - assert "Source is inside an agent directory" in result.stdout + assert "Source is inside a harness directory" in result.stdout assert "Moving to" in result.stdout # Verify it moved @@ -62,15 +62,15 @@ def test_add_detects_and_moves_agent_dir_source(tmp_path, monkeypatch): assert lock.sources[str(storage_dir)].source_type == LOCAL_SOURCE_TYPE -def test_add_detects_but_keeps_agent_dir_source(tmp_path, monkeypatch): - # Setup mock home and agent dir +def test_add_detects_but_keeps_harness_dir_source(tmp_path, monkeypatch): + # Setup mock home and harness dir fake_home = tmp_path / "home" fake_home.mkdir() - agent_dir = fake_home / ".agents" / "skills" - agent_dir.mkdir(parents=True) + harness_dir = fake_home / ".agents" / "skills" + harness_dir.mkdir(parents=True) - # Create a skill inside the agent dir - skill_dir = agent_dir / "unmanaged-skill" + # Create a skill inside the harness dir + skill_dir = harness_dir / "unmanaged-skill" skill_dir.mkdir() (skill_dir / "SKILL.md").write_text("info") @@ -80,11 +80,11 @@ def test_add_detects_but_keeps_agent_dir_source(tmp_path, monkeypatch): monkeypatch.setattr(jup.config, "JUP_CONFIG_DIR", fake_home / ".jup") config = get_config() - from jup.models import AgentConfig + from jup.models import HarnessConfig - config.custom_agents["default"] = AgentConfig( - name="default", - global_location=str(agent_dir), + config.custom_harnesses[".agents"] = HarnessConfig( + name=".agents", + global_location=str(harness_dir), local_location="./.agents/skills", ) jup.config.save_config(config) @@ -94,7 +94,7 @@ def test_add_detects_but_keeps_agent_dir_source(tmp_path, monkeypatch): result = runner.invoke(app, ["add", str(skill_dir)], input="n\n") assert result.exit_code == 0 - assert "Source is inside an agent directory" in result.stdout + assert "Source is inside a harness directory" in result.stdout # Verify it did NOT move assert skill_dir.exists() diff --git a/tests/integration/test_cli_commands.py b/tests/integration/test_cli_commands.py index f76948b..40892be 100644 --- a/tests/integration/test_cli_commands.py +++ b/tests/integration/test_cli_commands.py @@ -4,7 +4,7 @@ from typer.testing import CliRunner from unittest.mock import patch, MagicMock from jup.main import app -from jup.models import AgentConfig +from jup.models import HarnessConfig runner = CliRunner() @@ -15,9 +15,9 @@ def mock_jup_dir(tmp_path): jup_dir = tmp_path / ".jup" jup_dir.mkdir() - mock_agents = { - "default": AgentConfig( - name="default", + mock_harnesses = { + ".agents": HarnessConfig( + name=".agents", global_location=str(tmp_path / "global"), local_location=str(tmp_path / "local"), ) @@ -25,8 +25,8 @@ def mock_jup_dir(tmp_path): with patch("jup.config.JUP_CONFIG_DIR", jup_dir): with patch("jup.config.CONFIG_FILE", jup_dir / "config.json"): - with patch("jup.config.DEFAULT_AGENTS", mock_agents): - with patch("jup.models.DEFAULT_AGENTS", mock_agents): + with patch("jup.config.DEFAULT_HARNESSES", mock_harnesses): + with patch("jup.models.DEFAULT_HARNESSES", mock_harnesses): yield jup_dir diff --git a/tests/integration/test_cli_config.py b/tests/integration/test_cli_config.py index 7d2e774..e80fd26 100644 --- a/tests/integration/test_cli_config.py +++ b/tests/integration/test_cli_config.py @@ -2,7 +2,7 @@ from typer.testing import CliRunner from unittest.mock import patch from jup.main import app -from jup.models import AgentConfig +from jup.models import HarnessConfig runner = CliRunner() @@ -12,9 +12,9 @@ def mock_jup_dir(tmp_path): jup_dir = tmp_path / ".jup" jup_dir.mkdir() - mock_agents = { - "default": AgentConfig( - name="default", + mock_harnesses = { + ".agents": HarnessConfig( + name=".agents", global_location=str(tmp_path / "global"), local_location=str(tmp_path / "local"), ) @@ -22,8 +22,8 @@ def mock_jup_dir(tmp_path): with patch("jup.config.JUP_CONFIG_DIR", jup_dir): with patch("jup.config.CONFIG_FILE", jup_dir / "config.json"): - with patch("jup.config.DEFAULT_AGENTS", mock_agents): - with patch("jup.models.DEFAULT_AGENTS", mock_agents): + with patch("jup.config.DEFAULT_HARNESSES", mock_harnesses): + with patch("jup.models.DEFAULT_HARNESSES", mock_harnesses): yield jup_dir @@ -42,21 +42,21 @@ def test_config_set_scope(mock_jup_dir): assert result.stdout.strip() == "local" -def test_config_set_agents(mock_jup_dir): - result = runner.invoke(app, ["config", "set", "agents", "gemini,copilot"]) +def test_config_set_harnesses(mock_jup_dir): + result = runner.invoke(app, ["config", "set", "harnesses", "gemini,copilot"]) assert result.exit_code == 0 - assert "Set agents to gemini,copilot" in result.stdout + assert "Set harnesses to gemini,copilot" in result.stdout - result = runner.invoke(app, ["config", "get", "agents"]) + result = runner.invoke(app, ["config", "get", "harnesses"]) assert result.stdout.strip() == "gemini,copilot" -def test_config_unset_agents(mock_jup_dir): - runner.invoke(app, ["config", "set", "agents", "gemini"]) - result = runner.invoke(app, ["config", "unset", "agents"]) +def test_config_unset_harnesses(mock_jup_dir): + runner.invoke(app, ["config", "set", "harnesses", "gemini"]) + result = runner.invoke(app, ["config", "unset", "harnesses"]) assert result.exit_code == 0 - result = runner.invoke(app, ["config", "get", "agents"]) + result = runner.invoke(app, ["config", "get", "harnesses"]) assert result.stdout.strip() == "none" diff --git a/tests/integration/test_agent_commands.py b/tests/integration/test_harness_commands.py similarity index 55% rename from tests/integration/test_agent_commands.py rename to tests/integration/test_harness_commands.py index 895bded..44461f5 100644 --- a/tests/integration/test_agent_commands.py +++ b/tests/integration/test_harness_commands.py @@ -2,7 +2,7 @@ from typer.testing import CliRunner from unittest.mock import patch from jup.main import app -from jup.models import AgentConfig +from jup.models import HarnessConfig runner = CliRunner() @@ -12,9 +12,9 @@ def mock_jup_dir(tmp_path): jup_dir = tmp_path / ".jup" jup_dir.mkdir() - mock_agents = { - "default": AgentConfig( - name="default", + mock_harnesses = { + ".agents": HarnessConfig( + name=".agents", global_location=str(tmp_path / "global"), local_location=str(tmp_path / "local"), ) @@ -22,23 +22,22 @@ def mock_jup_dir(tmp_path): with patch("jup.config.JUP_CONFIG_DIR", jup_dir): with patch("jup.config.CONFIG_FILE", jup_dir / "config.json"): - with patch("jup.config.DEFAULT_AGENTS", mock_agents): - with patch("jup.models.DEFAULT_AGENTS", mock_agents): + with patch("jup.config.DEFAULT_HARNESSES", mock_harnesses): + with patch("jup.models.DEFAULT_HARNESSES", mock_harnesses): yield jup_dir -def test_agent_list_default(mock_jup_dir): - result = runner.invoke(app, ["agent", "list"]) +def test_harness_list_default(mock_jup_dir): + result = runner.invoke(app, ["harness", "list"]) assert result.exit_code == 0 - assert "default" in result.stdout - assert "default" in result.stdout + assert ".agents" in result.stdout -def test_agent_add_custom(mock_jup_dir): +def test_harness_add_custom(mock_jup_dir): result = runner.invoke( app, [ - "agent", + "harness", "add", "custom", "--global-location", @@ -48,18 +47,17 @@ def test_agent_add_custom(mock_jup_dir): ], ) assert result.exit_code == 0 - assert "Added custom agent provider: custom" in result.stdout + assert "Added custom harness provider: custom" in result.stdout - result = runner.invoke(app, ["agent", "list"]) - assert "custom" in result.stdout + result = runner.invoke(app, ["harness", "list"]) assert "custom" in result.stdout -def test_agent_add_duplicate(mock_jup_dir): +def test_harness_add_duplicate(mock_jup_dir): runner.invoke( app, [ - "agent", + "harness", "add", "custom", "--global-location", @@ -71,7 +69,7 @@ def test_agent_add_duplicate(mock_jup_dir): result = runner.invoke( app, [ - "agent", + "harness", "add", "custom", "--global-location", @@ -81,14 +79,14 @@ def test_agent_add_duplicate(mock_jup_dir): ], ) assert result.exit_code == 1 - assert "Agent 'custom' already exists." in result.stdout + assert "Harness 'custom' already exists." in result.stdout -def test_agent_edit_custom(mock_jup_dir): +def test_harness_edit_custom(mock_jup_dir): runner.invoke( app, [ - "agent", + "harness", "add", "custom", "--global-location", @@ -98,28 +96,28 @@ def test_agent_edit_custom(mock_jup_dir): ], ) result = runner.invoke( - app, ["agent", "edit", "custom", "--local-location", "/tmp/new-local"] + app, ["harness", "edit", "custom", "--local-location", "/tmp/new-local"] ) assert result.exit_code == 0 - assert "Updated custom agent provider: custom" in result.stdout + assert "Updated custom harness provider: custom" in result.stdout - result = runner.invoke(app, ["agent", "list"]) + result = runner.invoke(app, ["harness", "list"]) assert "/tmp/new-local" in result.stdout -def test_agent_edit_default_fails(mock_jup_dir): +def test_harness_edit_default_fails(mock_jup_dir): result = runner.invoke( - app, ["agent", "edit", "default", "--local-location", "/tmp/new-local"] + app, ["harness", "edit", ".agents", "--local-location", "/tmp/new-local"] ) assert result.exit_code == 1 - assert "Cannot edit default agent 'default'." in result.stdout + assert "Cannot edit default harness '.agents'." in result.stdout -def test_agent_remove_custom(mock_jup_dir): +def test_harness_remove_custom(mock_jup_dir): runner.invoke( app, [ - "agent", + "harness", "add", "custom", "--global-location", @@ -128,15 +126,15 @@ def test_agent_remove_custom(mock_jup_dir): "/tmp/local", ], ) - result = runner.invoke(app, ["agent", "remove", "custom"]) + result = runner.invoke(app, ["harness", "remove", "custom"]) assert result.exit_code == 0 - assert "Removed custom agent provider: custom" in result.stdout + assert "Removed custom harness provider: custom" in result.stdout - result = runner.invoke(app, ["agent", "list"]) + result = runner.invoke(app, ["harness", "list"]) assert "custom" not in result.stdout -def test_agent_remove_default_fails(mock_jup_dir): - result = runner.invoke(app, ["agent", "remove", "default"]) +def test_harness_remove_default_fails(mock_jup_dir): + result = runner.invoke(app, ["harness", "remove", ".agents"]) assert result.exit_code == 1 - assert "Cannot remove default agent 'default'." in result.stdout + assert "Cannot remove default harness '.agents'." in result.stdout diff --git a/tests/integration/test_sync_safety.py b/tests/integration/test_sync_safety.py index ad5e4cc..a3caf86 100644 --- a/tests/integration/test_sync_safety.py +++ b/tests/integration/test_sync_safety.py @@ -4,27 +4,27 @@ from jup.commands.utils import LOCAL_SOURCE_TYPE -def test_sync_does_not_delete_source_when_in_agent_dir(tmp_path, monkeypatch): - # Setup mock home and agent dir +def test_sync_does_not_delete_source_when_in_harness_dir(tmp_path, monkeypatch): + # Setup mock home and harness dir fake_home = tmp_path / "home" - agent_dir = fake_home / ".agents" / "skills" - agent_dir.mkdir(parents=True) + harness_dir = fake_home / ".agents" / "skills" + harness_dir.mkdir(parents=True) - # Create a skill inside the agent dir (the "unmanaged" scenario) - skill_dir = agent_dir / "my-skill" + # Create a skill inside the harness dir (the "unmanaged" scenario) + skill_dir = harness_dir / "my-skill" skill_dir.mkdir() (skill_dir / "SKILL.md").write_text("info") monkeypatch.setenv("HOME", str(fake_home)) config = get_config() - config.agents = ["default"] - # Mock agent location to our tmp path - from jup.models import AgentConfig + config.harnesses = [".agents"] + # Mock harness location to our tmp path + from jup.models import HarnessConfig - config.custom_agents["default"] = AgentConfig( - name="default", - global_location=str(agent_dir), + config.custom_harnesses[".agents"] = HarnessConfig( + name=".agents", + global_location=str(harness_dir), local_location="./.agents/skills", ) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 3c660ac..32a934d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,6 +1,6 @@ import pytest from unittest.mock import patch -from jup.models import JupConfig, SkillsLock, ScopeType, SyncMode, AgentConfig +from jup.models import JupConfig, SkillsLock, ScopeType, SyncMode, HarnessConfig from jup.config import ( get_config, save_config, @@ -17,10 +17,10 @@ def mock_jup_dir(tmp_path): jup_dir = tmp_path / ".jup" jup_dir.mkdir() - # Mock agents to use tmp_path - mock_agents = { - "default": AgentConfig( - name="default", + # Mock harnesses to use tmp_path + mock_harnesses = { + ".agents": HarnessConfig( + name=".agents", global_location=str(tmp_path / "global"), local_location=str(tmp_path / "local"), ) @@ -28,8 +28,8 @@ def mock_jup_dir(tmp_path): with patch("jup.config.JUP_CONFIG_DIR", jup_dir): with patch("jup.config.CONFIG_FILE", jup_dir / "config.json"): - with patch("jup.config.DEFAULT_AGENTS", mock_agents): - with patch("jup.models.DEFAULT_AGENTS", mock_agents): + with patch("jup.config.DEFAULT_HARNESSES", mock_harnesses): + with patch("jup.models.DEFAULT_HARNESSES", mock_harnesses): yield jup_dir @@ -41,13 +41,13 @@ def test_get_config_default(mock_jup_dir): def test_save_and_get_config(mock_jup_dir): config = JupConfig( - scope=ScopeType.LOCAL, agents=["gemini"], sync_mode=SyncMode.COPY + scope=ScopeType.LOCAL, harnesses=["gemini"], sync_mode=SyncMode.COPY ) save_config(config) loaded_config = get_config() assert loaded_config.scope == ScopeType.LOCAL - assert loaded_config.agents == ["gemini"] + assert loaded_config.harnesses == ["gemini"] assert loaded_config.sync_mode == SyncMode.COPY diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 67f38f8..e2ef661 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -2,7 +2,7 @@ JupConfig, SkillSource, SkillsLock, - AgentConfig, + HarnessConfig, SyncMode, ScopeType, ) @@ -11,7 +11,7 @@ def test_jup_config_defaults(): config = JupConfig() assert config.scope == ScopeType.GLOBAL - assert config.agents == [] + assert config.harnesses == [] assert config.sync_mode == SyncMode.LINK @@ -38,10 +38,10 @@ def test_skills_lock(): assert lock.sources["owner/repo"].repo == "owner/repo" -def test_agent_config(): - agent = AgentConfig( +def test_harness_config(): + harness = HarnessConfig( name="test", global_location="~/global", local_location="./local" ) - assert agent.name == "test" - assert agent.global_location == "~/global" - assert agent.local_location == "./local" + assert harness.name == "test" + assert harness.global_location == "~/global" + assert harness.local_location == "./local" diff --git a/uv.lock b/uv.lock index d39249b..98f23dc 100644 --- a/uv.lock +++ b/uv.lock @@ -328,7 +328,7 @@ wheels = [ [[package]] name = "jup" -version = "0.12.0b1" +version = "0.12.0b2" source = { editable = "." } dependencies = [ { name = "prompt-toolkit" }, From b5aa5a6ac0736deedb9b67ee1b1111ca3c9156d7 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Mon, 20 Apr 2026 21:57:37 +0000 Subject: [PATCH 2/6] 0.12.0-beta.4 Automatically generated by python-semantic-release --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd0846..93e410b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # CHANGELOG +## v0.12.0-beta.4 (2026-04-20) + +### Features + +- Rename "agent" to "harness" and ".agents" terminology cleanup + ([`9d60b06`](https://github.com/andrader/jup/commit/9d60b06d0d1928175b4f340b8cb47a1adff1e364)) + + ## v0.12.0-beta.3 (2026-04-20) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index 7120e0f..8032527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jup" -version = "0.12.0-beta.3" +version = "0.12.0-beta.4" description = "jup - Agent Skills Manager" readme = "README.md" requires-python = ">=3.12" From 6c9b8425ed9f8676ed31e2409715cf12662e2f2e Mon Sep 17 00:00:00 2001 From: Rubens Andrade <38709237+andrader@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:19:13 -0300 Subject: [PATCH 3/6] chore: bump version to 0.13.0-beta.1 and sync with stable --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8032527..d2e9694 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jup" -version = "0.12.0-beta.4" +version = "0.13.0-beta.1" description = "jup - Agent Skills Manager" readme = "README.md" requires-python = ">=3.12" From b0f4d9b8a659d13902f7542df1a1c24a593c2576 Mon Sep 17 00:00:00 2001 From: Rubens Andrade <38709237+andrader@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:30:24 -0300 Subject: [PATCH 4/6] feat: explicitly mention codex support in harness registry --- src/jup/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/jup/models.py b/src/jup/models.py index cf08acf..9def442 100644 --- a/src/jup/models.py +++ b/src/jup/models.py @@ -46,6 +46,7 @@ class SkillsLock(BaseModel): # Pre-defined registry of harnesses based on known paths, extensible later. +# Includes support for Gemini, Copilot, Claude, and Codex. DEFAULT_HARNESSES: Dict[str, HarnessConfig] = { "gemini": HarnessConfig( name="gemini", From 91dde586e17ac00f304ec61e1afa8001d89da31e Mon Sep 17 00:00:00 2001 From: semantic-release Date: Mon, 20 Apr 2026 22:30:45 +0000 Subject: [PATCH 5/6] 0.13.0-beta.2 Automatically generated by python-semantic-release --- CHANGELOG.md | 16 ++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e410b..7740671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # CHANGELOG +## v0.13.0-beta.2 (2026-04-20) + +### Features + +- Explicitly mention codex support in harness registry + ([`b0f4d9b`](https://github.com/andrader/jup/commit/b0f4d9b8a659d13902f7542df1a1c24a593c2576)) + + +## v0.13.0-beta.1 (2026-04-20) + +### Chores + +- Bump version to 0.13.0-beta.1 and sync with stable + ([`6c9b842`](https://github.com/andrader/jup/commit/6c9b8425ed9f8676ed31e2409715cf12662e2f2e)) + + ## v0.12.0-beta.4 (2026-04-20) ### Features diff --git a/pyproject.toml b/pyproject.toml index d2e9694..cc9240e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jup" -version = "0.13.0-beta.1" +version = "0.13.0-beta.2" description = "jup - Agent Skills Manager" readme = "README.md" requires-python = ">=3.12" From 8d2f64e30e18ebe1008d8b85ebfa76a8e33589d7 Mon Sep 17 00:00:00 2001 From: Rubens Andrade <38709237+andrader@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:08:09 -0300 Subject: [PATCH 6/6] ci: automate branch promotion and back-sync between main and release --- .github/workflows/promote-to-release.yml | 27 ++++++++++++ .github/workflows/release.yml | 10 +++++ .github/workflows/weekly-release-pr.yml | 52 ------------------------ uv.lock | 2 +- 4 files changed, 38 insertions(+), 53 deletions(-) create mode 100644 .github/workflows/promote-to-release.yml delete mode 100644 .github/workflows/weekly-release-pr.yml diff --git a/.github/workflows/promote-to-release.yml b/.github/workflows/promote-to-release.yml new file mode 100644 index 0000000..e36c0c2 --- /dev/null +++ b/.github/workflows/promote-to-release.yml @@ -0,0 +1,27 @@ +name: Promote to Release + +on: + schedule: + # Runs every Friday at 16:00 UTC + - cron: '0 16 * * 5' + workflow_dispatch: # Allow manual trigger + +jobs: + promote: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout release branch + uses: actions/checkout@v6 + with: + ref: release + fetch-depth: 0 + + - name: Fast-forward merge main into release + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git fetch origin main + git merge origin/main --no-edit + git push origin release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 981445a..7c685d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,3 +67,13 @@ jobs: git tag -f stable "$VERSION" git push -f origin stable fi + + - name: Back-sync release to main + if: steps.release.outputs.released == 'true' && github.ref_name == 'release' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git fetch origin main + git checkout main + git merge origin/release --no-edit + git push origin main diff --git a/.github/workflows/weekly-release-pr.yml b/.github/workflows/weekly-release-pr.yml deleted file mode 100644 index a654d8a..0000000 --- a/.github/workflows/weekly-release-pr.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Weekly Release PR - -on: - schedule: - # Runs every Friday at 16:00 UTC - - cron: '0 16 * * 5' - workflow_dispatch: # Allow manual trigger - -jobs: - create-pr: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repo - uses: actions/checkout@v6 - - - name: Create PR from main to release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Ensure labels exist - gh label create "release" --color "0E8A16" --description "Stable release promotion" || true - gh label create "automated-pr" --color "FBCA04" --description "Automatically generated PR" || true - - # Check if PR already exists - PR_EXISTS=$(gh pr list --base release --head main --state open --json number --jq '.[0].number') - - if [ -n "$PR_EXISTS" ]; then - echo "PR already exists: #$PR_EXISTS" - exit 0 - fi - - BODY=$(cat < release" \ - --body "$BODY" \ - --label "release" \ - --label "automated-pr" diff --git a/uv.lock b/uv.lock index 98f23dc..8d1a401 100644 --- a/uv.lock +++ b/uv.lock @@ -328,7 +328,7 @@ wheels = [ [[package]] name = "jup" -version = "0.12.0b2" +version = "0.13.0b1" source = { editable = "." } dependencies = [ { name = "prompt-toolkit" },