Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 37 additions & 11 deletions src/deepwork/jobs/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
18 changes: 16 additions & 2 deletions src/deepwork/standard_jobs/deepwork_jobs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/deepwork/standard_jobs/deepwork_jobs/steps/learn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions tests/unit/jobs/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from deepwork.jobs.discovery import (
ENV_ADDITIONAL_JOBS_FOLDERS,
ENV_DEV,
find_job_dir,
get_job_folders,
load_all_jobs,
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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"