tasktree 0.0.8__tar.gz → 0.0.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {tasktree-0.0.8 → tasktree-0.0.10}/PKG-INFO +92 -1
  2. {tasktree-0.0.8 → tasktree-0.0.10}/README.md +91 -0
  3. {tasktree-0.0.8 → tasktree-0.0.10}/pyproject.toml +1 -1
  4. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/cli.py +54 -6
  5. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/docker.py +17 -9
  6. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/executor.py +85 -12
  7. tasktree-0.0.10/src/tasktree/graph.py +266 -0
  8. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/hasher.py +46 -2
  9. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/parser.py +280 -15
  10. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/substitution.py +5 -2
  11. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_cli_options.py +1 -1
  12. tasktree-0.0.10/tests/integration/test_docker_build_args.py +79 -0
  13. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_end_to_end.py +3 -3
  14. tasktree-0.0.10/tests/integration/test_parameterized_dependencies.yaml +27 -0
  15. tasktree-0.0.10/tests/integration/test_parameterized_deps_execution.py +191 -0
  16. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_variables.py +1 -1
  17. tasktree-0.0.10/tests/unit/test_dependency_parsing.py +154 -0
  18. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_docker.py +159 -0
  19. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_executor.py +15 -15
  20. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_graph.py +9 -7
  21. tasktree-0.0.10/tests/unit/test_list_formatting.py +342 -0
  22. tasktree-0.0.10/tests/unit/test_parameterized_graph.py +155 -0
  23. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_parser.py +67 -0
  24. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_substitution.py +2 -2
  25. tasktree-0.0.8/src/tasktree/graph.py +0 -168
  26. {tasktree-0.0.8 → tasktree-0.0.10}/.claude/settings.local.json +0 -0
  27. {tasktree-0.0.8 → tasktree-0.0.10}/.github/workflows/claude-code-review.yml +0 -0
  28. {tasktree-0.0.8 → tasktree-0.0.10}/.github/workflows/claude.yml +0 -0
  29. {tasktree-0.0.8 → tasktree-0.0.10}/.github/workflows/release.yml +0 -0
  30. {tasktree-0.0.8 → tasktree-0.0.10}/.github/workflows/test.yml +0 -0
  31. {tasktree-0.0.8 → tasktree-0.0.10}/.gitignore +0 -0
  32. {tasktree-0.0.8 → tasktree-0.0.10}/.python-version +0 -0
  33. {tasktree-0.0.8 → tasktree-0.0.10}/CLAUDE.md +0 -0
  34. {tasktree-0.0.8 → tasktree-0.0.10}/example/source.txt +0 -0
  35. {tasktree-0.0.8 → tasktree-0.0.10}/example/tasktree.yaml +0 -0
  36. {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/01-basic-variables.md +0 -0
  37. {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/02-env-variable-type.md +0 -0
  38. {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/03-direct-env-substitution.md +0 -0
  39. {tasktree-0.0.8/requirements → tasktree-0.0.10/requirements/implemented}/04-file-read-variables.md +0 -0
  40. {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/bug-report-dependency-triggering.md +0 -0
  41. {tasktree-0.0.8/requirements/future → tasktree-0.0.10/requirements/implemented}/docker-task-environments.md +0 -0
  42. {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/shell-environment-requirements.md +0 -0
  43. {tasktree-0.0.8 → tasktree-0.0.10}/schema/README.md +0 -0
  44. {tasktree-0.0.8 → tasktree-0.0.10}/schema/tasktree-schema.json +0 -0
  45. {tasktree-0.0.8 → tasktree-0.0.10}/schema/vscode-settings-snippet.json +0 -0
  46. {tasktree-0.0.8 → tasktree-0.0.10}/src/__init__.py +0 -0
  47. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/__init__.py +0 -0
  48. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/state.py +0 -0
  49. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/tasks.py +0 -0
  50. {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/types.py +0 -0
  51. {tasktree-0.0.8 → tasktree-0.0.10}/tasktree.yaml +0 -0
  52. {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/__init__.py +0 -0
  53. {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/test_docker_basic.py +0 -0
  54. {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/test_docker_environment.py +0 -0
  55. {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/test_docker_ownership.py +0 -0
  56. {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/test_docker_volumes.py +0 -0
  57. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_arg_choices.py +0 -0
  58. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_arg_min_max.py +0 -0
  59. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_builtin_variables.py +0 -0
  60. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_clean_state.py +0 -0
  61. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_dependency_execution.py +0 -0
  62. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_exported_args.py +0 -0
  63. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_input_detection.py +0 -0
  64. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_missing_outputs.py +0 -0
  65. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_nested_imports.py +0 -0
  66. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_state_persistence.py +0 -0
  67. {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_working_directory.py +0 -0
  68. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_cli.py +0 -0
  69. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_environment_tracking.py +0 -0
  70. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_hasher.py +0 -0
  71. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_state.py +0 -0
  72. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_tasks.py +0 -0
  73. {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_types.py +0 -0
  74. {tasktree-0.0.8 → tasktree-0.0.10}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.8
3
+ Version: 0.0.10
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -594,6 +594,97 @@ If an exported argument with a default isn't available as an environment variabl
594
594
  2. The CLI automatically applies defaults before execution
595
595
  3. You can explicitly provide the value: `tt deploy prod-server port=8080`
596
596
 
597
+ ### Parameterized Dependencies
598
+
599
+ Dependencies can invoke tasks with specific arguments, enabling flexible and reusable task graphs:
600
+
601
+ **Syntax:**
602
+
603
+ ```yaml
604
+ tasks:
605
+ # Task with parameters
606
+ process:
607
+ args: [mode, verbose=false]
608
+ cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
609
+
610
+ # Simple dependency (uses defaults)
611
+ consumer1:
612
+ deps: [process] # Equivalent to: process(mode must be provided)
613
+ cmd: echo "done"
614
+
615
+ # Positional arguments
616
+ consumer2:
617
+ deps:
618
+ - process: [debug, true] # Maps to: mode=debug, verbose=true
619
+ cmd: echo "done"
620
+
621
+ # Named arguments
622
+ consumer3:
623
+ deps:
624
+ - process: {mode: release, verbose: false}
625
+ cmd: echo "done"
626
+
627
+ # Multiple invocations with different args
628
+ multi_build:
629
+ deps:
630
+ - process: [debug]
631
+ - process: [release]
632
+ cmd: echo "All builds complete"
633
+ ```
634
+
635
+ **Key behaviors:**
636
+
637
+ - **Simple string form** (`- task_name`): Uses task defaults for all arguments. Required arguments must have defaults or task invocation fails.
638
+ - **Positional form** (`- task_name: [arg1, arg2]`): Arguments mapped by position. Can omit trailing args if they have defaults.
639
+ - **Named form** (`- task_name: {arg1: val1}`): Arguments mapped by name. Can omit any arg with a default.
640
+ - **Multiple invocations**: Same task with different arguments creates separate graph nodes, each executing independently.
641
+ - **Normalization**: All forms normalized to named arguments with defaults filled before execution.
642
+ - **Cache separation**: `process(debug)` and `process(release)` cache independently.
643
+
644
+ **Restrictions:**
645
+
646
+ - **No empty lists**: `- task: []` is invalid (use `- task` instead)
647
+ - **No mixed positional and named**: Choose one form per dependency
648
+ - **Single-key dicts**: `{task1: [x], task2: [y]}` is invalid (multi-key not allowed)
649
+
650
+ **Validation:**
651
+
652
+ Validation happens at graph construction time with clear error messages:
653
+
654
+ ```
655
+ Task 'process' takes 2 arguments, got 3
656
+ Task 'build' has no argument named 'mode'
657
+ Task 'deploy' requires argument 'environment' (no default provided)
658
+ ```
659
+
660
+ **Example use cases:**
661
+
662
+ ```yaml
663
+ tasks:
664
+ # Compile for different platforms
665
+ compile:
666
+ args: [target]
667
+ cmd: cargo build --target {{arg.target}}
668
+
669
+ dist:
670
+ deps:
671
+ - compile: [x86_64-unknown-linux-gnu]
672
+ - compile: [aarch64-unknown-linux-gnu]
673
+ cmd: tar czf dist.tar.gz target/*/release/app
674
+
675
+ # Run tests with different configurations
676
+ test:
677
+ args: [config]
678
+ cmd: pytest --config={{arg.config}}
679
+
680
+ ci:
681
+ deps:
682
+ - test: [unit]
683
+ - test: [integration]
684
+ - test: [e2e]
685
+ cmd: echo "All tests passed"
686
+ ```
687
+
597
688
  ## Environment Variables
598
689
 
599
690
  Task Tree supports reading environment variables in two ways:
@@ -579,6 +579,97 @@ If an exported argument with a default isn't available as an environment variabl
579
579
  2. The CLI automatically applies defaults before execution
580
580
  3. You can explicitly provide the value: `tt deploy prod-server port=8080`
581
581
 
582
+ ### Parameterized Dependencies
583
+
584
+ Dependencies can invoke tasks with specific arguments, enabling flexible and reusable task graphs:
585
+
586
+ **Syntax:**
587
+
588
+ ```yaml
589
+ tasks:
590
+ # Task with parameters
591
+ process:
592
+ args: [mode, verbose=false]
593
+ cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
594
+
595
+ # Simple dependency (uses defaults)
596
+ consumer1:
597
+ deps: [process] # Equivalent to: process(mode must be provided)
598
+ cmd: echo "done"
599
+
600
+ # Positional arguments
601
+ consumer2:
602
+ deps:
603
+ - process: [debug, true] # Maps to: mode=debug, verbose=true
604
+ cmd: echo "done"
605
+
606
+ # Named arguments
607
+ consumer3:
608
+ deps:
609
+ - process: {mode: release, verbose: false}
610
+ cmd: echo "done"
611
+
612
+ # Multiple invocations with different args
613
+ multi_build:
614
+ deps:
615
+ - process: [debug]
616
+ - process: [release]
617
+ cmd: echo "All builds complete"
618
+ ```
619
+
620
+ **Key behaviors:**
621
+
622
+ - **Simple string form** (`- task_name`): Uses task defaults for all arguments. Required arguments must have defaults or task invocation fails.
623
+ - **Positional form** (`- task_name: [arg1, arg2]`): Arguments mapped by position. Can omit trailing args if they have defaults.
624
+ - **Named form** (`- task_name: {arg1: val1}`): Arguments mapped by name. Can omit any arg with a default.
625
+ - **Multiple invocations**: Same task with different arguments creates separate graph nodes, each executing independently.
626
+ - **Normalization**: All forms normalized to named arguments with defaults filled before execution.
627
+ - **Cache separation**: `process(debug)` and `process(release)` cache independently.
628
+
629
+ **Restrictions:**
630
+
631
+ - **No empty lists**: `- task: []` is invalid (use `- task` instead)
632
+ - **No mixed positional and named**: Choose one form per dependency
633
+ - **Single-key dicts**: `{task1: [x], task2: [y]}` is invalid (multi-key not allowed)
634
+
635
+ **Validation:**
636
+
637
+ Validation happens at graph construction time with clear error messages:
638
+
639
+ ```
640
+ Task 'process' takes 2 arguments, got 3
641
+ Task 'build' has no argument named 'mode'
642
+ Task 'deploy' requires argument 'environment' (no default provided)
643
+ ```
644
+
645
+ **Example use cases:**
646
+
647
+ ```yaml
648
+ tasks:
649
+ # Compile for different platforms
650
+ compile:
651
+ args: [target]
652
+ cmd: cargo build --target {{arg.target}}
653
+
654
+ dist:
655
+ deps:
656
+ - compile: [x86_64-unknown-linux-gnu]
657
+ - compile: [aarch64-unknown-linux-gnu]
658
+ cmd: tar czf dist.tar.gz target/*/release/app
659
+
660
+ # Run tests with different configurations
661
+ test:
662
+ args: [config]
663
+ cmd: pytest --config={{arg.config}}
664
+
665
+ ci:
666
+ deps:
667
+ - test: [unit]
668
+ - test: [integration]
669
+ - test: [e2e]
670
+ cmd: echo "All tests passed"
671
+ ```
672
+
582
673
  ## Environment Variables
583
674
 
584
675
  Task Tree supports reading environment variables in two ways:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tasktree"
3
- version = "0.0.8"
3
+ version = "0.0.10"
4
4
  description = "A task automation tool with incremental execution"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -26,6 +26,41 @@ app = typer.Typer(
26
26
  console = Console()
27
27
 
28
28
 
29
+ def _format_task_arguments(arg_specs: list[str | dict]) -> str:
30
+ """Format task arguments for display in list output.
31
+
32
+ Args:
33
+ arg_specs: List of argument specifications from task definition (strings or dicts)
34
+
35
+ Returns:
36
+ Formatted string showing arguments with types and defaults
37
+
38
+ Examples:
39
+ ["mode", "target"] -> "mode:str target:str"
40
+ ["mode=debug", "target=x86_64"] -> "mode:str [=debug] target:str [=x86_64]"
41
+ ["port:int", "debug:bool=false"] -> "port:int debug:bool [=false]"
42
+ [{"timeout": {"type": "int", "default": 30}}] -> "timeout:int [=30]"
43
+ """
44
+ if not arg_specs:
45
+ return ""
46
+
47
+ formatted_parts = []
48
+ for spec_str in arg_specs:
49
+ parsed = parse_arg_spec(spec_str)
50
+
51
+ # Format: name:type or name:type [=default]
52
+ # Argument names in normal intensity, types and defaults in dim
53
+ arg_part = f"{parsed.name}[dim]:{parsed.arg_type}[/dim]"
54
+
55
+ if parsed.default is not None:
56
+ # Use dim styling for the default value part
57
+ arg_part += f" [dim]\\[={parsed.default}][/dim]"
58
+
59
+ formatted_parts.append(arg_part)
60
+
61
+ return " ".join(formatted_parts)
62
+
63
+
29
64
  def _list_tasks(tasks_file: Optional[str] = None):
30
65
  """List all available tasks with descriptions."""
31
66
  recipe = _get_recipe(tasks_file)
@@ -33,14 +68,27 @@ def _list_tasks(tasks_file: Optional[str] = None):
33
68
  console.print("[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]")
34
69
  raise typer.Exit(1)
35
70
 
36
- table = Table(title="Available Tasks")
37
- table.add_column("Task", style="cyan", no_wrap=True)
38
- table.add_column("Description", style="white")
71
+ # Calculate maximum task name length for fixed-width column
72
+ max_task_name_len = max(len(name) for name in recipe.task_names()) if recipe.task_names() else 0
73
+
74
+ # Create borderless table with three columns
75
+ table = Table(show_edge=False, show_header=False, box=None, padding=(0, 2))
76
+
77
+ # Command column: fixed width to accommodate longest task name
78
+ table.add_column("Command", style="bold cyan", no_wrap=True, width=max_task_name_len)
79
+
80
+ # Arguments column: allow wrapping with sensible max width
81
+ table.add_column("Arguments", style="white", max_width=60)
82
+
83
+ # Description column: allow wrapping with sensible max width
84
+ table.add_column("Description", style="white", max_width=80)
39
85
 
40
86
  for task_name in sorted(recipe.task_names()):
41
87
  task = recipe.get_task(task_name)
42
88
  desc = task.desc if task else ""
43
- table.add_row(task_name, desc)
89
+ args_formatted = _format_task_arguments(task.args) if task else ""
90
+
91
+ table.add_row(task_name, args_formatted, desc)
44
92
 
45
93
  console.print(table)
46
94
 
@@ -351,9 +399,9 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
351
399
  state.load()
352
400
  executor = Executor(recipe, state)
353
401
 
354
- # Prune state before execution (compute hashes with effective environment)
402
+ # Prune state before execution (compute hashes with effective environment and dependencies)
355
403
  valid_hashes = {
356
- hash_task(t.cmd, t.outputs, t.working_dir, t.args, executor._get_effective_env_name(t))
404
+ hash_task(t.cmd, t.outputs, t.working_dir, t.args, executor._get_effective_env_name(t), t.deps)
357
405
  for t in recipe.tasks.values()
358
406
  }
359
407
  state.prune(valid_hashes)
@@ -87,16 +87,24 @@ class DockerManager:
87
87
 
88
88
  # Build the image
89
89
  try:
90
+ docker_build_cmd = [
91
+ "docker",
92
+ "build",
93
+ "-t",
94
+ image_tag,
95
+ "-f",
96
+ str(dockerfile_path),
97
+ ]
98
+
99
+ # Add build args if environment has them (docker environments use dict for args)
100
+ if isinstance(env.args, dict):
101
+ for arg_name, arg_value in env.args.items():
102
+ docker_build_cmd.extend(["--build-arg", f"{arg_name}={arg_value}"])
103
+
104
+ docker_build_cmd.append(str(context_path))
105
+
90
106
  subprocess.run(
91
- [
92
- "docker",
93
- "build",
94
- "-t",
95
- image_tag,
96
- "-f",
97
- str(dockerfile_path),
98
- str(context_path),
99
- ],
107
+ docker_build_cmd,
100
108
  check=True,
101
109
  capture_output=False, # Show build output to user
102
110
  )
@@ -64,6 +64,62 @@ class Executor:
64
64
  self.state = state_manager
65
65
  self.docker_manager = docker_module.DockerManager(recipe.project_root)
66
66
 
67
+ def _has_regular_args(self, task: Task) -> bool:
68
+ """Check if a task has any regular (non-exported) arguments.
69
+
70
+ Args:
71
+ task: Task to check
72
+
73
+ Returns:
74
+ True if task has at least one regular (non-exported) argument, False otherwise
75
+ """
76
+ if not task.args:
77
+ return False
78
+
79
+ # Check if any arg is not exported (doesn't start with $)
80
+ for arg_spec in task.args:
81
+ # Handle both string and dict arg specs
82
+ if isinstance(arg_spec, str):
83
+ # Remove default value part if present
84
+ arg_name = arg_spec.split('=')[0].split(':')[0].strip()
85
+ if not arg_name.startswith('$'):
86
+ return True
87
+ elif isinstance(arg_spec, dict):
88
+ # Dict format: { argname: { ... } } or { $argname: { ... } }
89
+ for key in arg_spec.keys():
90
+ if not key.startswith('$'):
91
+ return True
92
+
93
+ return False
94
+
95
+ def _filter_regular_args(self, task: Task, task_args: dict[str, Any]) -> dict[str, Any]:
96
+ """Filter task_args to only include regular (non-exported) arguments.
97
+
98
+ Args:
99
+ task: Task definition
100
+ task_args: Dictionary of all task arguments
101
+
102
+ Returns:
103
+ Dictionary containing only regular (non-exported) arguments
104
+ """
105
+ if not task.args or not task_args:
106
+ return {}
107
+
108
+ # Build set of exported arg names (without the $ prefix)
109
+ exported_names = set()
110
+ for arg_spec in task.args:
111
+ if isinstance(arg_spec, str):
112
+ arg_name = arg_spec.split('=')[0].split(':')[0].strip()
113
+ if arg_name.startswith('$'):
114
+ exported_names.add(arg_name[1:]) # Remove $ prefix
115
+ elif isinstance(arg_spec, dict):
116
+ for key in arg_spec.keys():
117
+ if key.startswith('$'):
118
+ exported_names.add(key[1:]) # Remove $ prefix
119
+
120
+ # Filter out exported args
121
+ return {k: v for k, v in task_args.items() if k not in exported_names}
122
+
67
123
  def _collect_early_builtin_variables(self, task: Task, timestamp: datetime) -> dict[str, str]:
68
124
  """Collect built-in variables that don't depend on working_dir.
69
125
 
@@ -277,9 +333,9 @@ class Executor:
277
333
  reason="forced",
278
334
  )
279
335
 
280
- # Compute hashes (include effective environment)
336
+ # Compute hashes (include effective environment and dependencies)
281
337
  effective_env = self._get_effective_env_name(task)
282
- task_hash = hash_task(task.cmd, task.outputs, task.working_dir, task.args, effective_env)
338
+ task_hash = hash_task(task.cmd, task.outputs, task.working_dir, task.args, effective_env, task.deps)
283
339
  args_hash = hash_args(args_dict) if args_dict else None
284
340
  cache_key = make_cache_key(task_hash, args_hash)
285
341
 
@@ -372,22 +428,39 @@ class Executor:
372
428
  # Resolve execution order
373
429
  if only:
374
430
  # Only execute the target task, skip dependencies
375
- execution_order = [task_name]
431
+ execution_order = [(task_name, args_dict)]
376
432
  else:
377
433
  # Execute task and all dependencies
378
- execution_order = resolve_execution_order(self.recipe, task_name)
434
+ execution_order = resolve_execution_order(self.recipe, task_name, args_dict)
379
435
 
380
436
  # Single phase: Check and execute incrementally
381
437
  statuses: dict[str, TaskStatus] = {}
382
- for name in execution_order:
438
+ for name, task_args in execution_order:
383
439
  task = self.recipe.tasks[name]
384
440
 
385
- # Determine task-specific args (only for target task)
386
- task_args = args_dict if name == task_name else {}
441
+ # Convert None to {} for internal use (None is used to distinguish simple deps in graph)
442
+ args_dict_for_execution = task_args if task_args is not None else {}
387
443
 
388
444
  # Check if task needs to run (based on CURRENT filesystem state)
389
- status = self.check_task_status(task, task_args, force=force)
390
- statuses[name] = status
445
+ status = self.check_task_status(task, args_dict_for_execution, force=force)
446
+
447
+ # Use a key that includes args for status tracking
448
+ # Only include regular (non-exported) args in status key for parameterized dependencies
449
+ # For the root task (invoked from CLI), status key is always just the task name
450
+ # For dependencies with parameterized invocations, include the regular args
451
+ is_root_task = (name == task_name)
452
+ if not is_root_task and args_dict_for_execution and self._has_regular_args(task):
453
+ import json
454
+ # Filter to only include regular (non-exported) args
455
+ regular_args = self._filter_regular_args(task, args_dict_for_execution)
456
+ if regular_args:
457
+ args_str = json.dumps(regular_args, sort_keys=True, separators=(",", ":"))
458
+ status_key = f"{name}({args_str})"
459
+ else:
460
+ status_key = name
461
+ else:
462
+ status_key = name
463
+ statuses[status_key] = status
391
464
 
392
465
  # Execute immediately if needed
393
466
  if status.will_run:
@@ -399,7 +472,7 @@ class Executor:
399
472
  file=sys.stderr,
400
473
  )
401
474
 
402
- self._run_task(task, task_args)
475
+ self._run_task(task, args_dict_for_execution)
403
476
 
404
477
  return statuses
405
478
 
@@ -962,9 +1035,9 @@ class Executor:
962
1035
  task: Task that was executed
963
1036
  args_dict: Arguments used for execution
964
1037
  """
965
- # Compute hashes (include effective environment)
1038
+ # Compute hashes (include effective environment and dependencies)
966
1039
  effective_env = self._get_effective_env_name(task)
967
- task_hash = hash_task(task.cmd, task.outputs, task.working_dir, task.args, effective_env)
1040
+ task_hash = hash_task(task.cmd, task.outputs, task.working_dir, task.args, effective_env, task.deps)
968
1041
  args_hash = hash_args(args_dict) if args_dict else None
969
1042
  cache_key = make_cache_key(task_hash, args_hash)
970
1043