Skip to content

Commit 1a15a27

Browse files
authored
feat(tasks): richer task generation with recursive decomposition (#420)
## Summary - Enhanced LLM task generation prompt to populate complexity_score, estimated_hours, uncertainty_level, depends_on, files_to_modify - Added recursive decomposition engine: classify → decompose → recurse (inspired by tinyagi/fractals) - New Task fields: parent_id, lineage, is_leaf, hierarchical_id - ASCII tree display: `cf tasks tree` shows hierarchy with status icons - Status propagation: children done → parent auto-completes - Lineage context injected into agent prompts via ContextPackager - `cf tasks generate --recursive [--max-depth N]` flag - Backward compatible: `cf tasks generate` (without --recursive) still works ## Validation - Tests: 51 new tests, 2357 v2 tests passing (0 regressions) - CI: All checks green (Backend Tests, Code Quality, Security) - Demo: Tree display, rich metadata, backward compat verified Closes #420
1 parent cf3fa89 commit 1a15a27

File tree

10 files changed

+1673
-32
lines changed

10 files changed

+1673
-32
lines changed

codeframe/cli/app.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,18 @@ def tasks_generate(
15471547
"--overwrite",
15481548
help="Delete existing tasks before generating new ones",
15491549
),
1550+
recursive: bool = typer.Option(
1551+
False,
1552+
"--recursive", "-r",
1553+
help="Use recursive decomposition",
1554+
),
1555+
max_depth: int = typer.Option(
1556+
3,
1557+
"--max-depth",
1558+
help="Maximum recursion depth (1-5)",
1559+
min=1,
1560+
max=5,
1561+
),
15501562
) -> None:
15511563
"""Generate tasks from the PRD.
15521564
@@ -1555,6 +1567,7 @@ def tasks_generate(
15551567
15561568
Use --overwrite to clear existing tasks first (useful for regeneration).
15571569
Without --overwrite, new tasks are appended (useful for multi-PRD projects).
1570+
Use --recursive for recursive decomposition into a task tree.
15581571
"""
15591572
from codeframe.core.workspace import get_workspace
15601573
from codeframe.core import prd, tasks
@@ -1585,13 +1598,27 @@ def tasks_generate(
15851598
from codeframe.cli.validators import require_anthropic_api_key
15861599
require_anthropic_api_key()
15871600

1588-
if no_llm:
1601+
if recursive:
1602+
console.print(f"[dim]Using recursive decomposition (max depth: {max_depth})...[/dim]")
1603+
1604+
from codeframe.adapters.llm import get_provider
1605+
from codeframe.core.task_tree import generate_task_tree, flatten_task_tree
1606+
1607+
provider = get_provider()
1608+
tree = generate_task_tree(
1609+
provider,
1610+
prd_record.content,
1611+
lineage=[],
1612+
depth=0,
1613+
max_depth=max_depth,
1614+
)
1615+
created = flatten_task_tree(tree, workspace, prd_id=prd_record.id)
1616+
elif no_llm:
15891617
console.print("[dim]Using simple extraction (--no-llm)[/dim]")
1618+
created = tasks.generate_from_prd(workspace, prd_record, use_llm=False)
15901619
else:
15911620
console.print("[dim]Using LLM for task generation...[/dim]")
1592-
1593-
# Generate tasks
1594-
created = tasks.generate_from_prd(workspace, prd_record, use_llm=not no_llm)
1621+
created = tasks.generate_from_prd(workspace, prd_record, use_llm=True)
15951622

15961623
# Emit event
15971624
emit_for_workspace(
@@ -1624,6 +1651,35 @@ def tasks_generate(
16241651
raise typer.Exit(1)
16251652

16261653

1654+
@tasks_app.command("tree")
1655+
def tasks_tree(
1656+
repo_path: Optional[Path] = typer.Option(
1657+
None,
1658+
"--workspace", "-w",
1659+
help="Workspace path (defaults to current directory)",
1660+
),
1661+
) -> None:
1662+
"""Display task hierarchy as tree."""
1663+
from codeframe.core.task_tree import display_task_tree
1664+
from codeframe.core.workspace import get_workspace
1665+
1666+
workspace_path = repo_path or Path.cwd()
1667+
1668+
try:
1669+
workspace = get_workspace(workspace_path)
1670+
output = display_task_tree(workspace)
1671+
if output:
1672+
typer.echo(output)
1673+
else:
1674+
typer.echo("No tasks found.")
1675+
except FileNotFoundError as e:
1676+
console.print(f"[red]Error:[/red] {e}")
1677+
raise typer.Exit(1)
1678+
except Exception as e:
1679+
console.print(f"[red]Error:[/red] {e}")
1680+
raise typer.Exit(1)
1681+
1682+
16271683
@tasks_app.command("list")
16281684
def tasks_list(
16291685
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status"),

codeframe/core/context_packager.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ def build(
5353

5454
prompt_parts = [context.to_prompt_context()]
5555

56+
# Add lineage context if available
57+
if (
58+
hasattr(context, "task")
59+
and context.task
60+
and hasattr(context.task, "lineage")
61+
and context.task.lineage
62+
):
63+
lineage_str = " \u2192 ".join(context.task.lineage)
64+
prompt_parts.append(
65+
f"\n## Task Lineage\nThis task is part of: {lineage_str}\n"
66+
)
67+
5668
if attempt > 0 and previous_errors:
5769
prompt_parts.append(self._build_retry_section(attempt, previous_errors))
5870

0 commit comments

Comments
 (0)