Skip to content

Commit 7daa06d

Browse files
committed
fix(core): reject explicit execution of a composite task (#958)
Flagged in PR review — the last gap in the "no execution path filters on is_leaf" class this issue named. The bulk selectors now filter is_leaf, but they are all *status-based*. `cf work start <id> --execute` and the v2 execute route with an explicit task_ids list both call start_task_run directly with an ID, bypassing every one of those filters, and would walk a composite BACKLOG -> READY -> IN_PROGRESS and hand it to an engine. Guarded at start_task_run rather than at each caller: it is the chokepoint both explicit paths funnel through, and the error names the fix ("run its child tasks instead") rather than just refusing.
1 parent e0a7982 commit 7daa06d

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

codeframe/core/runtime.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,26 @@ def start_task_run(workspace: Workspace, task_id: str) -> Run:
7676
Created Run
7777
7878
Raises:
79-
ValueError: If task not found
79+
ValueError: If task not found, or if it is a composite (#958)
8080
InvalidTransitionError: If task can't transition to IN_PROGRESS
8181
"""
8282
# Get the task
8383
task = tasks.get(workspace, task_id)
8484
if not task:
8585
raise ValueError(f"Task not found: {task_id}")
8686

87+
# Composites are containers, not work (#958). The bulk selectors filter
88+
# is_leaf, but this is the *explicit* path — `cf work start <id>` and the
89+
# v2 execute route with an explicit task_ids list both land here, so
90+
# without this guard a composite's ID still reaches an engine. Rejecting at
91+
# this chokepoint covers every explicit caller at once.
92+
if not task.is_leaf:
93+
raise ValueError(
94+
f"Task {task_id} is a composite (a container for subtasks), not "
95+
"executable work. Run its child tasks instead — its status is "
96+
"rolled up from theirs."
97+
)
98+
8799
# Check if there's already an active run
88100
active = get_active_run(workspace, task_id)
89101
if active:

tests/core/test_inert_orchestration_958.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,30 @@ def test_an_idle_composite_does_not_block_new_assignments(self, tmp_path):
265265
assert result.executing_count == 0
266266
assert result.can_assign, result.reason
267267

268+
def test_starting_a_composite_explicitly_is_rejected(self, tmp_path):
269+
"""Bulk selectors filter is_leaf; the explicit path needs its own guard.
270+
271+
`cf work start <composite-id>` and the v2 execute route with an
272+
explicit task_ids list both reach start_task_run directly, bypassing
273+
every status-based filter.
274+
"""
275+
from codeframe.core import runtime, tasks
276+
from codeframe.core.tasks import TaskStatus
277+
from codeframe.core.workspace import create_or_load_workspace
278+
279+
ws = create_or_load_workspace(tmp_path)
280+
parent, child, _ = self._tree(ws)
281+
282+
with pytest.raises(ValueError, match="composite"):
283+
runtime.start_task_run(ws, parent.id)
284+
285+
# No run was created, and the task was not dragged toward IN_PROGRESS.
286+
assert runtime.get_active_run(ws, parent.id) is None
287+
assert tasks.get(ws, parent.id).status == TaskStatus.BACKLOG
288+
289+
# A leaf still starts normally.
290+
assert runtime.start_task_run(ws, child.id) is not None
291+
268292
def test_get_ready_task_ids_is_not_capped_at_100(self, tmp_path):
269293
"""The v2 API's run-all-ready route must not silently run a subset."""
270294
from codeframe.core import runtime, tasks

0 commit comments

Comments
 (0)