diff --git a/src/deepwork/jobs/discovery.py b/src/deepwork/jobs/discovery.py index fe67d387..43285239 100644 --- a/src/deepwork/jobs/discovery.py +++ b/src/deepwork/jobs/discovery.py @@ -8,6 +8,12 @@ Additional directories can be appended via the ``DEEPWORK_ADDITIONAL_JOBS_FOLDERS`` environment variable, which accepts a **colon-delimited** list of absolute paths. + +When ``DEEPWORK_DEV`` is set (to any non-empty value), the search order is changed +so that additional folders are searched *before* the project-local and standard-jobs +directories. This ensures that a developer working on shared library jobs will have +edits (e.g. from ``deepwork_jobs/learn``) applied to the library checkout rather than +to the local ``.deepwork/jobs/`` copy. """ from __future__ import annotations @@ -34,31 +40,51 @@ class JobLoadError: # Environment variable for additional job folders (colon-delimited) ENV_ADDITIONAL_JOBS_FOLDERS = "DEEPWORK_ADDITIONAL_JOBS_FOLDERS" +# Environment variable to enable developer mode (any non-empty value) +# When set, additional job folders are searched *before* the project-local and +# standard-jobs directories so that edits land in the shared library checkout. +ENV_DEV = "DEEPWORK_DEV" + # Location of built-in standard jobs inside the package _STANDARD_JOBS_DIR = Path(__file__).parent.parent / "standard_jobs" +def _parse_additional_folders() -> list[Path]: + """Parse DEEPWORK_ADDITIONAL_JOBS_FOLDERS into a list of Paths.""" + extra = os.environ.get(ENV_ADDITIONAL_JOBS_FOLDERS, "") + folders: list[Path] = [] + if extra: + for entry in extra.split(":"): + entry = entry.strip() + if entry: + folders.append(Path(entry)) + return folders + + def get_job_folders(project_root: Path) -> list[Path]: """Return the ordered list of directories to scan for job definitions. The order determines priority when the same job name appears in multiple folders – the first directory that contains a matching job wins. + When ``DEEPWORK_DEV`` is set, additional folders (from + ``DEEPWORK_ADDITIONAL_JOBS_FOLDERS``) are placed **first** so that developer + edits (e.g. from the ``learn`` workflow) are applied to the shared library + checkout rather than to the project-local copy. + Returns: List of directory paths (may include non-existent paths which callers should skip). """ - folders: list[Path] = [ - project_root / ".deepwork" / "jobs", - _STANDARD_JOBS_DIR, - ] - - extra = os.environ.get(ENV_ADDITIONAL_JOBS_FOLDERS, "") - if extra: - for entry in extra.split(":"): - entry = entry.strip() - if entry: - folders.append(Path(entry)) + local_folder = project_root / ".deepwork" / "jobs" + additional = _parse_additional_folders() + + if os.environ.get(ENV_DEV): + # Dev mode: additional folders take priority over local/standard so that + # learning/editing targets the shared library checkout. + folders: list[Path] = additional + [local_folder, _STANDARD_JOBS_DIR] + else: + folders = [local_folder, _STANDARD_JOBS_DIR] + additional return folders diff --git a/src/deepwork/standard_jobs/deepwork_jobs/AGENTS.md b/src/deepwork/standard_jobs/deepwork_jobs/AGENTS.md index 83f7e191..2d2df5e6 100644 --- a/src/deepwork/standard_jobs/deepwork_jobs/AGENTS.md +++ b/src/deepwork/standard_jobs/deepwork_jobs/AGENTS.md @@ -93,7 +93,21 @@ Key design decisions: - Designed for keystone development mode where `~/.keystone/*/deepwork/library/jobs/` is the additional folder - Quality criteria "External Repo Handled" auto-passes for local jobs +### DEEPWORK_DEV: Developer Mode Job Targeting (v1.8.0) + +When `DEEPWORK_DEV` is set (any non-empty value), the job discovery system changes the folder search order so that `DEEPWORK_ADDITIONAL_JOBS_FOLDERS` paths are searched *before* the project-local `.deepwork/jobs/` and standard-jobs directories. + +This means: +- `start_workflow` returns `job_dir` pointing to the shared library checkout (not the local copy) +- The `learn` workflow automatically updates files in the external library without any extra agent logic +- `DEEPWORK_DEV` is typically set alongside `DEEPWORK_ADDITIONAL_JOBS_FOLDERS` in `flake.nix` or shell init + +Key design decisions: +- Implemented entirely in `src/deepwork/jobs/discovery.py` (`get_job_folders`) +- Agents receive the correct `job_dir` transparently — no special learn-step logic required +- When `DEEPWORK_DEV` is set but no additional folders are configured, the default order is preserved + ## Last Updated -- Date: 2026-03-23 -- From conversation about: Adding DEEPWORK_ADDITIONAL_JOBS_FOLDERS awareness to the learn workflow +- Date: 2026-03-25 +- From conversation about: Adding DEEPWORK_DEV env var for developer-mode job targeting diff --git a/src/deepwork/standard_jobs/deepwork_jobs/steps/learn.md b/src/deepwork/standard_jobs/deepwork_jobs/steps/learn.md index 804167b5..0606b92b 100644 --- a/src/deepwork/standard_jobs/deepwork_jobs/steps/learn.md +++ b/src/deepwork/standard_jobs/deepwork_jobs/steps/learn.md @@ -20,6 +20,7 @@ Analyze the conversation history to extract learnings and improvements, then app 2. **Locate the job directory using `job_dir`** - The MCP server returns `job_dir` (absolute path) when starting workflows — use this as the authoritative location - The job may live in `.deepwork/jobs/`, `src/deepwork/standard_jobs/`, or an **external folder** via `DEEPWORK_ADDITIONAL_JOBS_FOLDERS` + - When `DEEPWORK_DEV` is set, the MCP server automatically returns the path in `DEEPWORK_ADDITIONAL_JOBS_FOLDERS` (if the job exists there) as `job_dir`, so no special handling is needed in most cases - Check if `job_dir` is inside the current project's git repo or in a **separate git repository** (e.g. a library checkout at `~/.keystone/*/deepwork/library/jobs/`) - If `job_dir` is in a different git repo, note this — you'll need to handle commits/pushes separately in Step 8 diff --git a/tests/unit/jobs/test_discovery.py b/tests/unit/jobs/test_discovery.py index 64c52184..17ae64bc 100644 --- a/tests/unit/jobs/test_discovery.py +++ b/tests/unit/jobs/test_discovery.py @@ -6,6 +6,7 @@ from deepwork.jobs.discovery import ( ENV_ADDITIONAL_JOBS_FOLDERS, + ENV_DEV, find_job_dir, get_job_folders, load_all_jobs, @@ -91,6 +92,30 @@ def test_env_var_strips_whitespace( assert Path("/extra/a") in folders assert Path("/extra/b") in folders + def test_dev_mode_places_additional_folders_first( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv(ENV_DEV, "1") + monkeypatch.setenv(ENV_ADDITIONAL_JOBS_FOLDERS, "/extra/a:/extra/b") + folders = get_job_folders(tmp_path) + # Additional folders must come before local and standard + idx_extra_a = folders.index(Path("/extra/a")) + idx_extra_b = folders.index(Path("/extra/b")) + idx_local = folders.index(tmp_path / ".deepwork" / "jobs") + assert idx_extra_a < idx_local + assert idx_extra_b < idx_local + + def test_dev_mode_without_additional_folders_preserves_defaults( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + from deepwork.jobs.discovery import _STANDARD_JOBS_DIR + + monkeypatch.setenv(ENV_DEV, "1") + monkeypatch.delenv(ENV_ADDITIONAL_JOBS_FOLDERS, raising=False) + folders = get_job_folders(tmp_path) + assert tmp_path / ".deepwork" / "jobs" in folders + assert _STANDARD_JOBS_DIR in folders + class TestLoadAllJobs: """Tests for load_all_jobs.""" @@ -238,3 +263,16 @@ def test_prefers_first_folder_on_duplicate( ) result = find_job_dir(tmp_path, "dup") assert result == folder_a / "dup" + + def test_dev_mode_prefers_additional_folder_over_local( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + local_jobs = tmp_path / ".deepwork" / "jobs" + extra_jobs = tmp_path / "library_jobs" + _create_minimal_job(local_jobs, "my_job") + _create_minimal_job(extra_jobs, "my_job") + monkeypatch.setenv(ENV_DEV, "1") + monkeypatch.setenv(ENV_ADDITIONAL_JOBS_FOLDERS, str(extra_jobs)) + # In dev mode the extra folder should win over the local copy + result = find_job_dir(tmp_path, "my_job") + assert result == extra_jobs / "my_job"