-
Notifications
You must be signed in to change notification settings - Fork 0
Add CLI commands to list and clone jobs from repositories #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: ncrmro <8276365+ncrmro@users.noreply.github.com>
Co-authored-by: ncrmro <8276365+ncrmro@users.noreply.github.com>
Co-authored-by: ncrmro <8276365+ncrmro@users.noreply.github.com>
|
@copilot this should look for jobs in that repo in the .deepwork folder eg jobs inside of that repo or path to repo. |
Co-authored-by: ncrmro <8276365+ncrmro@users.noreply.github.com>
... Fixed in commit 4410128. The tool now checks for jobs in the following priority order:
This means |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds job management capabilities to the DeepWork CLI, enabling users to discover and import jobs from multiple sources including the default library, local filesystem paths, and GitHub repositories.
Changes:
- Adds
deepwork jobs listcommand to display available jobs with name, version, and summary information - Adds
deepwork jobs clonecommand to copy jobs into a project's.deepwork/jobs/directory with validation and automatic skill synchronization - Implements flexible source resolution with support for default library, local paths, and GitHub URLs with automatic repository cloning
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/deepwork/cli/jobs.py | New file implementing the jobs command group with list and clone subcommands, source resolution, job discovery, and GitHub repository cloning |
| src/deepwork/cli/main.py | Registers the new jobs command group with the CLI |
| tests/integration/test_jobs_command.py | Comprehensive integration tests covering list/clone operations for default library, local paths, error cases, and edge conditions |
| console.print(f"[yellow]→[/yellow] Cloning repository from {source}...") | ||
| tmp_dir = Path(tempfile.mkdtemp(prefix="deepwork_jobs_")) | ||
| try: | ||
| from git import GitCommandError |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitCommandError is imported locally inside the try block, while Repo is imported at the module level (line 9). This is inconsistent and unnecessary. Both imports come from the same git package and should be imported together at the module level for consistency.
Move the GitCommandError import to line 9 to match the existing import style.
| if _is_github_url(source): | ||
| # Clone to temporary directory | ||
| console.print(f"[yellow]→[/yellow] Cloning repository from {source}...") | ||
| tmp_dir = Path(tempfile.mkdtemp(prefix="deepwork_jobs_")) | ||
| try: | ||
| from git import GitCommandError | ||
|
|
||
| Repo.clone_from(source, tmp_dir, depth=1) | ||
| console.print(" [green]✓[/green] Repository cloned") | ||
|
|
||
| # Look for jobs in standard locations (in order of priority) | ||
| if (tmp_dir / ".deepwork" / "jobs").exists(): | ||
| return tmp_dir / ".deepwork" / "jobs" | ||
|
|
||
| if (tmp_dir / "library" / "jobs").exists(): | ||
| return tmp_dir / "library" / "jobs" | ||
|
|
||
| # Look for a jobs directory at the root | ||
| if (tmp_dir / "jobs").exists(): | ||
| return tmp_dir / "jobs" | ||
|
|
||
| # Clean up if jobs not found | ||
| shutil.rmtree(tmp_dir, ignore_errors=True) | ||
| raise JobsError( | ||
| "Could not find jobs in cloned repository. " | ||
| "Expected '.deepwork/jobs', 'library/jobs', or 'jobs' directory." | ||
| ) | ||
| except GitCommandError as e: | ||
| shutil.rmtree(tmp_dir, ignore_errors=True) | ||
| raise JobsError(f"Failed to clone repository: {e}") from e | ||
| except JobsError: | ||
| # Re-raise our own errors | ||
| raise | ||
| except Exception as e: | ||
| # Cleanup and wrap any other unexpected errors | ||
| shutil.rmtree(tmp_dir, ignore_errors=True) | ||
| raise JobsError(f"Unexpected error accessing repository: {e}") from e |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The temporary directory created for cloned repositories is never cleaned up after use. When cloning from a GitHub URL, a temp directory is created and a path inside it is returned. This temp directory persists on disk even after the command completes, leading to a resource leak.
The issue affects both the list and clone commands when using GitHub URLs. The temp directories will accumulate in the system's temp folder over time.
Consider using a context manager or storing the temp directory reference so it can be cleaned up after the command completes. One approach would be to use tempfile.TemporaryDirectory() as a context manager at the command level, or implement a cleanup mechanism that tracks and removes the temp directory after the jobs have been copied.
| """Integration tests for the jobs command.""" | ||
|
|
||
| import shutil | ||
| from pathlib import Path | ||
|
|
||
| from click.testing import CliRunner | ||
|
|
||
| from deepwork.cli.main import cli | ||
|
|
||
|
|
||
| class TestJobsCommand: | ||
| """Integration tests for 'deepwork jobs' command.""" | ||
|
|
||
| def test_jobs_list_default(self) -> None: | ||
| """Test listing jobs from default library.""" | ||
| runner = CliRunner() | ||
|
|
||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "list"], | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 0 | ||
| assert "Available Jobs" in result.output | ||
| assert "deepwork library/jobs (default)" in result.output | ||
| # The commit job should always be in the library | ||
| assert "commit" in result.output | ||
| assert "To clone a job" in result.output | ||
|
|
||
| def test_jobs_list_local_path(self, tmp_path: Path) -> None: | ||
| """Test listing jobs from a local path.""" | ||
| runner = CliRunner() | ||
|
|
||
| # Create a test jobs directory | ||
| jobs_dir = tmp_path / "test_repo" / "library" / "jobs" | ||
| jobs_dir.mkdir(parents=True) | ||
|
|
||
| # Create a test job | ||
| test_job_dir = jobs_dir / "test_job" | ||
| test_job_dir.mkdir() | ||
| (test_job_dir / "job.yml").write_text( | ||
| """name: test_job | ||
| version: "1.0.0" | ||
| summary: "A test job for integration testing" | ||
| description: | | ||
| This is a test job. | ||
|
|
||
| steps: | ||
| - id: test_step | ||
| name: "Test Step" | ||
| description: "A test step" | ||
| instructions_file: steps/test.md | ||
| inputs: [] | ||
| outputs: [] | ||
| dependencies: [] | ||
| quality_criteria: | ||
| - "Test passed" | ||
| """ | ||
| ) | ||
|
|
||
| # Create steps directory | ||
| (test_job_dir / "steps").mkdir() | ||
| (test_job_dir / "steps" / "test.md").write_text("# Test Step\n\nTest instructions.") | ||
|
|
||
| # List jobs from local path | ||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "list", str(tmp_path / "test_repo")], | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 0 | ||
| assert "Available Jobs" in result.output | ||
| assert "test_job" in result.output | ||
| assert "1.0.0" in result.output | ||
| assert "A test job for integration testing" in result.output | ||
|
|
||
| def test_jobs_list_from_deepwork_jobs_directory(self, tmp_path: Path) -> None: | ||
| """Test listing jobs from a .deepwork/jobs directory.""" | ||
| runner = CliRunner() | ||
|
|
||
| # Create a test .deepwork/jobs directory | ||
| jobs_dir = tmp_path / "test_repo" / ".deepwork" / "jobs" | ||
| jobs_dir.mkdir(parents=True) | ||
|
|
||
| # Create a test job | ||
| test_job_dir = jobs_dir / "deepwork_test_job" | ||
| test_job_dir.mkdir() | ||
| (test_job_dir / "job.yml").write_text( | ||
| """name: deepwork_test_job | ||
| version: "2.0.0" | ||
| summary: "A test job from .deepwork/jobs" | ||
| description: | | ||
| This is a test job from .deepwork/jobs. | ||
|
|
||
| steps: | ||
| - id: test_step | ||
| name: "Test Step" | ||
| description: "A test step" | ||
| instructions_file: steps/test.md | ||
| inputs: [] | ||
| outputs: [] | ||
| dependencies: [] | ||
| quality_criteria: | ||
| - "Test passed" | ||
| """ | ||
| ) | ||
|
|
||
| # Create steps directory | ||
| (test_job_dir / "steps").mkdir() | ||
| (test_job_dir / "steps" / "test.md").write_text("# Test Step\n\nTest instructions.") | ||
|
|
||
| # List jobs from local path | ||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "list", str(tmp_path / "test_repo")], | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 0 | ||
| assert "Available Jobs" in result.output | ||
| assert "deepwork_test_job" in result.output | ||
| assert "2.0.0" in result.output | ||
| assert "A test job from .deepwork/jobs" in result.output | ||
|
|
||
| def test_jobs_list_nonexistent_path(self) -> None: | ||
| """Test listing jobs from a nonexistent path.""" | ||
| runner = CliRunner() | ||
|
|
||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "list", "/nonexistent/path"], | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 1 | ||
| assert "Error:" in result.output | ||
| assert "Source not found" in result.output | ||
|
|
||
| def test_jobs_clone_from_default(self, mock_claude_project: Path) -> None: | ||
| """Test cloning a job from the default library.""" | ||
| runner = CliRunner() | ||
|
|
||
| # Install DeepWork first | ||
| result = runner.invoke( | ||
| cli, | ||
| ["install", "--platform", "claude", "--path", str(mock_claude_project)], | ||
| catch_exceptions=False, | ||
| ) | ||
| assert result.exit_code == 0 | ||
|
|
||
| # Clone the commit job | ||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "clone", "commit", "--path", str(mock_claude_project)], | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 0 | ||
| assert "Cloning Job" in result.output | ||
| assert "Using deepwork library/jobs (default)" in result.output | ||
| assert "Found job 'commit'" in result.output | ||
| assert "Validating job definition" in result.output | ||
| assert "commit v1.0.0" in result.output | ||
| assert "Copying job to project" in result.output | ||
| assert "Job 'commit' cloned successfully" in result.output | ||
|
|
||
| # Verify job was copied | ||
| cloned_job_path = mock_claude_project / ".deepwork" / "jobs" / "commit" | ||
| assert cloned_job_path.exists() | ||
| assert (cloned_job_path / "job.yml").exists() | ||
| assert (cloned_job_path / "steps").exists() | ||
|
|
||
| def test_jobs_clone_from_local_path(self, mock_claude_project: Path, tmp_path: Path) -> None: | ||
| """Test cloning a job from a local path.""" | ||
| runner = CliRunner() | ||
|
|
||
| # Install DeepWork first | ||
| result = runner.invoke( | ||
| cli, | ||
| ["install", "--platform", "claude", "--path", str(mock_claude_project)], | ||
| catch_exceptions=False, | ||
| ) | ||
| assert result.exit_code == 0 | ||
|
|
||
| # Create a test jobs directory | ||
| jobs_dir = tmp_path / "test_repo" / "library" / "jobs" | ||
| jobs_dir.mkdir(parents=True) | ||
|
|
||
| # Create a test job | ||
| test_job_dir = jobs_dir / "my_custom_job" | ||
| test_job_dir.mkdir() | ||
| (test_job_dir / "job.yml").write_text( | ||
| """name: my_custom_job | ||
| version: "2.0.0" | ||
| summary: "A custom test job" | ||
| description: | | ||
| This is a custom test job. | ||
|
|
||
| steps: | ||
| - id: custom_step | ||
| name: "Custom Step" | ||
| description: "A custom step" | ||
| instructions_file: steps/custom.md | ||
| inputs: [] | ||
| outputs: [] | ||
| dependencies: [] | ||
| quality_criteria: | ||
| - "Custom test passed" | ||
| """ | ||
| ) | ||
|
|
||
| # Create steps directory | ||
| (test_job_dir / "steps").mkdir() | ||
| (test_job_dir / "steps" / "custom.md").write_text("# Custom Step\n\nCustom instructions.") | ||
|
|
||
| # Clone the job from local path | ||
| result = runner.invoke( | ||
| cli, | ||
| [ | ||
| "jobs", | ||
| "clone", | ||
| "my_custom_job", | ||
| str(tmp_path / "test_repo"), | ||
| "--path", | ||
| str(mock_claude_project), | ||
| ], | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 0 | ||
| assert "Cloning Job" in result.output | ||
| assert "Found job 'my_custom_job'" in result.output | ||
| assert "my_custom_job v2.0.0" in result.output | ||
| assert "Job 'my_custom_job' cloned successfully" in result.output | ||
|
|
||
| # Verify job was copied | ||
| cloned_job_path = mock_claude_project / ".deepwork" / "jobs" / "my_custom_job" | ||
| assert cloned_job_path.exists() | ||
| assert (cloned_job_path / "job.yml").exists() | ||
| assert (cloned_job_path / "steps").exists() | ||
| assert (cloned_job_path / "steps" / "custom.md").exists() | ||
|
|
||
| def test_jobs_clone_nonexistent_job(self, mock_claude_project: Path) -> None: | ||
| """Test cloning a nonexistent job.""" | ||
| runner = CliRunner() | ||
|
|
||
| # Install DeepWork first | ||
| result = runner.invoke( | ||
| cli, | ||
| ["install", "--platform", "claude", "--path", str(mock_claude_project)], | ||
| catch_exceptions=False, | ||
| ) | ||
| assert result.exit_code == 0 | ||
|
|
||
| # Try to clone a nonexistent job | ||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "clone", "nonexistent_job", "--path", str(mock_claude_project)], | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 1 | ||
| assert "Error:" in result.output | ||
| assert "not found in source" in result.output | ||
|
|
||
| def test_jobs_clone_overwrite_existing(self, mock_claude_project: Path) -> None: | ||
| """Test cloning a job that already exists.""" | ||
| runner = CliRunner() | ||
|
|
||
| # Install DeepWork first | ||
| result = runner.invoke( | ||
| cli, | ||
| ["install", "--platform", "claude", "--path", str(mock_claude_project)], | ||
| catch_exceptions=False, | ||
| ) | ||
| assert result.exit_code == 0 | ||
|
|
||
| # Clone the commit job first time | ||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "clone", "commit", "--path", str(mock_claude_project)], | ||
| catch_exceptions=False, | ||
| ) | ||
| assert result.exit_code == 0 | ||
|
|
||
| # Try to clone again (without confirming overwrite) | ||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "clone", "commit", "--path", str(mock_claude_project)], | ||
| input="n\n", # Say no to overwrite | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 0 | ||
| assert "already exists" in result.output | ||
| assert "Clone cancelled" in result.output | ||
|
|
||
| def test_jobs_clone_without_deepwork_installed(self, tmp_path: Path) -> None: | ||
| """Test cloning a job without DeepWork installed.""" | ||
| runner = CliRunner() | ||
|
|
||
| # Create an empty project directory without DeepWork | ||
| project_dir = tmp_path / "empty_project" | ||
| project_dir.mkdir() | ||
|
|
||
| # Try to clone a job | ||
| result = runner.invoke( | ||
| cli, | ||
| ["jobs", "clone", "commit", "--path", str(project_dir)], | ||
| catch_exceptions=False, | ||
| ) | ||
|
|
||
| assert result.exit_code == 1 | ||
| assert "Error:" in result.output | ||
| assert "DeepWork not initialized" in result.output |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test suite is missing coverage for GitHub URL functionality. There are no tests that exercise the GitHub URL cloning path, which means the following code paths are untested:
- Valid GitHub URLs (github.com and *.github.com)
- Invalid GitHub URLs that fail _is_github_url validation
- GitHub clone failures (GitCommandError handling)
- Repository cloning with missing jobs directories
- Job discovery priority order in cloned repos (.deepwork/jobs > library/jobs > jobs)
Since other test files in the repository include comprehensive test coverage, tests should be added for these GitHub URL scenarios to match the testing standards of the codebase.
| if (source_path / ".deepwork" / "jobs").exists(): | ||
| return source_path / ".deepwork" / "jobs" | ||
| if (source_path / "library" / "jobs").exists(): | ||
| return source_path / "library" / "jobs" |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent job discovery behavior between local paths and GitHub URLs. When a local path is provided, the code checks for .deepwork/jobs and library/jobs, but if neither exists, it assumes the provided path itself is the jobs directory (line 96). However, for GitHub URLs (lines 111-119), the code additionally checks for a jobs directory at the repository root before falling back to an error.
This means if a repository has jobs in a jobs/ directory at the root (rather than .deepwork/jobs or library/jobs), it will be discovered when cloning from GitHub but not when pointing to a local clone of that same repository.
For consistency, consider adding a check for source_path / "jobs" in the local path handling (around line 94-95) before falling back to returning the path itself.
| return source_path / "library" / "jobs" | |
| return source_path / "library" / "jobs" | |
| if (source_path / "jobs").exists(): | |
| return source_path / "jobs" |
| if library_path.exists(): | ||
| return library_path | ||
|
|
||
| # Fallback: try to clone from GitHub |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Misleading comment: The comment states "Fallback: try to clone from GitHub" but the code immediately raises a JobsError without attempting any GitHub cloning. The comment should be removed or updated to accurately reflect that this is an error path when the local library cannot be found.
| # Fallback: try to clone from GitHub | |
| # If not found, require the caller to provide an explicit source path or URL |
| @@ -0,0 +1,317 @@ | |||
| """Integration tests for the jobs command.""" | |||
|
|
|||
| import shutil | |||
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'shutil' is not used.
| import shutil |
Adds
deepwork jobscommand group enabling discovery and import of jobs from multiple sources: the default library, local filesystem paths, or GitHub repositories.Commands
deepwork jobs list [SOURCE]Lists available jobs with name, version, and summary. Sources:
library/jobsin deepwork package/path/to/repoor/path/to/jobshttps://github.com/owner/repoJob Discovery Priority:
When pointing to a repository, the tool searches for jobs in the following order:
.deepwork/jobs- Jobs in deepwork-enabled repositorieslibrary/jobs- Library jobs (e.g., in the deepwork repo itself)jobs- Generic jobs directory at the rootdeepwork jobs clone JOB_NAME [SOURCE]Copies job to
.deepwork/jobs/with validation and automatic skill sync. Prompts before overwriting existing jobs.Implementation
New file:
src/deepwork/cli/jobs.py_resolve_source(): Resolves source to local path, cloning GitHub repos to temp directories with proper cleanup. Checks for jobs in.deepwork/jobs,library/jobs, orjobsdirectories in priority order._discover_jobs(): Discovers job directories containingjob.ymllist_jobs(): Renders job table using richclone(): Copies job structure, fixes permissions, runsdeepwork syncError handling:
ParseError,SyncError,GitCommandError)github.comor*.github.com)Example Usage
Note: The
spec_driven_developmentjob in library has a schema validation error (useshiddenfield which is not allowed). This is a pre-existing issue unrelated to these changes.Original prompt
Demo
Listing jobs
Cloning a Job
Git diff after cloniing a job