Skip to content
Merged
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
85 changes: 85 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Validate

on:
pull_request:
branches: ["*"]

permissions:
contents: write

jobs:
format:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: uv sync --extra dev

- name: Run ruff formatting
run: uv run ruff format src/ tests/

- name: Run ruff linting with auto-fix
run: uv run ruff check --fix src/ tests/

- name: Re-run formatting after auto-fix
run: uv run ruff format src/ tests/

- name: Verify all issues are fixed
run: |
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/

- name: Check for changes
id: check_changes
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi

- name: Commit and push formatting changes
if: steps.check_changes.outputs.has_changes == 'true'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add -A
git commit -m "style: auto-format code with ruff"
git push

tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: uv sync --extra dev

- name: Run tests
run: uv run pytest tests/ -v
2 changes: 2 additions & 0 deletions src/deepwork/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"__author__",
]


# Lazy imports to avoid circular dependencies and missing modules during development
def __getattr__(name: str) -> object:
"""Lazy import for core modules."""
Expand All @@ -19,5 +20,6 @@ def __getattr__(name: str) -> object:
StepInput,
parse_job_definition,
)

return locals()[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
11 changes: 3 additions & 8 deletions src/deepwork/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ class InstallError(Exception):
pass


def _inject_standard_job(
job_name: str, jobs_dir: Path, project_path: Path
) -> None:
def _inject_standard_job(job_name: str, jobs_dir: Path, project_path: Path) -> None:
"""
Inject a standard job definition into the project.

Expand Down Expand Up @@ -54,8 +52,7 @@ def _inject_standard_job(

shutil.copytree(standard_jobs_dir, target_dir)
console.print(
f" [green]✓[/green] Installed {job_name} "
f"({target_dir.relative_to(project_path)})"
f" [green]✓[/green] Installed {job_name} ({target_dir.relative_to(project_path)})"
)
except Exception as e:
raise InstallError(f"Failed to install {job_name}: {e}") from e
Expand Down Expand Up @@ -250,9 +247,7 @@ def _install_deepwork(platform_name: str | None, project_path: Path) -> None:
# Add platform if not already present
if platform_to_add not in config_data["platforms"]:
config_data["platforms"].append(platform_to_add)
console.print(
f" [green]✓[/green] Added {platform_config.display_name} to platforms"
)
console.print(f" [green]✓[/green] Added {platform_config.display_name} to platforms")
else:
console.print(f" [dim]•[/dim] {platform_config.display_name} already configured")

Expand Down
4 changes: 1 addition & 3 deletions src/deepwork/cli/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,7 @@ def sync_commands(project_path: Path) -> None:
if job_hooks_list:
console.print(" [dim]•[/dim] Syncing hooks...")
try:
hooks_count = sync_hooks_to_platform(
project_path, platform_config, job_hooks_list
)
hooks_count = sync_hooks_to_platform(project_path, platform_config, job_hooks_list)
stats["hooks"] += hooks_count
if hooks_count > 0:
console.print(f" [green]✓[/green] Synced {hooks_count} hook(s)")
Expand Down
13 changes: 3 additions & 10 deletions src/deepwork/core/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,7 @@ def _build_step_context(
instructions_file = job.job_dir / step.instructions_file
instructions_content = safe_read(instructions_file)
if instructions_content is None:
raise GeneratorError(
f"Step instructions file not found: {instructions_file}"
)
raise GeneratorError(f"Step instructions file not found: {instructions_file}")

# Separate user inputs and file inputs
user_inputs = [
Expand Down Expand Up @@ -142,9 +140,7 @@ def _build_step_context(
prompt_file_path = job.job_dir / hook.prompt_file
prompt_content = safe_read(prompt_file_path)
if prompt_content is None:
raise GeneratorError(
f"Stop hook prompt file not found: {prompt_file_path}"
)
raise GeneratorError(f"Stop hook prompt file not found: {prompt_file_path}")
hook_ctx["content"] = prompt_content
elif hook.is_script():
hook_ctx["type"] = "script"
Expand Down Expand Up @@ -203,9 +199,7 @@ def generate_step_command(

# Find step index
try:
step_index = next(
i for i, s in enumerate(job.steps) if s.id == step.id
)
step_index = next(i for i, s in enumerate(job.steps) if s.id == step.id)
except StopIteration as e:
raise GeneratorError(f"Step '{step.id}' not found in job '{job.name}'") from e

Expand Down Expand Up @@ -262,4 +256,3 @@ def generate_all_commands(
command_paths.append(command_path)

return command_paths

1 change: 0 additions & 1 deletion src/deepwork/core/hooks_syncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import yaml

from deepwork.core.detector import PlatformConfig
from deepwork.utils.yaml_utils import load_yaml


class HooksSyncError(Exception):
Expand Down
4 changes: 1 addition & 3 deletions src/deepwork/core/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,7 @@ def validate_dependencies(self) -> None:
for step in self.steps:
for dep_id in step.dependencies:
if dep_id not in step_ids:
raise ParseError(
f"Step '{step.id}' depends on non-existent step '{dep_id}'"
)
raise ParseError(f"Step '{step.id}' depends on non-existent step '{dep_id}'")

# Check for circular dependencies using topological sort
visited = set()
Expand Down
2 changes: 1 addition & 1 deletion src/deepwork/hooks/evaluate_policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def format_policy_message(policies: list) -> str:
lines = ["## DeepWork Policies Triggered", ""]
lines.append(
"Comply with the following policies. "
"To mark a policy as addressed, include `<promise policy=\"Policy Name\">addressed</promise>` "
'To mark a policy as addressed, include `<promise policy="Policy Name">addressed</promise>` '
"in your response."
)
lines.append("")
Expand Down
4 changes: 1 addition & 3 deletions src/deepwork/utils/fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ def safe_read(path: Path | str) -> str | None:
return path_obj.read_text(encoding="utf-8")


def copy_dir(
src: Path | str, dst: Path | str, ignore_patterns: list[str] | None = None
) -> None:
def copy_dir(src: Path | str, dst: Path | str, ignore_patterns: list[str] | None = None) -> None:
"""
Recursively copy directory, optionally ignoring patterns.

Expand Down
18 changes: 5 additions & 13 deletions tests/integration/test_full_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
class TestJobWorkflow:
"""Integration tests for complete job workflow."""

def test_parse_and_generate_workflow(
self, fixtures_dir: Path, temp_dir: Path
) -> None:
def test_parse_and_generate_workflow(self, fixtures_dir: Path, temp_dir: Path) -> None:
"""Test complete workflow: parse job → generate commands."""
# Step 1: Parse job definition
job_dir = fixtures_dir / "jobs" / "complex_job"
Expand Down Expand Up @@ -40,7 +38,7 @@ def test_parse_and_generate_workflow(
assert f"# {job.name}.{job.steps[i].id}" in content

# Check step numbers
assert f"Step {i+1} of 4" in content
assert f"Step {i + 1} of 4" in content

def test_simple_job_workflow(self, fixtures_dir: Path, temp_dir: Path) -> None:
"""Test workflow with simple single-step job."""
Expand Down Expand Up @@ -68,9 +66,7 @@ def test_simple_job_workflow(self, fixtures_dir: Path, temp_dir: Path) -> None:
assert "input_param" in content
assert "Command Complete" in content # Standalone completion message

def test_command_generation_with_dependencies(
self, fixtures_dir: Path, temp_dir: Path
) -> None:
def test_command_generation_with_dependencies(self, fixtures_dir: Path, temp_dir: Path) -> None:
"""Test that generated commands properly handle dependencies."""
job_dir = fixtures_dir / "jobs" / "complex_job"
job = parse_job_definition(job_dir)
Expand Down Expand Up @@ -99,9 +95,7 @@ def test_command_generation_with_dependencies(
assert "## Workflow Complete" in step4_content
assert "## Next Step" not in step4_content

def test_command_generation_with_file_inputs(
self, fixtures_dir: Path, temp_dir: Path
) -> None:
def test_command_generation_with_file_inputs(self, fixtures_dir: Path, temp_dir: Path) -> None:
"""Test that generated commands properly handle file inputs."""
job_dir = fixtures_dir / "jobs" / "complex_job"
job = parse_job_definition(job_dir)
Expand All @@ -125,9 +119,7 @@ def test_command_generation_with_file_inputs(
assert "primary_research.md" in step4_content
assert "secondary_research.md" in step4_content

def test_command_generation_with_user_inputs(
self, fixtures_dir: Path, temp_dir: Path
) -> None:
def test_command_generation_with_user_inputs(self, fixtures_dir: Path, temp_dir: Path) -> None:
"""Test that generated commands properly handle user parameter inputs."""
job_dir = fixtures_dir / "jobs" / "complex_job"
job = parse_job_definition(job_dir)
Expand Down
22 changes: 11 additions & 11 deletions tests/integration/test_install_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ def test_install_with_claude(self, mock_claude_project: Path) -> None:
runner = CliRunner()

result = runner.invoke(
cli, ["install", "--platform", "claude", "--path", str(mock_claude_project)],
catch_exceptions=False
cli,
["install", "--platform", "claude", "--path", str(mock_claude_project)],
catch_exceptions=False,
)

assert result.exit_code == 0
Expand Down Expand Up @@ -54,8 +55,7 @@ def test_install_with_auto_detect(self, mock_claude_project: Path) -> None:
runner = CliRunner()

result = runner.invoke(
cli, ["install", "--path", str(mock_claude_project)],
catch_exceptions=False
cli, ["install", "--path", str(mock_claude_project)], catch_exceptions=False
)

assert result.exit_code == 0
Expand Down Expand Up @@ -101,9 +101,7 @@ def test_install_fails_with_multiple_platforms(self, temp_dir: Path) -> None:
assert result.exit_code != 0
assert "Multiple AI platforms detected" in result.output

def test_install_with_specified_platform_when_missing(
self, mock_git_repo: Path
) -> None:
def test_install_with_specified_platform_when_missing(self, mock_git_repo: Path) -> None:
"""Test that install fails when specified platform is not present."""
runner = CliRunner()

Expand All @@ -121,15 +119,17 @@ def test_install_is_idempotent(self, mock_claude_project: Path) -> None:

# First install
result1 = runner.invoke(
cli, ["install", "--platform", "claude", "--path", str(mock_claude_project)],
catch_exceptions=False
cli,
["install", "--platform", "claude", "--path", str(mock_claude_project)],
catch_exceptions=False,
)
assert result1.exit_code == 0

# Second install
result2 = runner.invoke(
cli, ["install", "--platform", "claude", "--path", str(mock_claude_project)],
catch_exceptions=False
cli,
["install", "--platform", "claude", "--path", str(mock_claude_project)],
catch_exceptions=False,
)
assert result2.exit_code == 0

Expand Down
8 changes: 3 additions & 5 deletions tests/unit/test_evaluate_policies.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""Tests for the hooks evaluate_policies module."""

import pytest

from deepwork.hooks.evaluate_policies import extract_promise_tags, format_policy_message
from deepwork.core.policy_parser import Policy
from deepwork.hooks.evaluate_policies import extract_promise_tags, format_policy_message


class TestExtractPromiseTags:
Expand Down Expand Up @@ -45,11 +43,11 @@ def test_returns_empty_set_for_no_promises(self) -> None:

def test_multiline_promise_content(self) -> None:
"""Test promise tag with multiline content."""
text = '''<promise policy="Complex Policy">
text = """<promise policy="Complex Policy">
I have addressed this by:
1. Updating the docs
2. Running tests
</promise>'''
</promise>"""
result = extract_promise_tags(text)
assert result == {"Complex Policy"}

Expand Down
Loading