diff --git a/.claude/settings.json b/.claude/settings.json index cf4e3c4c..cd45655b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -130,7 +130,13 @@ "Bash(deepwork:*)", "Bash(.claude/hooks/commit_job_git_commit.sh:*)", "Bash(./.deepwork/jobs/deepwork_jobs/make_new_job.sh:*)", - "WebSearch" + "WebSearch", + "Skill(test_global_job)", + "Skill(test_global_job.test_step)", + "Skill(job_porter)", + "Skill(job_porter.list_jobs)", + "Skill(job_porter.port_job)", + "Skill(job_porter.explain_scopes)" ] }, "hooks": { diff --git a/.claude/skills/deepwork_jobs.define/SKILL.md b/.claude/skills/deepwork_jobs.define/SKILL.md index f1469253..ff43d1fd 100644 --- a/.claude/skills/deepwork_jobs.define/SKILL.md +++ b/.claude/skills/deepwork_jobs.define/SKILL.md @@ -32,6 +32,17 @@ Guide the user through defining a job specification by asking structured questio The output of this step is **only** the `job.yml` file - a complete specification of the workflow. The actual step instruction files will be created in the next step (`implement`). +### Step 0.5: Determine Job Scope (Local or Global) + +Before starting the workflow definition, ask the user where they want this job to be installed: + +**Ask structured questions:** +- "Where would you like this job to be installed?" + - **Local** - Available only in this project (stored in `.deepwork/jobs/`) + - **Global** - Available across all projects with DeepWork installed (stored in `~/.deepwork/jobs/`) + +**Store this decision** to use later when creating the job directory. Most users will want local jobs (project-specific workflows), but global jobs are useful for workflows that apply across many projects (e.g., generic documentation tasks, code review processes). + ### Step 1: Understand the Job Purpose Start by asking structured questions to understand what the user wants to accomplish: diff --git a/.claude/skills/deepwork_jobs.implement/SKILL.md b/.claude/skills/deepwork_jobs.implement/SKILL.md index a0c1d388..4539b31c 100644 --- a/.claude/skills/deepwork_jobs.implement/SKILL.md +++ b/.claude/skills/deepwork_jobs.implement/SKILL.md @@ -34,23 +34,29 @@ Read the `job.yml` specification file and create all the necessary files to make ### Step 1: Create Directory Structure Using Script +**Important**: If the user chose **global** scope during the define step, add the `--global` flag when running the script. + Run the `make_new_job.sh` script to create the standard directory structure: ```bash +# For local jobs (default) .deepwork/jobs/deepwork_jobs/make_new_job.sh [job_name] + +# For global jobs (if user chose global scope) +.deepwork/jobs/deepwork_jobs/make_new_job.sh [job_name] --global ``` This creates: -- `.deepwork/jobs/[job_name]/` - Main job directory -- `.deepwork/jobs/[job_name]/steps/` - Step instruction files -- `.deepwork/jobs/[job_name]/hooks/` - Custom validation scripts (with .gitkeep) -- `.deepwork/jobs/[job_name]/templates/` - Example file formats (with .gitkeep) -- `.deepwork/jobs/[job_name]/AGENTS.md` - Job management guidance +- `[location]/[job_name]/` - Main job directory (either `.deepwork/jobs/` or `~/.deepwork/jobs/`) +- `[location]/[job_name]/steps/` - Step instruction files +- `[location]/[job_name]/hooks/` - Custom validation scripts (with .gitkeep) +- `[location]/[job_name]/templates/` - Example file formats (with .gitkeep) +- `[location]/[job_name]/AGENTS.md` - Job management guidance **Note**: If the directory already exists (e.g., job.yml was created by define step), you can skip this step or manually create the additional directories: ```bash -mkdir -p .deepwork/jobs/[job_name]/hooks .deepwork/jobs/[job_name]/templates -touch .deepwork/jobs/[job_name]/hooks/.gitkeep .deepwork/jobs/[job_name]/templates/.gitkeep +mkdir -p [location]/[job_name]/hooks [location]/[job_name]/templates +touch [location]/[job_name]/hooks/.gitkeep [location]/[job_name]/templates/.gitkeep ``` ### Step 2: Read and Validate the Specification diff --git a/.claude/skills/job_porter.explain_scopes/SKILL.md b/.claude/skills/job_porter.explain_scopes/SKILL.md new file mode 100644 index 00000000..87c6a5dd --- /dev/null +++ b/.claude/skills/job_porter.explain_scopes/SKILL.md @@ -0,0 +1,235 @@ +--- +name: job_porter.explain_scopes +description: "Provides detailed explanation of local vs global jobs and when to use each" + +--- + +# job_porter.explain_scopes + +**Standalone skill** - can be run anytime + +> Helps users port DeepWork jobs between local and global locations + + +## Instructions + +**Goal**: Provides detailed explanation of local vs global jobs and when to use each + +# Explain Job Scopes + +## Objective + +Help users understand the difference between local and global jobs, and when to use each scope. + +## Task + +Create a comprehensive guide explaining local vs global jobs with examples and best practices. + +### Step 1: Create the Scope Guide + +Generate a detailed markdown document explaining job scopes: + +```bash +cat > scope_guide.md << 'EOF' +# Understanding DeepWork Job Scopes + +## Overview + +DeepWork supports two types of job scopes: **local** and **global**. Understanding when to use each helps you organize your workflows effectively. + +## Local Jobs + +**Location**: `.deepwork/jobs/` (within your project) + +**Characteristics**: +- ✅ Available only in the current project +- ✅ Version controlled with your project (via Git) +- ✅ Can be shared with team members through your repository +- ✅ Can be customized for project-specific needs + +**Use local jobs when**: +- The workflow is specific to this project's domain or tech stack +- The job needs access to project-specific files or configurations +- You want team members to have the same workflows +- The job is still being developed/refined + +**Examples**: +- `deploy_staging` - Deploys this specific application +- `run_project_tests` - Runs tests specific to this codebase +- `generate_api_docs` - Creates docs from this project's API +- `competitive_research` - Research specific to your product/market + +## Global Jobs + +**Location**: `~/.deepwork/jobs/` (in your home directory) + +**Characteristics**: +- ✅ Available across all projects on your system +- ✅ Persists even if you delete projects +- ✅ Personal workflows that apply to many contexts +- ✅ No need to recreate in each project + +**Use global jobs when**: +- The workflow applies across multiple projects +- The job is a general-purpose utility +- You want to use it in projects where you don't control the repo +- The workflow is mature and stable + +**Examples**: +- `git_commit_summary` - Works with any Git repository +- `write_tutorial` - Generic documentation workflow +- `code_review_checklist` - Applies to any codebase +- `meeting_notes` - General note-taking workflow + +## Decision Guide + +Ask yourself these questions: + +| Question | If Yes → | If No → | +|----------|----------|---------| +| Does this workflow only make sense in this specific project? | **Local** | Continue... | +| Do I want this workflow version-controlled with my code? | **Local** | Continue... | +| Will I use this workflow in multiple different projects? | **Global** | Continue... | +| Is this a general utility that works anywhere? | **Global** | **Local** | + +## Migration Strategy + +You can always change your mind! Use `/job_porter.port_job` to move jobs between scopes. + +**Common migrations**: +- **Local → Global**: When you realize a workflow is useful across projects +- **Global → Local**: When you want to customize a global workflow for a specific project + +## Best Practices + +### Start Local, Go Global Later +When creating new jobs, start with local scope. Once you've used it successfully and realize it applies elsewhere, port it to global. + +### Keep Team Workflows Local +If your team shares a repository, keep shared workflows local so everyone has access. + +### Personal Utilities as Global +General-purpose tools you use frequently (like documentation generators, Git utilities, etc.) work best as global jobs. + +### Version Control Local Jobs +Local jobs are in `.deepwork/jobs/`, so they're version controlled. This is perfect for team collaboration. + +### Document Global Jobs +Since global jobs aren't in version control, document them separately or maintain a personal repository of your global jobs. + +## Examples by Role + +### For Engineers +- **Local**: `deploy_app`, `run_integration_tests`, `update_dependencies` +- **Global**: `git_summary`, `code_review`, `technical_blog_post` + +### For Product Managers +- **Local**: `product_roadmap`, `feature_spec`, `release_notes` +- **Global**: `meeting_notes`, `stakeholder_update`, `competitive_research` + +### For Data Analysts +- **Local**: `etl_pipeline`, `dashboard_update`, `model_training` +- **Global**: `data_exploration`, `report_template`, `chart_generator` + +## Getting Help + +- List all your jobs: `/job_porter.list_jobs` +- Port a job: `/job_porter.port_job` +- See this guide: `/job_porter.explain_scopes` + +--- + +*Remember*: The scope decision isn't permanent. You can always move jobs later as your needs evolve! +EOF + +echo "✓ Scope guide created: scope_guide.md" +``` + +### Step 2: Display Key Points + +Show the user the most important takeaways: + +```bash +echo "" +echo "=== Key Takeaways ===" +echo "" +echo "LOCAL JOBS (.deepwork/jobs/):" +echo " • Project-specific workflows" +echo " • Version controlled with your code" +echo " • Shared with team members" +echo " • Example: deploy_staging, run_project_tests" +echo "" +echo "GLOBAL JOBS (~/.deepwork/jobs/):" +echo " • Available across all projects" +echo " • Personal workflows and utilities" +echo " • No need to recreate in each project" +echo " • Example: git_commit_summary, write_tutorial" +echo "" +echo "RULE OF THUMB:" +echo " Start local, go global when you realize it's useful elsewhere." +echo "" +``` + +### Step 3: Offer Next Steps + +Provide actionable next steps: + +```bash +echo "=== What You Can Do Now ===" +echo "" +echo "1. Review the full guide: cat scope_guide.md" +echo "2. List your current jobs: /job_porter.list_jobs" +echo "3. Port a job if needed: /job_porter.port_job" +echo "4. When creating new jobs with /deepwork_jobs, choose the right scope" +echo "" +``` + +## Quality Criteria + +- **Comprehensive guide**: `scope_guide.md` covers all aspects of job scopes +- **Clear distinctions**: Local vs global differences are well explained +- **Practical examples**: Real-world examples for different use cases +- **Decision framework**: Clear guidance on choosing between local and global +- **Actionable**: User knows exactly what to do next +- **Role-specific examples**: Provided examples for different user types + + +### Job Context + +This job assists users in managing job scope by porting jobs between local +project-specific locations (.deepwork/jobs/) and global system-wide locations +(~/.deepwork/jobs/). It guides users through discovering available jobs, +understanding the differences between local and global scope, and safely +migrating jobs between scopes. + + + +## Work Branch + +Use branch format: `deepwork/job_porter-[instance]-YYYYMMDD` + +- If on a matching work branch: continue using it +- If on main/master: create new branch with `git checkout -b deepwork/job_porter-[instance]-$(date +%Y%m%d)` + +## Outputs + +**Required outputs**: +- `scope_guide.md` + +## Guardrails + +- Do NOT skip prerequisite verification if this step has dependencies +- Do NOT produce partial outputs; complete all required outputs before finishing +- Do NOT proceed without required inputs; ask the user if any are missing +- Do NOT modify files outside the scope of this step's defined outputs + +## On Completion + +1. Verify outputs are created +2. Inform user: "explain_scopes complete, outputs: scope_guide.md" + +This standalone skill can be re-run anytime. + +--- + +**Reference files**: `.deepwork/jobs/job_porter/job.yml`, `.deepwork/jobs/job_porter/steps/explain_scopes.md` \ No newline at end of file diff --git a/.claude/skills/job_porter.list_jobs/SKILL.md b/.claude/skills/job_porter.list_jobs/SKILL.md new file mode 100644 index 00000000..a171de46 --- /dev/null +++ b/.claude/skills/job_porter.list_jobs/SKILL.md @@ -0,0 +1,159 @@ +--- +name: job_porter.list_jobs +description: "Shows all available jobs in both local and global locations with their current scope" + +--- + +# job_porter.list_jobs + +**Standalone skill** - can be run anytime + +> Helps users port DeepWork jobs between local and global locations + + +## Instructions + +**Goal**: Shows all available jobs in both local and global locations with their current scope + +# List Available Jobs + +## Objective + +Display all DeepWork jobs available in both local and global locations, helping users understand what jobs they have and where they're located. + +## Task + +Discover and list all jobs from both local and global locations, presenting them in a clear, organized format. + +### Step 1: Discover Jobs from Both Locations + +Run the sync command with output to see job counts: + +```bash +deepwork sync 2>&1 | grep -A3 "Found.*job(s)" +``` + +This will show you the count of local and global jobs. + +### Step 2: List Local Jobs + +List all jobs in the local `.deepwork/jobs/` directory: + +```bash +if [ -d ".deepwork/jobs" ]; then + echo "=== LOCAL JOBS (.deepwork/jobs/) ===" + for job_dir in .deepwork/jobs/*/; do + if [ -f "${job_dir}job.yml" ]; then + job_name=$(basename "$job_dir") + # Extract version from job.yml + version=$(grep "^version:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + # Extract summary + summary=$(grep "^summary:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + echo " 📁 $job_name (v$version)" + echo " $summary" + echo "" + fi + done +else + echo "=== LOCAL JOBS ===" + echo " (none)" + echo "" +fi +``` + +### Step 3: List Global Jobs + +List all jobs in the global `~/.deepwork/jobs/` directory: + +```bash +if [ -d "$HOME/.deepwork/jobs" ]; then + echo "=== GLOBAL JOBS (~/.deepwork/jobs/) ===" + for job_dir in $HOME/.deepwork/jobs/*/; do + if [ -f "${job_dir}job.yml" ]; then + job_name=$(basename "$job_dir") + # Extract version from job.yml + version=$(grep "^version:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + # Extract summary + summary=$(grep "^summary:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + echo " 🌍 $job_name (v$version)" + echo " $summary" + echo "" + fi + done +else + echo "=== GLOBAL JOBS ===" + echo " (none)" + echo "" +fi +``` + +### Step 4: Create Summary Output + +Save the complete list to `jobs_list.txt`: + +```bash +{ + echo "# DeepWork Jobs Inventory" + echo "Generated: $(date)" + echo "" + # Run the above discovery commands +} > jobs_list.txt +``` + +### Step 5: Provide Guidance + +Explain what the user can do next: + +1. **Port a job**: Use `/job_porter.port_job` to move a job between local and global +2. **Learn about scopes**: Use `/job_porter.explain_scopes` to understand when to use each scope +3. **Create new jobs**: When creating jobs with `/deepwork_jobs`, you'll be asked to choose the scope + +## Quality Criteria + +- **Both locations checked**: Listed jobs from both `.deepwork/jobs/` and `~/.deepwork/jobs/` +- **Clear organization**: Jobs are clearly separated by scope (local vs global) +- **Useful metadata**: Each job shows name, version, and summary +- **Visual distinction**: Used emojis or markers to distinguish local (📁) from global (🌍) jobs +- **Output file created**: `jobs_list.txt` contains the complete inventory +- **Next steps provided**: User knows how to port jobs or learn more about scopes + + +### Job Context + +This job assists users in managing job scope by porting jobs between local +project-specific locations (.deepwork/jobs/) and global system-wide locations +(~/.deepwork/jobs/). It guides users through discovering available jobs, +understanding the differences between local and global scope, and safely +migrating jobs between scopes. + + + +## Work Branch + +Use branch format: `deepwork/job_porter-[instance]-YYYYMMDD` + +- If on a matching work branch: continue using it +- If on main/master: create new branch with `git checkout -b deepwork/job_porter-[instance]-$(date +%Y%m%d)` + +## Outputs + +**Required outputs**: +- `jobs_list.txt` + +## Guardrails + +- Do NOT skip prerequisite verification if this step has dependencies +- Do NOT produce partial outputs; complete all required outputs before finishing +- Do NOT proceed without required inputs; ask the user if any are missing +- Do NOT modify files outside the scope of this step's defined outputs + +## On Completion + +1. Verify outputs are created +2. Inform user: "list_jobs complete, outputs: jobs_list.txt" + +This standalone skill can be re-run anytime. + +--- + +**Reference files**: `.deepwork/jobs/job_porter/job.yml`, `.deepwork/jobs/job_porter/steps/list_jobs.md` \ No newline at end of file diff --git a/.claude/skills/job_porter.port_job/SKILL.md b/.claude/skills/job_porter.port_job/SKILL.md new file mode 100644 index 00000000..8bc7b577 --- /dev/null +++ b/.claude/skills/job_porter.port_job/SKILL.md @@ -0,0 +1,201 @@ +--- +name: job_porter.port_job +description: "Moves a job between local and global scope with safety validation" + +--- + +# job_porter.port_job + +**Standalone skill** - can be run anytime + +> Helps users port DeepWork jobs between local and global locations + + +## Instructions + +**Goal**: Moves a job between local and global scope with safety validation + +# Port a Job + +## Objective + +Safely move a DeepWork job between local (project-specific) and global (system-wide) locations. + +## Task + +Guide the user through porting a job, asking structured questions to understand their needs and ensuring the operation is safe. + +### Step 1: Understand the Request + +Ask structured questions to determine what the user wants to do: + +1. **Which job do you want to port?** + - List available jobs from both locations (use the list_jobs step logic) + - Get the exact job name from the user + +2. **Where do you want to port it?** + - **Local** - Make it available only in this project (`.deepwork/jobs/`) + - **Global** - Make it available across all projects (`~/.deepwork/jobs/`) + +Store the job name as `{job_name}` and destination as `{destination}` (either "local" or "global"). + +### Step 2: Validate the Job Exists + +Check if the job exists in either location: + +```bash +job_name="{job_name}" + +# Check local +if [ -d ".deepwork/jobs/$job_name" ]; then + current_location="local" + echo "✓ Found '$job_name' in local location" +elif [ -d "$HOME/.deepwork/jobs/$job_name" ]; then + current_location="global" + echo "✓ Found '$job_name' in global location" +else + echo "✗ Job '$job_name' not found in local or global locations" + exit 1 +fi +``` + +### Step 3: Check if Already at Destination + +Verify the job isn't already in the target location: + +```bash +destination="{destination}" + +if [ "$current_location" = "$destination" ]; then + echo "⚠️ Job '$job_name' is already in $destination location" + echo "No action needed." + exit 0 +fi +``` + +### Step 4: Run the Port Command + +Execute the deepwork port command: + +```bash +deepwork port "$job_name" --to "$destination" +``` + +### Step 5: Verify the Port + +Confirm the job was moved successfully: + +```bash +if [ "$destination" = "local" ]; then + if [ -d ".deepwork/jobs/$job_name" ]; then + echo "✓ Job successfully ported to local location" + echo " Location: .deepwork/jobs/$job_name" + else + echo "✗ Port failed - job not found at destination" + exit 1 + fi +elif [ "$destination" = "global" ]; then + if [ -d "$HOME/.deepwork/jobs/$job_name" ]; then + echo "✓ Job successfully ported to global location" + echo " Location: ~/.deepwork/jobs/$job_name" + else + echo "✗ Port failed - job not found at destination" + exit 1 + fi +fi +``` + +### Step 6: Sync Skills + +After porting, regenerate the skills for all platforms: + +```bash +echo "" +echo "Syncing skills to reflect the change..." +deepwork sync +``` + +### Step 7: Provide Confirmation + +Create a summary output file with the results: + +```bash +{ + echo "# Job Port Complete" + echo "" + echo "**Job**: $job_name" + echo "**From**: $current_location" + echo "**To**: $destination" + echo "**Date**: $(date)" + echo "" + echo "## Next Steps" + echo "" + if [ "$destination" = "global" ]; then + echo "The job is now available in all projects with DeepWork installed." + echo "You can use it by running the skill in any project." + else + echo "The job is now available only in this project." + echo "To use it in other projects, you'll need to port it to global or recreate it." + fi +} > port_result.txt + +echo "" +echo "Summary saved to port_result.txt" +``` + +## Quality Criteria + +- **Job validated**: Confirmed the job exists before attempting to port +- **Destination verified**: Checked the job isn't already at the destination +- **Port command executed**: Used `deepwork port` to move the job +- **Success confirmed**: Verified the job exists at the new location +- **Skills synced**: Ran `deepwork sync` to regenerate skills +- **Summary created**: `port_result.txt` contains complete details of the operation +- **Clear guidance**: User understands what happened and what to do next + + +### Job Context + +This job assists users in managing job scope by porting jobs between local +project-specific locations (.deepwork/jobs/) and global system-wide locations +(~/.deepwork/jobs/). It guides users through discovering available jobs, +understanding the differences between local and global scope, and safely +migrating jobs between scopes. + + +## Required Inputs + +**User Parameters** - Gather from user before starting: +- **job_name**: Name of the job to port +- **destination**: Target scope (local or global) + + +## Work Branch + +Use branch format: `deepwork/job_porter-[instance]-YYYYMMDD` + +- If on a matching work branch: continue using it +- If on main/master: create new branch with `git checkout -b deepwork/job_porter-[instance]-$(date +%Y%m%d)` + +## Outputs + +**Required outputs**: +- `port_result.txt` + +## Guardrails + +- Do NOT skip prerequisite verification if this step has dependencies +- Do NOT produce partial outputs; complete all required outputs before finishing +- Do NOT proceed without required inputs; ask the user if any are missing +- Do NOT modify files outside the scope of this step's defined outputs + +## On Completion + +1. Verify outputs are created +2. Inform user: "port_job complete, outputs: port_result.txt" + +This standalone skill can be re-run anytime. + +--- + +**Reference files**: `.deepwork/jobs/job_porter/job.yml`, `.deepwork/jobs/job_porter/steps/port_job.md` \ No newline at end of file diff --git a/.claude/skills/job_porter/SKILL.md b/.claude/skills/job_porter/SKILL.md new file mode 100644 index 00000000..a5c3a27b --- /dev/null +++ b/.claude/skills/job_porter/SKILL.md @@ -0,0 +1,71 @@ +--- +name: job_porter +description: "Helps users port DeepWork jobs between local and global locations" +--- + +# job_porter + +Helps users port DeepWork jobs between local and global locations + +> **CRITICAL**: Always invoke steps using the Skill tool. Never copy/paste step instructions directly. + +This job assists users in managing job scope by porting jobs between local +project-specific locations (.deepwork/jobs/) and global system-wide locations +(~/.deepwork/jobs/). It guides users through discovering available jobs, +understanding the differences between local and global scope, and safely +migrating jobs between scopes. + + +## Standalone Skills + +These skills can be run independently at any time: + +- **list_jobs** - Shows all available jobs in both local and global locations with their current scope + Command: `/job_porter.list_jobs` +- **port_job** - Moves a job between local and global scope with safety validation + Command: `/job_porter.port_job` +- **explain_scopes** - Provides detailed explanation of local vs global jobs and when to use each + Command: `/job_porter.explain_scopes` + + +## Execution Instructions + +### Step 1: Analyze Intent + +Parse any text following `/job_porter` to determine user intent: +- "list_jobs" or related terms → run standalone skill `job_porter.list_jobs` +- "port_job" or related terms → run standalone skill `job_porter.port_job` +- "explain_scopes" or related terms → run standalone skill `job_porter.explain_scopes` + +### Step 2: Invoke Starting Step + +Use the Skill tool to invoke the identified starting step: +``` +Skill tool: job_porter.list_jobs +``` + +### Step 3: Continue Workflow Automatically + +After each step completes: +1. Check if there's a next step in the workflow sequence +2. Invoke the next step using the Skill tool +3. Repeat until workflow is complete or user intervenes + +**Note**: Standalone skills do not auto-continue to other steps. + +### Handling Ambiguous Intent + +If user intent is unclear, use AskUserQuestion to clarify: +- Present available steps as numbered options +- Let user select the starting point + +## Guardrails + +- Do NOT copy/paste step instructions directly; always use the Skill tool to invoke steps +- Do NOT skip steps in a workflow unless the user explicitly requests it +- Do NOT proceed to the next step if the current step's outputs are incomplete +- Do NOT make assumptions about user intent; ask for clarification when ambiguous + +## Context Files + +- Job definition: `.deepwork/jobs/job_porter/job.yml` \ No newline at end of file diff --git a/.claude/skills/test_global_job.test_step/SKILL.md b/.claude/skills/test_global_job.test_step/SKILL.md new file mode 100644 index 00000000..a9f1216a --- /dev/null +++ b/.claude/skills/test_global_job.test_step/SKILL.md @@ -0,0 +1,61 @@ +--- +name: test_global_job.test_step +description: "A simple test step" +user-invocable: false + +--- + +# test_global_job.test_step + +**Standalone skill** - can be run anytime + +> A test global job for demonstration + + +## Instructions + +**Goal**: A simple test step + +# Test Step + +This is a simple test step for the global job. + +## Task + +Just create a file called output.txt with some text in it. + + +### Job Context + +This is a test job to demonstrate global job functionality + + +## Work Branch + +Use branch format: `deepwork/test_global_job-[instance]-YYYYMMDD` + +- If on a matching work branch: continue using it +- If on main/master: create new branch with `git checkout -b deepwork/test_global_job-[instance]-$(date +%Y%m%d)` + +## Outputs + +**Required outputs**: +- `output.txt` + +## Guardrails + +- Do NOT skip prerequisite verification if this step has dependencies +- Do NOT produce partial outputs; complete all required outputs before finishing +- Do NOT proceed without required inputs; ask the user if any are missing +- Do NOT modify files outside the scope of this step's defined outputs + +## On Completion + +1. Verify outputs are created +2. Inform user: "test_step complete, outputs: output.txt" + +This standalone skill can be re-run anytime. + +--- + +**Reference files**: `.deepwork/jobs/test_global_job/job.yml`, `.deepwork/jobs/test_global_job/steps/test_step.md` \ No newline at end of file diff --git a/.claude/skills/test_global_job/SKILL.md b/.claude/skills/test_global_job/SKILL.md new file mode 100644 index 00000000..a8f65f8a --- /dev/null +++ b/.claude/skills/test_global_job/SKILL.md @@ -0,0 +1,60 @@ +--- +name: test_global_job +description: "A test global job for demonstration" +--- + +# test_global_job + +A test global job for demonstration + +> **CRITICAL**: Always invoke steps using the Skill tool. Never copy/paste step instructions directly. + +This is a test job to demonstrate global job functionality + +## Standalone Skills + +These skills can be run independently at any time: + +- **test_step** - A simple test step + Command: `/test_global_job.test_step` + + +## Execution Instructions + +### Step 1: Analyze Intent + +Parse any text following `/test_global_job` to determine user intent: +- "test_step" or related terms → run standalone skill `test_global_job.test_step` + +### Step 2: Invoke Starting Step + +Use the Skill tool to invoke the identified starting step: +``` +Skill tool: test_global_job.test_step +``` + +### Step 3: Continue Workflow Automatically + +After each step completes: +1. Check if there's a next step in the workflow sequence +2. Invoke the next step using the Skill tool +3. Repeat until workflow is complete or user intervenes + +**Note**: Standalone skills do not auto-continue to other steps. + +### Handling Ambiguous Intent + +If user intent is unclear, use AskUserQuestion to clarify: +- Present available steps as numbered options +- Let user select the starting point + +## Guardrails + +- Do NOT copy/paste step instructions directly; always use the Skill tool to invoke steps +- Do NOT skip steps in a workflow unless the user explicitly requests it +- Do NOT proceed to the next step if the current step's outputs are incomplete +- Do NOT make assumptions about user intent; ask for clarification when ambiguous + +## Context Files + +- Job definition: `.deepwork/jobs/test_global_job/job.yml` \ No newline at end of file diff --git a/.deepwork/jobs/deepwork_jobs/make_new_job.sh b/.deepwork/jobs/deepwork_jobs/make_new_job.sh index c561d6d2..3fe05237 100755 --- a/.deepwork/jobs/deepwork_jobs/make_new_job.sh +++ b/.deepwork/jobs/deepwork_jobs/make_new_job.sh @@ -37,24 +37,38 @@ validate_job_name() { # Main script main() { if [[ $# -lt 1 ]]; then - echo "Usage: $0 " + echo "Usage: $0 [--global]" echo "" echo "Creates the directory structure for a new DeepWork job." echo "" echo "Arguments:" echo " job_name Name of the job (lowercase, underscores allowed)" + echo " --global Create the job in global location (~/.deepwork/jobs)" echo "" echo "Example:" echo " $0 competitive_research" + echo " $0 competitive_research --global" exit 1 fi local job_name="$1" + local is_global=false + + # Check for --global flag + if [[ $# -gt 1 && "$2" == "--global" ]]; then + is_global=true + fi + validate_job_name "$job_name" - # Determine the base path - look for .deepwork directory + # Determine the base path local base_path - if [[ -d ".deepwork/jobs" ]]; then + if [[ "$is_global" == true ]]; then + # Create in global location + base_path="$HOME/.deepwork/jobs" + mkdir -p "$base_path" + info "Creating global job in $base_path" + elif [[ -d ".deepwork/jobs" ]]; then base_path=".deepwork/jobs" elif [[ -d "../.deepwork/jobs" ]]; then base_path="../.deepwork/jobs" diff --git a/.deepwork/jobs/deepwork_jobs/steps/define.md b/.deepwork/jobs/deepwork_jobs/steps/define.md index 31de7440..65958efe 100644 --- a/.deepwork/jobs/deepwork_jobs/steps/define.md +++ b/.deepwork/jobs/deepwork_jobs/steps/define.md @@ -12,6 +12,17 @@ Guide the user through defining a job specification by asking structured questio The output of this step is **only** the `job.yml` file - a complete specification of the workflow. The actual step instruction files will be created in the next step (`implement`). +### Step 0.5: Determine Job Scope (Local or Global) + +Before starting the workflow definition, ask the user where they want this job to be installed: + +**Ask structured questions:** +- "Where would you like this job to be installed?" + - **Local** - Available only in this project (stored in `.deepwork/jobs/`) + - **Global** - Available across all projects with DeepWork installed (stored in `~/.deepwork/jobs/`) + +**Store this decision** to use later when creating the job directory. Most users will want local jobs (project-specific workflows), but global jobs are useful for workflows that apply across many projects (e.g., generic documentation tasks, code review processes). + ### Step 1: Understand the Job Purpose Start by asking structured questions to understand what the user wants to accomplish: diff --git a/.deepwork/jobs/deepwork_jobs/steps/implement.md b/.deepwork/jobs/deepwork_jobs/steps/implement.md index 749c8c6f..bb0e052d 100644 --- a/.deepwork/jobs/deepwork_jobs/steps/implement.md +++ b/.deepwork/jobs/deepwork_jobs/steps/implement.md @@ -10,23 +10,29 @@ Read the `job.yml` specification file and create all the necessary files to make ### Step 1: Create Directory Structure Using Script +**Important**: If the user chose **global** scope during the define step, add the `--global` flag when running the script. + Run the `make_new_job.sh` script to create the standard directory structure: ```bash +# For local jobs (default) .deepwork/jobs/deepwork_jobs/make_new_job.sh [job_name] + +# For global jobs (if user chose global scope) +.deepwork/jobs/deepwork_jobs/make_new_job.sh [job_name] --global ``` This creates: -- `.deepwork/jobs/[job_name]/` - Main job directory -- `.deepwork/jobs/[job_name]/steps/` - Step instruction files -- `.deepwork/jobs/[job_name]/hooks/` - Custom validation scripts (with .gitkeep) -- `.deepwork/jobs/[job_name]/templates/` - Example file formats (with .gitkeep) -- `.deepwork/jobs/[job_name]/AGENTS.md` - Job management guidance +- `[location]/[job_name]/` - Main job directory (either `.deepwork/jobs/` or `~/.deepwork/jobs/`) +- `[location]/[job_name]/steps/` - Step instruction files +- `[location]/[job_name]/hooks/` - Custom validation scripts (with .gitkeep) +- `[location]/[job_name]/templates/` - Example file formats (with .gitkeep) +- `[location]/[job_name]/AGENTS.md` - Job management guidance **Note**: If the directory already exists (e.g., job.yml was created by define step), you can skip this step or manually create the additional directories: ```bash -mkdir -p .deepwork/jobs/[job_name]/hooks .deepwork/jobs/[job_name]/templates -touch .deepwork/jobs/[job_name]/hooks/.gitkeep .deepwork/jobs/[job_name]/templates/.gitkeep +mkdir -p [location]/[job_name]/hooks [location]/[job_name]/templates +touch [location]/[job_name]/hooks/.gitkeep [location]/[job_name]/templates/.gitkeep ``` ### Step 2: Read and Validate the Specification diff --git a/.deepwork/jobs/job_porter/AGENTS.md b/.deepwork/jobs/job_porter/AGENTS.md new file mode 100644 index 00000000..15f0271b --- /dev/null +++ b/.deepwork/jobs/job_porter/AGENTS.md @@ -0,0 +1,30 @@ +# Job Management + +This folder and its subfolders are managed using the `deepwork_jobs` slash commands. + +## Recommended Commands + +- `/deepwork_jobs.define` - Create or modify the job.yml specification +- `/deepwork_jobs.implement` - Generate step instruction files from the specification +- `/deepwork_jobs.learn` - Improve instructions based on execution learnings + +## Directory Structure + +``` +. +├── AGENTS.md # This file - project context and guidance +├── job.yml # Job specification (created by /deepwork_jobs.define) +├── steps/ # Step instruction files (created by /deepwork_jobs.implement) +│ └── *.md # One file per step +├── hooks/ # Custom validation scripts and prompts +│ └── *.md|*.sh # Hook files referenced in job.yml +└── templates/ # Example file formats and templates + └── *.md|*.yml # Templates referenced in step instructions +``` + +## Editing Guidelines + +1. **Use slash commands** for structural changes (adding steps, modifying job.yml) +2. **Direct edits** are fine for minor instruction tweaks +3. **Run `/deepwork_jobs.learn`** after executing job steps to capture improvements +4. **Run `deepwork sync`** after any changes to regenerate commands diff --git a/.deepwork/jobs/job_porter/hooks/.gitkeep b/.deepwork/jobs/job_porter/hooks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.deepwork/jobs/job_porter/job.yml b/.deepwork/jobs/job_porter/job.yml new file mode 100644 index 00000000..55c0d423 --- /dev/null +++ b/.deepwork/jobs/job_porter/job.yml @@ -0,0 +1,46 @@ +name: job_porter +version: 1.0.0 +summary: Helps users port DeepWork jobs between local and global locations +description: | + This job assists users in managing job scope by porting jobs between local + project-specific locations (.deepwork/jobs/) and global system-wide locations + (~/.deepwork/jobs/). It guides users through discovering available jobs, + understanding the differences between local and global scope, and safely + migrating jobs between scopes. + +changelog: + - version: 1.0.0 + changes: Initial version - port jobs between local and global scopes + +steps: + - id: list_jobs + name: List Available Jobs + description: Shows all available jobs in both local and global locations with their current scope + instructions_file: steps/list_jobs.md + exposed: true + outputs: + - jobs_list.txt + dependencies: [] + + - id: port_job + name: Port a Job + description: Moves a job between local and global scope with safety validation + instructions_file: steps/port_job.md + exposed: true + inputs: + - name: job_name + description: Name of the job to port + - name: destination + description: Target scope (local or global) + outputs: + - port_result.txt + dependencies: [] + + - id: explain_scopes + name: Explain Job Scopes + description: Provides detailed explanation of local vs global jobs and when to use each + instructions_file: steps/explain_scopes.md + exposed: true + outputs: + - scope_guide.md + dependencies: [] diff --git a/.deepwork/jobs/job_porter/steps/explain_scopes.md b/.deepwork/jobs/job_porter/steps/explain_scopes.md new file mode 100644 index 00000000..123c4cba --- /dev/null +++ b/.deepwork/jobs/job_porter/steps/explain_scopes.md @@ -0,0 +1,177 @@ +# Explain Job Scopes + +## Objective + +Help users understand the difference between local and global jobs, and when to use each scope. + +## Task + +Create a comprehensive guide explaining local vs global jobs with examples and best practices. + +### Step 1: Create the Scope Guide + +Generate a detailed markdown document explaining job scopes: + +```bash +cat > scope_guide.md << 'EOF' +# Understanding DeepWork Job Scopes + +## Overview + +DeepWork supports two types of job scopes: **local** and **global**. Understanding when to use each helps you organize your workflows effectively. + +## Local Jobs + +**Location**: `.deepwork/jobs/` (within your project) + +**Characteristics**: +- ✅ Available only in the current project +- ✅ Version controlled with your project (via Git) +- ✅ Can be shared with team members through your repository +- ✅ Can be customized for project-specific needs + +**Use local jobs when**: +- The workflow is specific to this project's domain or tech stack +- The job needs access to project-specific files or configurations +- You want team members to have the same workflows +- The job is still being developed/refined + +**Examples**: +- `deploy_staging` - Deploys this specific application +- `run_project_tests` - Runs tests specific to this codebase +- `generate_api_docs` - Creates docs from this project's API +- `competitive_research` - Research specific to your product/market + +## Global Jobs + +**Location**: `~/.deepwork/jobs/` (in your home directory) + +**Characteristics**: +- ✅ Available across all projects on your system +- ✅ Persists even if you delete projects +- ✅ Personal workflows that apply to many contexts +- ✅ No need to recreate in each project + +**Use global jobs when**: +- The workflow applies across multiple projects +- The job is a general-purpose utility +- You want to use it in projects where you don't control the repo +- The workflow is mature and stable + +**Examples**: +- `git_commit_summary` - Works with any Git repository +- `write_tutorial` - Generic documentation workflow +- `code_review_checklist` - Applies to any codebase +- `meeting_notes` - General note-taking workflow + +## Decision Guide + +Ask yourself these questions: + +| Question | If Yes → | If No → | +|----------|----------|---------| +| Does this workflow only make sense in this specific project? | **Local** | Continue... | +| Do I want this workflow version-controlled with my code? | **Local** | Continue... | +| Will I use this workflow in multiple different projects? | **Global** | Continue... | +| Is this a general utility that works anywhere? | **Global** | **Local** | + +## Migration Strategy + +You can always change your mind! Use `/job_porter.port_job` to move jobs between scopes. + +**Common migrations**: +- **Local → Global**: When you realize a workflow is useful across projects +- **Global → Local**: When you want to customize a global workflow for a specific project + +## Best Practices + +### Start Local, Go Global Later +When creating new jobs, start with local scope. Once you've used it successfully and realize it applies elsewhere, port it to global. + +### Keep Team Workflows Local +If your team shares a repository, keep shared workflows local so everyone has access. + +### Personal Utilities as Global +General-purpose tools you use frequently (like documentation generators, Git utilities, etc.) work best as global jobs. + +### Version Control Local Jobs +Local jobs are in `.deepwork/jobs/`, so they're version controlled. This is perfect for team collaboration. + +### Document Global Jobs +Since global jobs aren't in version control, document them separately or maintain a personal repository of your global jobs. + +## Examples by Role + +### For Engineers +- **Local**: `deploy_app`, `run_integration_tests`, `update_dependencies` +- **Global**: `git_summary`, `code_review`, `technical_blog_post` + +### For Product Managers +- **Local**: `product_roadmap`, `feature_spec`, `release_notes` +- **Global**: `meeting_notes`, `stakeholder_update`, `competitive_research` + +### For Data Analysts +- **Local**: `etl_pipeline`, `dashboard_update`, `model_training` +- **Global**: `data_exploration`, `report_template`, `chart_generator` + +## Getting Help + +- List all your jobs: `/job_porter.list_jobs` +- Port a job: `/job_porter.port_job` +- See this guide: `/job_porter.explain_scopes` + +--- + +*Remember*: The scope decision isn't permanent. You can always move jobs later as your needs evolve! +EOF + +echo "✓ Scope guide created: scope_guide.md" +``` + +### Step 2: Display Key Points + +Show the user the most important takeaways: + +```bash +echo "" +echo "=== Key Takeaways ===" +echo "" +echo "LOCAL JOBS (.deepwork/jobs/):" +echo " • Project-specific workflows" +echo " • Version controlled with your code" +echo " • Shared with team members" +echo " • Example: deploy_staging, run_project_tests" +echo "" +echo "GLOBAL JOBS (~/.deepwork/jobs/):" +echo " • Available across all projects" +echo " • Personal workflows and utilities" +echo " • No need to recreate in each project" +echo " • Example: git_commit_summary, write_tutorial" +echo "" +echo "RULE OF THUMB:" +echo " Start local, go global when you realize it's useful elsewhere." +echo "" +``` + +### Step 3: Offer Next Steps + +Provide actionable next steps: + +```bash +echo "=== What You Can Do Now ===" +echo "" +echo "1. Review the full guide: cat scope_guide.md" +echo "2. List your current jobs: /job_porter.list_jobs" +echo "3. Port a job if needed: /job_porter.port_job" +echo "4. When creating new jobs with /deepwork_jobs, choose the right scope" +echo "" +``` + +## Quality Criteria + +- **Comprehensive guide**: `scope_guide.md` covers all aspects of job scopes +- **Clear distinctions**: Local vs global differences are well explained +- **Practical examples**: Real-world examples for different use cases +- **Decision framework**: Clear guidance on choosing between local and global +- **Actionable**: User knows exactly what to do next +- **Role-specific examples**: Provided examples for different user types diff --git a/.deepwork/jobs/job_porter/steps/list_jobs.md b/.deepwork/jobs/job_porter/steps/list_jobs.md new file mode 100644 index 00000000..b75c585f --- /dev/null +++ b/.deepwork/jobs/job_porter/steps/list_jobs.md @@ -0,0 +1,101 @@ +# List Available Jobs + +## Objective + +Display all DeepWork jobs available in both local and global locations, helping users understand what jobs they have and where they're located. + +## Task + +Discover and list all jobs from both local and global locations, presenting them in a clear, organized format. + +### Step 1: Discover Jobs from Both Locations + +Run the sync command with output to see job counts: + +```bash +deepwork sync 2>&1 | grep -A3 "Found.*job(s)" +``` + +This will show you the count of local and global jobs. + +### Step 2: List Local Jobs + +List all jobs in the local `.deepwork/jobs/` directory: + +```bash +if [ -d ".deepwork/jobs" ]; then + echo "=== LOCAL JOBS (.deepwork/jobs/) ===" + for job_dir in .deepwork/jobs/*/; do + if [ -f "${job_dir}job.yml" ]; then + job_name=$(basename "$job_dir") + # Extract version from job.yml + version=$(grep "^version:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + # Extract summary + summary=$(grep "^summary:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + echo " 📁 $job_name (v$version)" + echo " $summary" + echo "" + fi + done +else + echo "=== LOCAL JOBS ===" + echo " (none)" + echo "" +fi +``` + +### Step 3: List Global Jobs + +List all jobs in the global `~/.deepwork/jobs/` directory: + +```bash +if [ -d "$HOME/.deepwork/jobs" ]; then + echo "=== GLOBAL JOBS (~/.deepwork/jobs/) ===" + for job_dir in $HOME/.deepwork/jobs/*/; do + if [ -f "${job_dir}job.yml" ]; then + job_name=$(basename "$job_dir") + # Extract version from job.yml + version=$(grep "^version:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + # Extract summary + summary=$(grep "^summary:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + echo " 🌍 $job_name (v$version)" + echo " $summary" + echo "" + fi + done +else + echo "=== GLOBAL JOBS ===" + echo " (none)" + echo "" +fi +``` + +### Step 4: Create Summary Output + +Save the complete list to `jobs_list.txt`: + +```bash +{ + echo "# DeepWork Jobs Inventory" + echo "Generated: $(date)" + echo "" + # Run the above discovery commands +} > jobs_list.txt +``` + +### Step 5: Provide Guidance + +Explain what the user can do next: + +1. **Port a job**: Use `/job_porter.port_job` to move a job between local and global +2. **Learn about scopes**: Use `/job_porter.explain_scopes` to understand when to use each scope +3. **Create new jobs**: When creating jobs with `/deepwork_jobs`, you'll be asked to choose the scope + +## Quality Criteria + +- **Both locations checked**: Listed jobs from both `.deepwork/jobs/` and `~/.deepwork/jobs/` +- **Clear organization**: Jobs are clearly separated by scope (local vs global) +- **Useful metadata**: Each job shows name, version, and summary +- **Visual distinction**: Used emojis or markers to distinguish local (📁) from global (🌍) jobs +- **Output file created**: `jobs_list.txt` contains the complete inventory +- **Next steps provided**: User knows how to port jobs or learn more about scopes diff --git a/.deepwork/jobs/job_porter/steps/port_job.md b/.deepwork/jobs/job_porter/steps/port_job.md new file mode 100644 index 00000000..8b24884c --- /dev/null +++ b/.deepwork/jobs/job_porter/steps/port_job.md @@ -0,0 +1,137 @@ +# Port a Job + +## Objective + +Safely move a DeepWork job between local (project-specific) and global (system-wide) locations. + +## Task + +Guide the user through porting a job, asking structured questions to understand their needs and ensuring the operation is safe. + +### Step 1: Understand the Request + +Ask structured questions to determine what the user wants to do: + +1. **Which job do you want to port?** + - List available jobs from both locations (use the list_jobs step logic) + - Get the exact job name from the user + +2. **Where do you want to port it?** + - **Local** - Make it available only in this project (`.deepwork/jobs/`) + - **Global** - Make it available across all projects (`~/.deepwork/jobs/`) + +Store the job name as `{job_name}` and destination as `{destination}` (either "local" or "global"). + +### Step 2: Validate the Job Exists + +Check if the job exists in either location: + +```bash +job_name="{job_name}" + +# Check local +if [ -d ".deepwork/jobs/$job_name" ]; then + current_location="local" + echo "✓ Found '$job_name' in local location" +elif [ -d "$HOME/.deepwork/jobs/$job_name" ]; then + current_location="global" + echo "✓ Found '$job_name' in global location" +else + echo "✗ Job '$job_name' not found in local or global locations" + exit 1 +fi +``` + +### Step 3: Check if Already at Destination + +Verify the job isn't already in the target location: + +```bash +destination="{destination}" + +if [ "$current_location" = "$destination" ]; then + echo "⚠️ Job '$job_name' is already in $destination location" + echo "No action needed." + exit 0 +fi +``` + +### Step 4: Run the Port Command + +Execute the deepwork port command: + +```bash +deepwork port "$job_name" --to "$destination" +``` + +### Step 5: Verify the Port + +Confirm the job was moved successfully: + +```bash +if [ "$destination" = "local" ]; then + if [ -d ".deepwork/jobs/$job_name" ]; then + echo "✓ Job successfully ported to local location" + echo " Location: .deepwork/jobs/$job_name" + else + echo "✗ Port failed - job not found at destination" + exit 1 + fi +elif [ "$destination" = "global" ]; then + if [ -d "$HOME/.deepwork/jobs/$job_name" ]; then + echo "✓ Job successfully ported to global location" + echo " Location: ~/.deepwork/jobs/$job_name" + else + echo "✗ Port failed - job not found at destination" + exit 1 + fi +fi +``` + +### Step 6: Sync Skills + +After porting, regenerate the skills for all platforms: + +```bash +echo "" +echo "Syncing skills to reflect the change..." +deepwork sync +``` + +### Step 7: Provide Confirmation + +Create a summary output file with the results: + +```bash +{ + echo "# Job Port Complete" + echo "" + echo "**Job**: $job_name" + echo "**From**: $current_location" + echo "**To**: $destination" + echo "**Date**: $(date)" + echo "" + echo "## Next Steps" + echo "" + if [ "$destination" = "global" ]; then + echo "The job is now available in all projects with DeepWork installed." + echo "You can use it by running the skill in any project." + else + echo "The job is now available only in this project." + echo "To use it in other projects, you'll need to port it to global or recreate it." + fi +} > port_result.txt + +echo "" +echo "Summary saved to port_result.txt" +``` + +## Quality Criteria + +- **Job validated**: Confirmed the job exists before attempting to port +- **Destination verified**: Checked the job isn't already at the destination +- **Port command executed**: Used `deepwork port` to move the job +- **Success confirmed**: Verified the job exists at the new location +- **Skills synced**: Ran `deepwork sync` to regenerate skills +- **Summary created**: `port_result.txt` contains complete details of the operation +- **Clear guidance**: User understands what happened and what to do next diff --git a/.deepwork/jobs/job_porter/templates/.gitkeep b/.deepwork/jobs/job_porter/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.gemini/skills/deepwork_jobs/define.toml b/.gemini/skills/deepwork_jobs/define.toml index 8a705168..89ffea63 100644 --- a/.gemini/skills/deepwork_jobs/define.toml +++ b/.gemini/skills/deepwork_jobs/define.toml @@ -32,6 +32,17 @@ Guide the user through defining a job specification by asking structured questio The output of this step is **only** the `job.yml` file - a complete specification of the workflow. The actual step instruction files will be created in the next step (`implement`). +### Step 0.5: Determine Job Scope (Local or Global) + +Before starting the workflow definition, ask the user where they want this job to be installed: + +**Ask structured questions:** +- "Where would you like this job to be installed?" + - **Local** - Available only in this project (stored in `.deepwork/jobs/`) + - **Global** - Available across all projects with DeepWork installed (stored in `~/.deepwork/jobs/`) + +**Store this decision** to use later when creating the job directory. Most users will want local jobs (project-specific workflows), but global jobs are useful for workflows that apply across many projects (e.g., generic documentation tasks, code review processes). + ### Step 1: Understand the Job Purpose Start by asking structured questions to understand what the user wants to accomplish: diff --git a/.gemini/skills/deepwork_jobs/implement.toml b/.gemini/skills/deepwork_jobs/implement.toml index 484f4bcc..cb775b94 100644 --- a/.gemini/skills/deepwork_jobs/implement.toml +++ b/.gemini/skills/deepwork_jobs/implement.toml @@ -34,23 +34,29 @@ Read the `job.yml` specification file and create all the necessary files to make ### Step 1: Create Directory Structure Using Script +**Important**: If the user chose **global** scope during the define step, add the `--global` flag when running the script. + Run the `make_new_job.sh` script to create the standard directory structure: ```bash +# For local jobs (default) .deepwork/jobs/deepwork_jobs/make_new_job.sh [job_name] + +# For global jobs (if user chose global scope) +.deepwork/jobs/deepwork_jobs/make_new_job.sh [job_name] --global ``` This creates: -- `.deepwork/jobs/[job_name]/` - Main job directory -- `.deepwork/jobs/[job_name]/steps/` - Step instruction files -- `.deepwork/jobs/[job_name]/hooks/` - Custom validation scripts (with .gitkeep) -- `.deepwork/jobs/[job_name]/templates/` - Example file formats (with .gitkeep) -- `.deepwork/jobs/[job_name]/AGENTS.md` - Job management guidance +- `[location]/[job_name]/` - Main job directory (either `.deepwork/jobs/` or `~/.deepwork/jobs/`) +- `[location]/[job_name]/steps/` - Step instruction files +- `[location]/[job_name]/hooks/` - Custom validation scripts (with .gitkeep) +- `[location]/[job_name]/templates/` - Example file formats (with .gitkeep) +- `[location]/[job_name]/AGENTS.md` - Job management guidance **Note**: If the directory already exists (e.g., job.yml was created by define step), you can skip this step or manually create the additional directories: ```bash -mkdir -p .deepwork/jobs/[job_name]/hooks .deepwork/jobs/[job_name]/templates -touch .deepwork/jobs/[job_name]/hooks/.gitkeep .deepwork/jobs/[job_name]/templates/.gitkeep +mkdir -p [location]/[job_name]/hooks [location]/[job_name]/templates +touch [location]/[job_name]/hooks/.gitkeep [location]/[job_name]/templates/.gitkeep ``` ### Step 2: Read and Validate the Specification diff --git a/.gemini/skills/job_porter/explain_scopes.toml b/.gemini/skills/job_porter/explain_scopes.toml new file mode 100644 index 00000000..58fdc76b --- /dev/null +++ b/.gemini/skills/job_porter/explain_scopes.toml @@ -0,0 +1,232 @@ +# job_porter:explain_scopes +# +# Provides detailed explanation of local vs global jobs and when to use each +# +# Generated by DeepWork - do not edit manually + +description = "Provides detailed explanation of local vs global jobs and when to use each" + +prompt = """ +# job_porter:explain_scopes + +**Standalone command** - can be run anytime + +> Helps users port DeepWork jobs between local and global locations + + +## Instructions + +**Goal**: Provides detailed explanation of local vs global jobs and when to use each + +# Explain Job Scopes + +## Objective + +Help users understand the difference between local and global jobs, and when to use each scope. + +## Task + +Create a comprehensive guide explaining local vs global jobs with examples and best practices. + +### Step 1: Create the Scope Guide + +Generate a detailed markdown document explaining job scopes: + +```bash +cat > scope_guide.md << 'EOF' +# Understanding DeepWork Job Scopes + +## Overview + +DeepWork supports two types of job scopes: **local** and **global**. Understanding when to use each helps you organize your workflows effectively. + +## Local Jobs + +**Location**: `.deepwork/jobs/` (within your project) + +**Characteristics**: +- ✅ Available only in the current project +- ✅ Version controlled with your project (via Git) +- ✅ Can be shared with team members through your repository +- ✅ Can be customized for project-specific needs + +**Use local jobs when**: +- The workflow is specific to this project's domain or tech stack +- The job needs access to project-specific files or configurations +- You want team members to have the same workflows +- The job is still being developed/refined + +**Examples**: +- `deploy_staging` - Deploys this specific application +- `run_project_tests` - Runs tests specific to this codebase +- `generate_api_docs` - Creates docs from this project's API +- `competitive_research` - Research specific to your product/market + +## Global Jobs + +**Location**: `~/.deepwork/jobs/` (in your home directory) + +**Characteristics**: +- ✅ Available across all projects on your system +- ✅ Persists even if you delete projects +- ✅ Personal workflows that apply to many contexts +- ✅ No need to recreate in each project + +**Use global jobs when**: +- The workflow applies across multiple projects +- The job is a general-purpose utility +- You want to use it in projects where you don't control the repo +- The workflow is mature and stable + +**Examples**: +- `git_commit_summary` - Works with any Git repository +- `write_tutorial` - Generic documentation workflow +- `code_review_checklist` - Applies to any codebase +- `meeting_notes` - General note-taking workflow + +## Decision Guide + +Ask yourself these questions: + +| Question | If Yes → | If No → | +|----------|----------|---------| +| Does this workflow only make sense in this specific project? | **Local** | Continue... | +| Do I want this workflow version-controlled with my code? | **Local** | Continue... | +| Will I use this workflow in multiple different projects? | **Global** | Continue... | +| Is this a general utility that works anywhere? | **Global** | **Local** | + +## Migration Strategy + +You can always change your mind! Use `/job_porter.port_job` to move jobs between scopes. + +**Common migrations**: +- **Local → Global**: When you realize a workflow is useful across projects +- **Global → Local**: When you want to customize a global workflow for a specific project + +## Best Practices + +### Start Local, Go Global Later +When creating new jobs, start with local scope. Once you've used it successfully and realize it applies elsewhere, port it to global. + +### Keep Team Workflows Local +If your team shares a repository, keep shared workflows local so everyone has access. + +### Personal Utilities as Global +General-purpose tools you use frequently (like documentation generators, Git utilities, etc.) work best as global jobs. + +### Version Control Local Jobs +Local jobs are in `.deepwork/jobs/`, so they're version controlled. This is perfect for team collaboration. + +### Document Global Jobs +Since global jobs aren't in version control, document them separately or maintain a personal repository of your global jobs. + +## Examples by Role + +### For Engineers +- **Local**: `deploy_app`, `run_integration_tests`, `update_dependencies` +- **Global**: `git_summary`, `code_review`, `technical_blog_post` + +### For Product Managers +- **Local**: `product_roadmap`, `feature_spec`, `release_notes` +- **Global**: `meeting_notes`, `stakeholder_update`, `competitive_research` + +### For Data Analysts +- **Local**: `etl_pipeline`, `dashboard_update`, `model_training` +- **Global**: `data_exploration`, `report_template`, `chart_generator` + +## Getting Help + +- List all your jobs: `/job_porter.list_jobs` +- Port a job: `/job_porter.port_job` +- See this guide: `/job_porter.explain_scopes` + +--- + +*Remember*: The scope decision isn't permanent. You can always move jobs later as your needs evolve! +EOF + +echo "✓ Scope guide created: scope_guide.md" +``` + +### Step 2: Display Key Points + +Show the user the most important takeaways: + +```bash +echo "" +echo "=== Key Takeaways ===" +echo "" +echo "LOCAL JOBS (.deepwork/jobs/):" +echo " • Project-specific workflows" +echo " • Version controlled with your code" +echo " • Shared with team members" +echo " • Example: deploy_staging, run_project_tests" +echo "" +echo "GLOBAL JOBS (~/.deepwork/jobs/):" +echo " • Available across all projects" +echo " • Personal workflows and utilities" +echo " • No need to recreate in each project" +echo " • Example: git_commit_summary, write_tutorial" +echo "" +echo "RULE OF THUMB:" +echo " Start local, go global when you realize it's useful elsewhere." +echo "" +``` + +### Step 3: Offer Next Steps + +Provide actionable next steps: + +```bash +echo "=== What You Can Do Now ===" +echo "" +echo "1. Review the full guide: cat scope_guide.md" +echo "2. List your current jobs: /job_porter.list_jobs" +echo "3. Port a job if needed: /job_porter.port_job" +echo "4. When creating new jobs with /deepwork_jobs, choose the right scope" +echo "" +``` + +## Quality Criteria + +- **Comprehensive guide**: `scope_guide.md` covers all aspects of job scopes +- **Clear distinctions**: Local vs global differences are well explained +- **Practical examples**: Real-world examples for different use cases +- **Decision framework**: Clear guidance on choosing between local and global +- **Actionable**: User knows exactly what to do next +- **Role-specific examples**: Provided examples for different user types + + +### Job Context + +This job assists users in managing job scope by porting jobs between local +project-specific locations (.deepwork/jobs/) and global system-wide locations +(~/.deepwork/jobs/). It guides users through discovering available jobs, +understanding the differences between local and global scope, and safely +migrating jobs between scopes. + + + +## Work Branch + +Use branch format: `deepwork/job_porter-[instance]-YYYYMMDD` + +- If on a matching work branch: continue using it +- If on main/master: create new branch with `git checkout -b deepwork/job_porter-[instance]-$(date +%Y%m%d)` + +## Outputs + +**Required outputs**: +- `scope_guide.md` + +## On Completion + +1. Verify outputs are created +2. Inform user: "explain_scopes complete, outputs: scope_guide.md" + +This standalone command can be re-run anytime. + +--- + +**Reference files**: `.deepwork/jobs/job_porter/job.yml`, `.deepwork/jobs/job_porter/steps/explain_scopes.md` +""" \ No newline at end of file diff --git a/.gemini/skills/job_porter/index.toml b/.gemini/skills/job_porter/index.toml new file mode 100644 index 00000000..ad3cdbe4 --- /dev/null +++ b/.gemini/skills/job_porter/index.toml @@ -0,0 +1,61 @@ +# job_porter +# +# Helps users port DeepWork jobs between local and global locations +# +# Generated by DeepWork - do not edit manually + +description = "Helps users port DeepWork jobs between local and global locations" + +prompt = """ +# job_porter + +**Multi-step workflow**: Helps users port DeepWork jobs between local and global locations + +> **NOTE**: Gemini CLI requires manual command invocation. After each step, tell the user which command to run next. + +This job assists users in managing job scope by porting jobs between local +project-specific locations (.deepwork/jobs/) and global system-wide locations +(~/.deepwork/jobs/). It guides users through discovering available jobs, +understanding the differences between local and global scope, and safely +migrating jobs between scopes. + + +## Available Steps + +1. **list_jobs** - Shows all available jobs in both local and global locations with their current scope + Command: `/job_porter:list_jobs` +2. **port_job** - Moves a job between local and global scope with safety validation + Command: `/job_porter:port_job` +3. **explain_scopes** - Provides detailed explanation of local vs global jobs and when to use each + Command: `/job_porter:explain_scopes` + +## Execution Instructions + +### Step 1: Analyze Intent + +Parse any text following `/job_porter` to determine user intent: +- "list_jobs" or related terms → start at `/job_porter:list_jobs` +- "port_job" or related terms → start at `/job_porter:port_job` +- "explain_scopes" or related terms → start at `/job_porter:explain_scopes` + +### Step 2: Direct User to Starting Step + +Tell the user which command to run: +``` +/job_porter:list_jobs +``` + +### Step 3: Guide Through Workflow + +After each step completes, tell the user the next command to run until workflow is complete. + +### Handling Ambiguous Intent + +If user intent is unclear: +- Present available steps as numbered options +- Ask user to select the starting point + +## Reference + +- Job definition: `.deepwork/jobs/job_porter/job.yml` +""" \ No newline at end of file diff --git a/.gemini/skills/job_porter/list_jobs.toml b/.gemini/skills/job_porter/list_jobs.toml new file mode 100644 index 00000000..f7f7ebea --- /dev/null +++ b/.gemini/skills/job_porter/list_jobs.toml @@ -0,0 +1,156 @@ +# job_porter:list_jobs +# +# Shows all available jobs in both local and global locations with their current scope +# +# Generated by DeepWork - do not edit manually + +description = "Shows all available jobs in both local and global locations with their current scope" + +prompt = """ +# job_porter:list_jobs + +**Standalone command** - can be run anytime + +> Helps users port DeepWork jobs between local and global locations + + +## Instructions + +**Goal**: Shows all available jobs in both local and global locations with their current scope + +# List Available Jobs + +## Objective + +Display all DeepWork jobs available in both local and global locations, helping users understand what jobs they have and where they're located. + +## Task + +Discover and list all jobs from both local and global locations, presenting them in a clear, organized format. + +### Step 1: Discover Jobs from Both Locations + +Run the sync command with output to see job counts: + +```bash +deepwork sync 2>&1 | grep -A3 "Found.*job(s)" +``` + +This will show you the count of local and global jobs. + +### Step 2: List Local Jobs + +List all jobs in the local `.deepwork/jobs/` directory: + +```bash +if [ -d ".deepwork/jobs" ]; then + echo "=== LOCAL JOBS (.deepwork/jobs/) ===" + for job_dir in .deepwork/jobs/*/; do + if [ -f "${job_dir}job.yml" ]; then + job_name=$(basename "$job_dir") + # Extract version from job.yml + version=$(grep "^version:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + # Extract summary + summary=$(grep "^summary:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + echo " 📁 $job_name (v$version)" + echo " $summary" + echo "" + fi + done +else + echo "=== LOCAL JOBS ===" + echo " (none)" + echo "" +fi +``` + +### Step 3: List Global Jobs + +List all jobs in the global `~/.deepwork/jobs/` directory: + +```bash +if [ -d "$HOME/.deepwork/jobs" ]; then + echo "=== GLOBAL JOBS (~/.deepwork/jobs/) ===" + for job_dir in $HOME/.deepwork/jobs/*/; do + if [ -f "${job_dir}job.yml" ]; then + job_name=$(basename "$job_dir") + # Extract version from job.yml + version=$(grep "^version:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + # Extract summary + summary=$(grep "^summary:" "${job_dir}job.yml" | head -1 | cut -d'"' -f2 | tr -d "'") + echo " 🌍 $job_name (v$version)" + echo " $summary" + echo "" + fi + done +else + echo "=== GLOBAL JOBS ===" + echo " (none)" + echo "" +fi +``` + +### Step 4: Create Summary Output + +Save the complete list to `jobs_list.txt`: + +```bash +{ + echo "# DeepWork Jobs Inventory" + echo "Generated: $(date)" + echo "" + # Run the above discovery commands +} > jobs_list.txt +``` + +### Step 5: Provide Guidance + +Explain what the user can do next: + +1. **Port a job**: Use `/job_porter.port_job` to move a job between local and global +2. **Learn about scopes**: Use `/job_porter.explain_scopes` to understand when to use each scope +3. **Create new jobs**: When creating jobs with `/deepwork_jobs`, you'll be asked to choose the scope + +## Quality Criteria + +- **Both locations checked**: Listed jobs from both `.deepwork/jobs/` and `~/.deepwork/jobs/` +- **Clear organization**: Jobs are clearly separated by scope (local vs global) +- **Useful metadata**: Each job shows name, version, and summary +- **Visual distinction**: Used emojis or markers to distinguish local (📁) from global (🌍) jobs +- **Output file created**: `jobs_list.txt` contains the complete inventory +- **Next steps provided**: User knows how to port jobs or learn more about scopes + + +### Job Context + +This job assists users in managing job scope by porting jobs between local +project-specific locations (.deepwork/jobs/) and global system-wide locations +(~/.deepwork/jobs/). It guides users through discovering available jobs, +understanding the differences between local and global scope, and safely +migrating jobs between scopes. + + + +## Work Branch + +Use branch format: `deepwork/job_porter-[instance]-YYYYMMDD` + +- If on a matching work branch: continue using it +- If on main/master: create new branch with `git checkout -b deepwork/job_porter-[instance]-$(date +%Y%m%d)` + +## Outputs + +**Required outputs**: +- `jobs_list.txt` + +## On Completion + +1. Verify outputs are created +2. Inform user: "list_jobs complete, outputs: jobs_list.txt" + +This standalone command can be re-run anytime. + +--- + +**Reference files**: `.deepwork/jobs/job_porter/job.yml`, `.deepwork/jobs/job_porter/steps/list_jobs.md` +""" \ No newline at end of file diff --git a/.gemini/skills/job_porter/port_job.toml b/.gemini/skills/job_porter/port_job.toml new file mode 100644 index 00000000..2ba804f3 --- /dev/null +++ b/.gemini/skills/job_porter/port_job.toml @@ -0,0 +1,198 @@ +# job_porter:port_job +# +# Moves a job between local and global scope with safety validation +# +# Generated by DeepWork - do not edit manually + +description = "Moves a job between local and global scope with safety validation" + +prompt = """ +# job_porter:port_job + +**Standalone command** - can be run anytime + +> Helps users port DeepWork jobs between local and global locations + + +## Instructions + +**Goal**: Moves a job between local and global scope with safety validation + +# Port a Job + +## Objective + +Safely move a DeepWork job between local (project-specific) and global (system-wide) locations. + +## Task + +Guide the user through porting a job, asking structured questions to understand their needs and ensuring the operation is safe. + +### Step 1: Understand the Request + +Ask structured questions to determine what the user wants to do: + +1. **Which job do you want to port?** + - List available jobs from both locations (use the list_jobs step logic) + - Get the exact job name from the user + +2. **Where do you want to port it?** + - **Local** - Make it available only in this project (`.deepwork/jobs/`) + - **Global** - Make it available across all projects (`~/.deepwork/jobs/`) + +Store the job name as `{job_name}` and destination as `{destination}` (either "local" or "global"). + +### Step 2: Validate the Job Exists + +Check if the job exists in either location: + +```bash +job_name="{job_name}" + +# Check local +if [ -d ".deepwork/jobs/$job_name" ]; then + current_location="local" + echo "✓ Found '$job_name' in local location" +elif [ -d "$HOME/.deepwork/jobs/$job_name" ]; then + current_location="global" + echo "✓ Found '$job_name' in global location" +else + echo "✗ Job '$job_name' not found in local or global locations" + exit 1 +fi +``` + +### Step 3: Check if Already at Destination + +Verify the job isn't already in the target location: + +```bash +destination="{destination}" + +if [ "$current_location" = "$destination" ]; then + echo "⚠️ Job '$job_name' is already in $destination location" + echo "No action needed." + exit 0 +fi +``` + +### Step 4: Run the Port Command + +Execute the deepwork port command: + +```bash +deepwork port "$job_name" --to "$destination" +``` + +### Step 5: Verify the Port + +Confirm the job was moved successfully: + +```bash +if [ "$destination" = "local" ]; then + if [ -d ".deepwork/jobs/$job_name" ]; then + echo "✓ Job successfully ported to local location" + echo " Location: .deepwork/jobs/$job_name" + else + echo "✗ Port failed - job not found at destination" + exit 1 + fi +elif [ "$destination" = "global" ]; then + if [ -d "$HOME/.deepwork/jobs/$job_name" ]; then + echo "✓ Job successfully ported to global location" + echo " Location: ~/.deepwork/jobs/$job_name" + else + echo "✗ Port failed - job not found at destination" + exit 1 + fi +fi +``` + +### Step 6: Sync Skills + +After porting, regenerate the skills for all platforms: + +```bash +echo "" +echo "Syncing skills to reflect the change..." +deepwork sync +``` + +### Step 7: Provide Confirmation + +Create a summary output file with the results: + +```bash +{ + echo "# Job Port Complete" + echo "" + echo "**Job**: $job_name" + echo "**From**: $current_location" + echo "**To**: $destination" + echo "**Date**: $(date)" + echo "" + echo "## Next Steps" + echo "" + if [ "$destination" = "global" ]; then + echo "The job is now available in all projects with DeepWork installed." + echo "You can use it by running the skill in any project." + else + echo "The job is now available only in this project." + echo "To use it in other projects, you'll need to port it to global or recreate it." + fi +} > port_result.txt + +echo "" +echo "Summary saved to port_result.txt" +``` + +## Quality Criteria + +- **Job validated**: Confirmed the job exists before attempting to port +- **Destination verified**: Checked the job isn't already at the destination +- **Port command executed**: Used `deepwork port` to move the job +- **Success confirmed**: Verified the job exists at the new location +- **Skills synced**: Ran `deepwork sync` to regenerate skills +- **Summary created**: `port_result.txt` contains complete details of the operation +- **Clear guidance**: User understands what happened and what to do next + + +### Job Context + +This job assists users in managing job scope by porting jobs between local +project-specific locations (.deepwork/jobs/) and global system-wide locations +(~/.deepwork/jobs/). It guides users through discovering available jobs, +understanding the differences between local and global scope, and safely +migrating jobs between scopes. + + +## Required Inputs + +**User Parameters** - Gather from user before starting: +- **job_name**: Name of the job to port +- **destination**: Target scope (local or global) + + +## Work Branch + +Use branch format: `deepwork/job_porter-[instance]-YYYYMMDD` + +- If on a matching work branch: continue using it +- If on main/master: create new branch with `git checkout -b deepwork/job_porter-[instance]-$(date +%Y%m%d)` + +## Outputs + +**Required outputs**: +- `port_result.txt` + +## On Completion + +1. Verify outputs are created +2. Inform user: "port_job complete, outputs: port_result.txt" + +This standalone command can be re-run anytime. + +--- + +**Reference files**: `.deepwork/jobs/job_porter/job.yml`, `.deepwork/jobs/job_porter/steps/port_job.md` +""" \ No newline at end of file diff --git a/.gemini/skills/test_global_job/index.toml b/.gemini/skills/test_global_job/index.toml new file mode 100644 index 00000000..ff11a71e --- /dev/null +++ b/.gemini/skills/test_global_job/index.toml @@ -0,0 +1,50 @@ +# test_global_job +# +# A test global job for demonstration +# +# Generated by DeepWork - do not edit manually + +description = "A test global job for demonstration" + +prompt = """ +# test_global_job + +**Multi-step workflow**: A test global job for demonstration + +> **NOTE**: Gemini CLI requires manual command invocation. After each step, tell the user which command to run next. + +This is a test job to demonstrate global job functionality + +## Available Steps + +1. **test_step** - A simple test step + Command: `/test_global_job:test_step` + +## Execution Instructions + +### Step 1: Analyze Intent + +Parse any text following `/test_global_job` to determine user intent: +- "test_step" or related terms → start at `/test_global_job:test_step` + +### Step 2: Direct User to Starting Step + +Tell the user which command to run: +``` +/test_global_job:test_step +``` + +### Step 3: Guide Through Workflow + +After each step completes, tell the user the next command to run until workflow is complete. + +### Handling Ambiguous Intent + +If user intent is unclear: +- Present available steps as numbered options +- Ask user to select the starting point + +## Reference + +- Job definition: `.deepwork/jobs/test_global_job/job.yml` +""" \ No newline at end of file diff --git a/.gemini/skills/test_global_job/test_step.toml b/.gemini/skills/test_global_job/test_step.toml new file mode 100644 index 00000000..50a571cd --- /dev/null +++ b/.gemini/skills/test_global_job/test_step.toml @@ -0,0 +1,57 @@ +# test_global_job:test_step +# +# A simple test step +# +# Generated by DeepWork - do not edit manually + +description = "A simple test step" + +prompt = """ +# test_global_job:test_step + +**Standalone command** - can be run anytime + +> A test global job for demonstration + + +## Instructions + +**Goal**: A simple test step + +# Test Step + +This is a simple test step for the global job. + +## Task + +Just create a file called output.txt with some text in it. + + +### Job Context + +This is a test job to demonstrate global job functionality + + +## Work Branch + +Use branch format: `deepwork/test_global_job-[instance]-YYYYMMDD` + +- If on a matching work branch: continue using it +- If on main/master: create new branch with `git checkout -b deepwork/test_global_job-[instance]-$(date +%Y%m%d)` + +## Outputs + +**Required outputs**: +- `output.txt` + +## On Completion + +1. Verify outputs are created +2. Inform user: "test_step complete, outputs: output.txt" + +This standalone command can be re-run anytime. + +--- + +**Reference files**: `.deepwork/jobs/test_global_job/job.yml`, `.deepwork/jobs/test_global_job/steps/test_step.md` +""" \ No newline at end of file diff --git a/README.md b/README.md index 76a659de..d08e666d 100644 --- a/README.md +++ b/README.md @@ -195,17 +195,25 @@ Send [@tylerwillis](https://x.com/tylerwillis) a message on X. Advanced: Directory Structure ``` +# Local (project-specific) your-project/ ├── .deepwork/ │ ├── config.yml # Platform configuration │ ├── rules/ # Automated rules -│ └── jobs/ # Job definitions +│ └── jobs/ # Local job definitions │ └── job_name/ │ ├── job.yml # Job metadata │ └── steps/ # Step instructions ├── .claude/ # Generated Claude skills │ └── skills/ └── deepwork-output/ # Job outputs (gitignored) + +# Global (available across all projects) +~/.deepwork/ +└── jobs/ # Global job definitions + └── job_name/ + ├── job.yml # Job metadata + └── steps/ # Step instructions ``` @@ -255,6 +263,48 @@ See [Architecture](doc/architecture.md) for full rules documentation. +
+Advanced: Global Jobs + +DeepWork supports both local and global jobs: + +- **Local jobs** (default) - Stored in `.deepwork/jobs/` in your project. Available only in that project. +- **Global jobs** - Stored in `~/.deepwork/jobs/` on your system. Available across all projects with DeepWork installed. + +### When to use global jobs + +Use global jobs for workflows that apply across many projects: +- Generic documentation tasks +- Code review processes +- Git commit summaries +- General research workflows + +### Creating a global job + +When defining a new job with `/deepwork_jobs`, you'll be asked whether to create it locally or globally. + +You can also create global jobs directly: + +```bash +.deepwork/jobs/deepwork_jobs/make_new_job.sh my_job --global +``` + +### Porting jobs between local and global + +Move existing jobs between local and global locations: + +```bash +# Move a job to global (available in all projects) +deepwork port my_job --to global + +# Move a job to local (available only in this project) +deepwork port my_job --to local +``` + +After porting, run `deepwork sync` to regenerate skills. + +
+
Advanced: Nix Flakes diff --git a/src/deepwork/cli/main.py b/src/deepwork/cli/main.py index b503ea9a..3c935969 100644 --- a/src/deepwork/cli/main.py +++ b/src/deepwork/cli/main.py @@ -16,6 +16,7 @@ def cli() -> None: # Import commands from deepwork.cli.hook import hook # noqa: E402 from deepwork.cli.install import install # noqa: E402 +from deepwork.cli.port import port # noqa: E402 from deepwork.cli.rules import rules # noqa: E402 from deepwork.cli.sync import sync # noqa: E402 @@ -23,6 +24,7 @@ def cli() -> None: cli.add_command(sync) cli.add_command(hook) cli.add_command(rules) +cli.add_command(port) if __name__ == "__main__": diff --git a/src/deepwork/cli/port.py b/src/deepwork/cli/port.py new file mode 100644 index 00000000..26aeba25 --- /dev/null +++ b/src/deepwork/cli/port.py @@ -0,0 +1,139 @@ +"""Port command for moving jobs between local and global locations.""" + +import shutil +from pathlib import Path + +import click +from rich.console import Console + +from deepwork.utils.paths import ( + discover_all_jobs_dirs, + ensure_global_jobs_dir, + get_global_jobs_dir, + get_local_jobs_dir, + is_job_global, +) + +console = Console() + + +class PortError(Exception): + """Exception raised for port errors.""" + + pass + + +@click.command() +@click.argument("job_name", type=str) +@click.option( + "--to", + type=click.Choice(["local", "global"], case_sensitive=False), + required=True, + help="Destination: local or global", +) +@click.option( + "--path", + type=click.Path(exists=True, file_okay=False, path_type=Path), + default=".", + help="Path to project directory (default: current directory)", +) +def port(job_name: str, to: str, path: Path) -> None: + """ + Port a job between local and global locations. + + Examples: + deepwork port my_job --to global + deepwork port my_job --to local + """ + try: + _port_job(job_name, to.lower(), path) + except PortError as e: + console.print(f"[red]Error:[/red] {e}") + raise click.Abort() from e + except Exception as e: + console.print(f"[red]Unexpected error:[/red] {e}") + raise + + +def _port_job(job_name: str, destination: str, project_path: Path) -> None: + """ + Port a job between local and global locations. + + Args: + job_name: Name of the job to port + destination: Target location ("local" or "global") + project_path: Path to project directory + + Raises: + PortError: If port fails + """ + console.print(f"\n[bold cyan]Porting Job: {job_name}[/bold cyan]\n") + + # Find the job + job_dirs_with_location = discover_all_jobs_dirs(project_path) + source_job_dir = None + source_location = None + + for job_dir, location in job_dirs_with_location: + if job_dir.name == job_name: + source_job_dir = job_dir + source_location = location + break + + if source_job_dir is None: + raise PortError( + f"Job '{job_name}' not found in local or global locations.\n" + "Run 'deepwork sync' to see available jobs." + ) + + # Check if already in destination + if source_location == destination: + console.print( + f"[yellow]⚠[/yellow] Job '{job_name}' is already in {destination} location" + ) + return + + # Determine destination path + if destination == "global": + ensure_global_jobs_dir() + dest_dir = get_global_jobs_dir() / job_name + console.print(f"[yellow]→[/yellow] Moving from local to global...") + else: # destination == "local" + local_jobs_dir = get_local_jobs_dir(project_path) + local_jobs_dir.mkdir(parents=True, exist_ok=True) + dest_dir = local_jobs_dir / job_name + console.print(f"[yellow]→[/yellow] Moving from global to local...") + + # Check if destination already exists + if dest_dir.exists(): + raise PortError( + f"Job '{job_name}' already exists at destination: {dest_dir}\n" + "Please remove or rename the existing job first." + ) + + # Copy the job directory + try: + shutil.copytree(source_job_dir, dest_dir) + console.print(f" [green]✓[/green] Copied to {dest_dir}") + except Exception as e: + raise PortError(f"Failed to copy job directory: {e}") from e + + # Remove the source directory + try: + shutil.rmtree(source_job_dir) + console.print(f" [green]✓[/green] Removed from {source_job_dir}") + except Exception as e: + # Try to clean up the destination if we can't remove source + try: + shutil.rmtree(dest_dir) + except Exception: + pass + raise PortError(f"Failed to remove source directory: {e}") from e + + # Success message + console.print() + console.print(f"[bold green]✓ Job '{job_name}' ported to {destination}![/bold green]") + console.print() + console.print("[bold]Next steps:[/bold]") + console.print(" • Run [cyan]deepwork sync[/cyan] to regenerate skills") + console.print() diff --git a/src/deepwork/cli/sync.py b/src/deepwork/cli/sync.py index 03c47a30..1d887479 100644 --- a/src/deepwork/cli/sync.py +++ b/src/deepwork/cli/sync.py @@ -11,6 +11,7 @@ from deepwork.core.hooks_syncer import collect_job_hooks, sync_hooks_to_platform from deepwork.core.parser import parse_job_definition from deepwork.utils.fs import ensure_dir +from deepwork.utils.paths import discover_all_jobs_dirs from deepwork.utils.yaml_utils import load_yaml console = Console() @@ -80,14 +81,19 @@ def sync_skills(project_path: Path) -> None: console.print("[bold cyan]Syncing DeepWork Skills[/bold cyan]\n") - # Discover jobs - jobs_dir = deepwork_dir / "jobs" - if not jobs_dir.exists(): - job_dirs = [] - else: - job_dirs = [d for d in jobs_dir.iterdir() if d.is_dir() and (d / "job.yml").exists()] + # Discover jobs from both local and global locations + job_dirs_with_location = discover_all_jobs_dirs(project_path) + job_dirs = [job_dir for job_dir, _ in job_dirs_with_location] + # Report discovered jobs by location + local_count = sum(1 for _, loc in job_dirs_with_location if loc == "local") + global_count = sum(1 for _, loc in job_dirs_with_location if loc == "global") + console.print(f"[yellow]→[/yellow] Found {len(job_dirs)} job(s) to sync") + if local_count > 0: + console.print(f" [dim]•[/dim] {local_count} local job(s)") + if global_count > 0: + console.print(f" [dim]•[/dim] {global_count} global job(s)") # Parse all jobs jobs = [] @@ -109,8 +115,8 @@ def sync_skills(project_path: Path) -> None: console.print(f" • {job_name}: {error}") raise SyncError(f"Failed to parse {len(failed_jobs)} job(s)") - # Collect hooks from all jobs - job_hooks_list = collect_job_hooks(jobs_dir) + # Collect hooks from all jobs (both local and global) + job_hooks_list = collect_job_hooks(project_path) if job_hooks_list: console.print(f"[yellow]→[/yellow] Found {len(job_hooks_list)} job(s) with hooks") diff --git a/src/deepwork/core/hooks_syncer.py b/src/deepwork/core/hooks_syncer.py index 35a01036..67c55a0d 100644 --- a/src/deepwork/core/hooks_syncer.py +++ b/src/deepwork/core/hooks_syncer.py @@ -7,6 +7,7 @@ import yaml from deepwork.core.adapters import AgentAdapter +from deepwork.utils.paths import discover_all_jobs_dirs class HooksSyncError(Exception): @@ -119,24 +120,22 @@ def from_job_dir(cls, job_dir: Path) -> "JobHooks | None": ) -def collect_job_hooks(jobs_dir: Path) -> list[JobHooks]: +def collect_job_hooks(project_path: Path) -> list[JobHooks]: """ - Collect hooks from all jobs in the jobs directory. + Collect hooks from all jobs in both local and global locations. Args: - jobs_dir: Path to .deepwork/jobs directory + project_path: Path to project root Returns: List of JobHooks for all jobs with hooks defined """ - if not jobs_dir.exists(): - return [] - job_hooks_list = [] - for job_dir in jobs_dir.iterdir(): - if not job_dir.is_dir(): - continue - + + # Discover jobs from both local and global locations + job_dirs_with_location = discover_all_jobs_dirs(project_path) + + for job_dir, _ in job_dirs_with_location: job_hooks = JobHooks.from_job_dir(job_dir) if job_hooks: job_hooks_list.append(job_hooks) diff --git a/src/deepwork/standard_jobs/deepwork_jobs/make_new_job.sh b/src/deepwork/standard_jobs/deepwork_jobs/make_new_job.sh index c561d6d2..3fe05237 100755 --- a/src/deepwork/standard_jobs/deepwork_jobs/make_new_job.sh +++ b/src/deepwork/standard_jobs/deepwork_jobs/make_new_job.sh @@ -37,24 +37,38 @@ validate_job_name() { # Main script main() { if [[ $# -lt 1 ]]; then - echo "Usage: $0 " + echo "Usage: $0 [--global]" echo "" echo "Creates the directory structure for a new DeepWork job." echo "" echo "Arguments:" echo " job_name Name of the job (lowercase, underscores allowed)" + echo " --global Create the job in global location (~/.deepwork/jobs)" echo "" echo "Example:" echo " $0 competitive_research" + echo " $0 competitive_research --global" exit 1 fi local job_name="$1" + local is_global=false + + # Check for --global flag + if [[ $# -gt 1 && "$2" == "--global" ]]; then + is_global=true + fi + validate_job_name "$job_name" - # Determine the base path - look for .deepwork directory + # Determine the base path local base_path - if [[ -d ".deepwork/jobs" ]]; then + if [[ "$is_global" == true ]]; then + # Create in global location + base_path="$HOME/.deepwork/jobs" + mkdir -p "$base_path" + info "Creating global job in $base_path" + elif [[ -d ".deepwork/jobs" ]]; then base_path=".deepwork/jobs" elif [[ -d "../.deepwork/jobs" ]]; then base_path="../.deepwork/jobs" diff --git a/src/deepwork/standard_jobs/deepwork_jobs/steps/define.md b/src/deepwork/standard_jobs/deepwork_jobs/steps/define.md index 31de7440..65958efe 100644 --- a/src/deepwork/standard_jobs/deepwork_jobs/steps/define.md +++ b/src/deepwork/standard_jobs/deepwork_jobs/steps/define.md @@ -12,6 +12,17 @@ Guide the user through defining a job specification by asking structured questio The output of this step is **only** the `job.yml` file - a complete specification of the workflow. The actual step instruction files will be created in the next step (`implement`). +### Step 0.5: Determine Job Scope (Local or Global) + +Before starting the workflow definition, ask the user where they want this job to be installed: + +**Ask structured questions:** +- "Where would you like this job to be installed?" + - **Local** - Available only in this project (stored in `.deepwork/jobs/`) + - **Global** - Available across all projects with DeepWork installed (stored in `~/.deepwork/jobs/`) + +**Store this decision** to use later when creating the job directory. Most users will want local jobs (project-specific workflows), but global jobs are useful for workflows that apply across many projects (e.g., generic documentation tasks, code review processes). + ### Step 1: Understand the Job Purpose Start by asking structured questions to understand what the user wants to accomplish: diff --git a/src/deepwork/standard_jobs/deepwork_jobs/steps/implement.md b/src/deepwork/standard_jobs/deepwork_jobs/steps/implement.md index 749c8c6f..bb0e052d 100644 --- a/src/deepwork/standard_jobs/deepwork_jobs/steps/implement.md +++ b/src/deepwork/standard_jobs/deepwork_jobs/steps/implement.md @@ -10,23 +10,29 @@ Read the `job.yml` specification file and create all the necessary files to make ### Step 1: Create Directory Structure Using Script +**Important**: If the user chose **global** scope during the define step, add the `--global` flag when running the script. + Run the `make_new_job.sh` script to create the standard directory structure: ```bash +# For local jobs (default) .deepwork/jobs/deepwork_jobs/make_new_job.sh [job_name] + +# For global jobs (if user chose global scope) +.deepwork/jobs/deepwork_jobs/make_new_job.sh [job_name] --global ``` This creates: -- `.deepwork/jobs/[job_name]/` - Main job directory -- `.deepwork/jobs/[job_name]/steps/` - Step instruction files -- `.deepwork/jobs/[job_name]/hooks/` - Custom validation scripts (with .gitkeep) -- `.deepwork/jobs/[job_name]/templates/` - Example file formats (with .gitkeep) -- `.deepwork/jobs/[job_name]/AGENTS.md` - Job management guidance +- `[location]/[job_name]/` - Main job directory (either `.deepwork/jobs/` or `~/.deepwork/jobs/`) +- `[location]/[job_name]/steps/` - Step instruction files +- `[location]/[job_name]/hooks/` - Custom validation scripts (with .gitkeep) +- `[location]/[job_name]/templates/` - Example file formats (with .gitkeep) +- `[location]/[job_name]/AGENTS.md` - Job management guidance **Note**: If the directory already exists (e.g., job.yml was created by define step), you can skip this step or manually create the additional directories: ```bash -mkdir -p .deepwork/jobs/[job_name]/hooks .deepwork/jobs/[job_name]/templates -touch .deepwork/jobs/[job_name]/hooks/.gitkeep .deepwork/jobs/[job_name]/templates/.gitkeep +mkdir -p [location]/[job_name]/hooks [location]/[job_name]/templates +touch [location]/[job_name]/hooks/.gitkeep [location]/[job_name]/templates/.gitkeep ``` ### Step 2: Read and Validate the Specification diff --git a/src/deepwork/utils/paths.py b/src/deepwork/utils/paths.py new file mode 100644 index 00000000..c8d08155 --- /dev/null +++ b/src/deepwork/utils/paths.py @@ -0,0 +1,108 @@ +"""Utilities for managing local and global job paths.""" + +from pathlib import Path + + +def get_global_deepwork_dir() -> Path: + """ + Get the global DeepWork directory path. + + Returns: + Path to ~/.deepwork directory + """ + return Path.home() / ".deepwork" + + +def get_global_jobs_dir() -> Path: + """ + Get the global jobs directory path. + + Returns: + Path to ~/.deepwork/jobs directory + """ + return get_global_deepwork_dir() / "jobs" + + +def get_local_deepwork_dir(project_path: Path) -> Path: + """ + Get the local DeepWork directory path for a project. + + Args: + project_path: Path to project root + + Returns: + Path to .deepwork directory in project + """ + return project_path / ".deepwork" + + +def get_local_jobs_dir(project_path: Path) -> Path: + """ + Get the local jobs directory path for a project. + + Args: + project_path: Path to project root + + Returns: + Path to .deepwork/jobs directory in project + """ + return get_local_deepwork_dir(project_path) / "jobs" + + +def discover_all_jobs_dirs(project_path: Path) -> list[tuple[Path, str]]: + """ + Discover all job directories from both local and global locations. + + Args: + project_path: Path to project root + + Returns: + List of tuples (job_dir_path, location_type) where location_type is "local" or "global" + """ + job_dirs: list[tuple[Path, str]] = [] + + # Check local jobs + local_jobs = get_local_jobs_dir(project_path) + if local_jobs.exists(): + for job_dir in local_jobs.iterdir(): + if job_dir.is_dir() and (job_dir / "job.yml").exists(): + job_dirs.append((job_dir, "local")) + + # Check global jobs + global_jobs = get_global_jobs_dir() + if global_jobs.exists(): + for job_dir in global_jobs.iterdir(): + if job_dir.is_dir() and (job_dir / "job.yml").exists(): + job_dirs.append((job_dir, "global")) + + return job_dirs + + +def is_job_global(job_path: Path) -> bool: + """ + Check if a job is in the global location. + + Args: + job_path: Path to job directory + + Returns: + True if job is in global location, False otherwise + """ + global_jobs = get_global_jobs_dir() + try: + job_path.relative_to(global_jobs) + return True + except ValueError: + return False + + +def ensure_global_jobs_dir() -> Path: + """ + Ensure the global jobs directory exists. + + Returns: + Path to global jobs directory + """ + global_jobs = get_global_jobs_dir() + global_jobs.mkdir(parents=True, exist_ok=True) + return global_jobs diff --git a/tests/integration/test_port_command.py b/tests/integration/test_port_command.py new file mode 100644 index 00000000..e1591bb7 --- /dev/null +++ b/tests/integration/test_port_command.py @@ -0,0 +1,186 @@ +"""Integration tests for the port command.""" + +import shutil +import tempfile +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from deepwork.cli.port import port +from deepwork.utils.paths import get_global_jobs_dir, get_local_jobs_dir + + +@pytest.fixture +def temp_project(monkeypatch): + """Create a temporary project with DeepWork structure.""" + with tempfile.TemporaryDirectory() as tmpdir: + project_path = Path(tmpdir) / "project" + project_path.mkdir() + + # Create .deepwork/jobs directory + local_jobs = get_local_jobs_dir(project_path) + local_jobs.mkdir(parents=True) + + # Set up fake home for global jobs + fake_home = Path(tmpdir) / "fake_home" + fake_home.mkdir() + monkeypatch.setattr(Path, "home", lambda: fake_home) + + yield project_path + + +def create_test_job(jobs_dir: Path, job_name: str) -> Path: + """Create a test job in the specified jobs directory.""" + job_dir = jobs_dir / job_name + job_dir.mkdir() + + # Create job.yml + (job_dir / "job.yml").write_text( + f"""name: {job_name} +version: 1.0.0 +summary: Test job +steps: + - id: test_step + name: Test Step + description: A test step + instructions_file: steps/test_step.md + outputs: + - output.txt +""" + ) + + # Create steps directory and file + steps_dir = job_dir / "steps" + steps_dir.mkdir() + (steps_dir / "test_step.md").write_text("# Test Step\n\nThis is a test step.") + + return job_dir + + +def test_port_local_to_global(temp_project): + """Test porting a job from local to global.""" + runner = CliRunner() + + # Create a local job + local_jobs = get_local_jobs_dir(temp_project) + create_test_job(local_jobs, "test_job") + + # Port to global + result = runner.invoke(port, ["test_job", "--to", "global", "--path", str(temp_project)]) + + assert result.exit_code == 0 + assert "ported to global" in result.output.lower() + + # Verify job exists in global location + global_jobs = get_global_jobs_dir() + global_job = global_jobs / "test_job" + assert global_job.exists() + assert (global_job / "job.yml").exists() + assert (global_job / "steps" / "test_step.md").exists() + + # Verify job removed from local location + local_job = local_jobs / "test_job" + assert not local_job.exists() + + +def test_port_global_to_local(temp_project): + """Test porting a job from global to local.""" + runner = CliRunner() + + # Create a global job + global_jobs = get_global_jobs_dir() + global_jobs.mkdir(parents=True, exist_ok=True) + create_test_job(global_jobs, "global_job") + + # Port to local + result = runner.invoke(port, ["global_job", "--to", "local", "--path", str(temp_project)]) + + assert result.exit_code == 0 + assert "ported to local" in result.output.lower() + + # Verify job exists in local location + local_jobs = get_local_jobs_dir(temp_project) + local_job = local_jobs / "global_job" + assert local_job.exists() + assert (local_job / "job.yml").exists() + assert (local_job / "steps" / "test_step.md").exists() + + # Verify job removed from global location + global_job = global_jobs / "global_job" + assert not global_job.exists() + + +def test_port_job_not_found(temp_project): + """Test porting a non-existent job.""" + runner = CliRunner() + + result = runner.invoke(port, ["nonexistent_job", "--to", "global", "--path", str(temp_project)]) + + assert result.exit_code != 0 + assert "not found" in result.output.lower() + + +def test_port_already_at_destination(temp_project): + """Test porting a job that's already at the destination.""" + runner = CliRunner() + + # Create a local job + local_jobs = get_local_jobs_dir(temp_project) + create_test_job(local_jobs, "local_job") + + # Try to port to local (where it already is) + result = runner.invoke(port, ["local_job", "--to", "local", "--path", str(temp_project)]) + + assert result.exit_code == 0 + assert "already in local" in result.output.lower() + + +def test_port_destination_exists(temp_project): + """Test porting when destination already has a job with the same name.""" + runner = CliRunner() + + # Create a local job + local_jobs = get_local_jobs_dir(temp_project) + create_test_job(local_jobs, "duplicate_job") + + # Create a global job with the same name + global_jobs = get_global_jobs_dir() + global_jobs.mkdir(parents=True, exist_ok=True) + create_test_job(global_jobs, "duplicate_job") + + # Try to port local to global + result = runner.invoke(port, ["duplicate_job", "--to", "global", "--path", str(temp_project)]) + + assert result.exit_code != 0 + assert "already exists at destination" in result.output.lower() + + +def test_port_preserves_job_structure(temp_project): + """Test that porting preserves the complete job structure.""" + runner = CliRunner() + + # Create a job with multiple files + local_jobs = get_local_jobs_dir(temp_project) + job_dir = create_test_job(local_jobs, "complex_job") + + # Add additional files + (job_dir / "hooks").mkdir() + (job_dir / "hooks" / "validate.sh").write_text("#!/bin/bash\necho 'validate'") + (job_dir / "templates").mkdir() + (job_dir / "templates" / "template.md").write_text("# Template") + (job_dir / "AGENTS.md").write_text("# Agents\nGuidance here") + + # Port to global + result = runner.invoke(port, ["complex_job", "--to", "global", "--path", str(temp_project)]) + + assert result.exit_code == 0 + + # Verify all files were copied + global_jobs = get_global_jobs_dir() + global_job = global_jobs / "complex_job" + assert (global_job / "job.yml").exists() + assert (global_job / "steps" / "test_step.md").exists() + assert (global_job / "hooks" / "validate.sh").exists() + assert (global_job / "templates" / "template.md").exists() + assert (global_job / "AGENTS.md").exists() diff --git a/tests/unit/test_hooks_syncer.py b/tests/unit/test_hooks_syncer.py index 99edcfdb..fdd8387f 100644 --- a/tests/unit/test_hooks_syncer.py +++ b/tests/unit/test_hooks_syncer.py @@ -142,23 +142,28 @@ class TestCollectJobHooks: def test_collects_hooks_from_multiple_jobs(self, temp_dir: Path) -> None: """Test collecting hooks from multiple job directories.""" - jobs_dir = temp_dir / "jobs" + project_path = temp_dir / "project" + project_path.mkdir() + jobs_dir = project_path / ".deepwork" / "jobs" # Create first job with hooks job1_dir = jobs_dir / "job1" (job1_dir / "hooks").mkdir(parents=True) (job1_dir / "hooks" / "global_hooks.yml").write_text("Stop:\n - hook1.sh\n") + (job1_dir / "job.yml").write_text("name: job1\nversion: 1.0.0\n") # Create second job with hooks job2_dir = jobs_dir / "job2" (job2_dir / "hooks").mkdir(parents=True) (job2_dir / "hooks" / "global_hooks.yml").write_text("Stop:\n - hook2.sh\n") + (job2_dir / "job.yml").write_text("name: job2\nversion: 1.0.0\n") - # Create job without hooks + # Create job without hooks (but with job.yml) job3_dir = jobs_dir / "job3" job3_dir.mkdir(parents=True) + (job3_dir / "job.yml").write_text("name: job3\nversion: 1.0.0\n") - result = collect_job_hooks(jobs_dir) + result = collect_job_hooks(project_path) assert len(result) == 2 job_names = {jh.job_name for jh in result} @@ -166,8 +171,8 @@ def test_collects_hooks_from_multiple_jobs(self, temp_dir: Path) -> None: def test_returns_empty_for_nonexistent_dir(self, temp_dir: Path) -> None: """Test returns empty list when jobs dir doesn't exist.""" - jobs_dir = temp_dir / "nonexistent" - result = collect_job_hooks(jobs_dir) + project_path = temp_dir / "nonexistent" + result = collect_job_hooks(project_path) assert result == [] diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py new file mode 100644 index 00000000..a5b9a044 --- /dev/null +++ b/tests/unit/test_paths.py @@ -0,0 +1,194 @@ +"""Unit tests for paths utility module.""" + +import tempfile +from pathlib import Path + +import pytest + +from deepwork.utils.paths import ( + discover_all_jobs_dirs, + ensure_global_jobs_dir, + get_global_deepwork_dir, + get_global_jobs_dir, + get_local_deepwork_dir, + get_local_jobs_dir, + is_job_global, +) + + +def test_get_global_deepwork_dir(): + """Test getting the global DeepWork directory path.""" + global_dir = get_global_deepwork_dir() + assert global_dir == Path.home() / ".deepwork" + + +def test_get_global_jobs_dir(): + """Test getting the global jobs directory path.""" + global_jobs = get_global_jobs_dir() + assert global_jobs == Path.home() / ".deepwork" / "jobs" + + +def test_get_local_deepwork_dir(): + """Test getting the local DeepWork directory path.""" + project_path = Path("/tmp/test_project") + local_dir = get_local_deepwork_dir(project_path) + assert local_dir == project_path / ".deepwork" + + +def test_get_local_jobs_dir(): + """Test getting the local jobs directory path.""" + project_path = Path("/tmp/test_project") + local_jobs = get_local_jobs_dir(project_path) + assert local_jobs == project_path / ".deepwork" / "jobs" + + +def test_discover_all_jobs_dirs_empty(): + """Test discovering jobs when no jobs exist.""" + with tempfile.TemporaryDirectory() as tmpdir: + project_path = Path(tmpdir) + job_dirs = discover_all_jobs_dirs(project_path) + assert job_dirs == [] + + +def test_discover_all_jobs_dirs_local_only(): + """Test discovering local jobs only.""" + with tempfile.TemporaryDirectory() as tmpdir: + project_path = Path(tmpdir) + local_jobs = get_local_jobs_dir(project_path) + local_jobs.mkdir(parents=True) + + # Create a test job + test_job = local_jobs / "test_job" + test_job.mkdir() + (test_job / "job.yml").write_text("name: test_job\nversion: 1.0.0") + + job_dirs = discover_all_jobs_dirs(project_path) + assert len(job_dirs) == 1 + assert job_dirs[0][0] == test_job + assert job_dirs[0][1] == "local" + + +def test_discover_all_jobs_dirs_global_only(monkeypatch): + """Test discovering global jobs only.""" + with tempfile.TemporaryDirectory() as tmpdir: + project_path = Path(tmpdir) + fake_home = Path(tmpdir) / "fake_home" + fake_home.mkdir() + + # Monkeypatch Path.home() to return our fake home + monkeypatch.setattr(Path, "home", lambda: fake_home) + + global_jobs = get_global_jobs_dir() + global_jobs.mkdir(parents=True) + + # Create a test job + test_job = global_jobs / "global_test_job" + test_job.mkdir() + (test_job / "job.yml").write_text("name: global_test_job\nversion: 1.0.0") + + job_dirs = discover_all_jobs_dirs(project_path) + assert len(job_dirs) == 1 + assert job_dirs[0][0] == test_job + assert job_dirs[0][1] == "global" + + +def test_discover_all_jobs_dirs_both(monkeypatch): + """Test discovering both local and global jobs.""" + with tempfile.TemporaryDirectory() as tmpdir: + project_path = Path(tmpdir) + fake_home = Path(tmpdir) / "fake_home" + fake_home.mkdir() + + # Monkeypatch Path.home() to return our fake home + monkeypatch.setattr(Path, "home", lambda: fake_home) + + # Create local job + local_jobs = get_local_jobs_dir(project_path) + local_jobs.mkdir(parents=True) + local_job = local_jobs / "local_job" + local_job.mkdir() + (local_job / "job.yml").write_text("name: local_job\nversion: 1.0.0") + + # Create global job + global_jobs = get_global_jobs_dir() + global_jobs.mkdir(parents=True) + global_job = global_jobs / "global_job" + global_job.mkdir() + (global_job / "job.yml").write_text("name: global_job\nversion: 1.0.0") + + job_dirs = discover_all_jobs_dirs(project_path) + assert len(job_dirs) == 2 + + # Check that we have one local and one global + locations = [loc for _, loc in job_dirs] + assert "local" in locations + assert "global" in locations + + +def test_discover_all_jobs_dirs_ignores_non_jobs(): + """Test that discovery ignores directories without job.yml.""" + with tempfile.TemporaryDirectory() as tmpdir: + project_path = Path(tmpdir) + local_jobs = get_local_jobs_dir(project_path) + local_jobs.mkdir(parents=True) + + # Create a directory without job.yml + (local_jobs / "not_a_job").mkdir() + + # Create a valid job + valid_job = local_jobs / "valid_job" + valid_job.mkdir() + (valid_job / "job.yml").write_text("name: valid_job\nversion: 1.0.0") + + job_dirs = discover_all_jobs_dirs(project_path) + assert len(job_dirs) == 1 + assert job_dirs[0][0].name == "valid_job" + + +def test_is_job_global(monkeypatch): + """Test checking if a job is global.""" + with tempfile.TemporaryDirectory() as tmpdir: + fake_home = Path(tmpdir) / "fake_home" + fake_home.mkdir() + + # Monkeypatch Path.home() to return our fake home + monkeypatch.setattr(Path, "home", lambda: fake_home) + + # Create paths + global_jobs = get_global_jobs_dir() + global_jobs.mkdir(parents=True) + global_job = global_jobs / "global_job" + global_job.mkdir() + + local_job = Path(tmpdir) / ".deepwork" / "jobs" / "local_job" + local_job.mkdir(parents=True) + + # Test global job + assert is_job_global(global_job) is True + + # Test local job + assert is_job_global(local_job) is False + + +def test_ensure_global_jobs_dir(monkeypatch): + """Test ensuring the global jobs directory exists.""" + with tempfile.TemporaryDirectory() as tmpdir: + fake_home = Path(tmpdir) / "fake_home" + fake_home.mkdir() + + # Monkeypatch Path.home() to return our fake home + monkeypatch.setattr(Path, "home", lambda: fake_home) + + global_jobs = get_global_jobs_dir() + assert not global_jobs.exists() + + # Ensure it's created + result = ensure_global_jobs_dir() + assert result == global_jobs + assert global_jobs.exists() + assert global_jobs.is_dir() + + # Calling again should be idempotent + result2 = ensure_global_jobs_dir() + assert result2 == global_jobs + assert global_jobs.exists()