Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [Unreleased]

### Added
- `tree` command to display parent-child ticket hierarchy with box-drawing characters
- Supports `--status`, `-a` (assignee), and `-T` (tag) filters
- Ancestor tickets are preserved when filtering to maintain tree structure
- Children sorted by subtree depth (shallow first), then by ID

## [0.3.1] - 2026-01-28

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Commands:
close <id> Set status to closed
reopen <id> Set status to open
status <id> <status> Update status (open|in_progress|closed)
tree [id] [--status=X] [-a X] [-T X] Show parent-child ticket hierarchy
dep <id> <dep-id> Add dependency (id depends on dep-id)
dep tree [--full] <id> Show dependency tree (--full disables dedup)
dep cycle Find dependency cycles in open tickets
Expand Down
37 changes: 31 additions & 6 deletions features/steps/ticket_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,31 @@ def step_ticket_linked_to(context, ticket_id, link_id):
link_path.write_text(content)


@given(r'ticket "(?P<ticket_id>[^"]+)" has assignee "(?P<assignee>[^"]+)"')
def step_ticket_has_assignee(context, ticket_id, assignee):
"""Set ticket assignee."""
ticket_path = Path(context.test_dir) / '.tickets' / f'{ticket_id}.md'
content = ticket_path.read_text()
if re.search(r'^assignee:', content, re.MULTILINE):
content = re.sub(r'^assignee:.*$', f'assignee: {assignee}', content, flags=re.MULTILINE)
else:
content = content.replace('---\n# ', f'assignee: {assignee}\n---\n# ', 1)
ticket_path.write_text(content)


@given(r'ticket "(?P<ticket_id>[^"]+)" has tags "(?P<tags>[^"]+)"')
def step_ticket_has_tags(context, ticket_id, tags):
"""Set ticket tags."""
ticket_path = Path(context.test_dir) / '.tickets' / f'{ticket_id}.md'
content = ticket_path.read_text()
tags_yaml = f'tags: [{tags}]'
if re.search(r'^tags:', content, re.MULTILINE):
content = re.sub(r'^tags:.*$', tags_yaml, content, flags=re.MULTILINE)
else:
content = content.replace('---\n# ', f'{tags_yaml}\n---\n# ', 1)
ticket_path.write_text(content)


@given(r'ticket "(?P<ticket_id>[^"]+)" has a notes section')
def step_ticket_has_notes(context, ticket_id):
"""Ensure ticket has a notes section."""
Expand Down Expand Up @@ -591,21 +616,21 @@ def step_jsonl_deps_is_array(context):
raise AssertionError("No JSONL line with deps field found")


@then(r'the dep tree output should have (?P<first_id>[^\s]+) before (?P<second_id>[^\s]+)')
def step_dep_tree_order(context, first_id, second_id):
"""Assert that first_id appears before second_id in dep tree output."""
@then(r'the (?:dep )?tree output should have (?P<first_id>[^\s]+) before (?P<second_id>[^\s]+)')
def step_tree_output_order(context, first_id, second_id):
"""Assert that first_id appears before second_id in tree output."""
output = context.stdout
lines = output.split('\n')

first_line = -1
second_line = -1

for i, line in enumerate(lines):
if first_id in line:
first_line = i
if second_id in line:
second_line = i

assert first_line != -1, f"'{first_id}' not found in output:\n{output}"
assert second_line != -1, f"'{second_id}' not found in output:\n{output}"
assert first_line < second_line, \
Expand Down
158 changes: 158 additions & 0 deletions features/ticket_tree.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
Feature: Ticket Tree (Parent-Child Hierarchy)
As a user
I want to view the parent-child ticket hierarchy
So that I can understand how tickets are organized

Background:
Given a clean tickets directory

Scenario: Tree with no args shows root tickets and children
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And a ticket exists with ID "proj-0002" and title "Login page" with parent "proj-0001"
And a ticket exists with ID "proj-0003" and title "Standalone task"
When I run "ticket tree"
Then the command should succeed
And the output should contain "proj-0001"
And the output should contain "proj-0002"
And the output should contain "proj-0003"

Scenario: Tree with specific ID shows subtree
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And a ticket exists with ID "proj-0002" and title "Login page" with parent "proj-0001"
And a ticket exists with ID "proj-0003" and title "Standalone task"
When I run "ticket tree proj-0001"
Then the command should succeed
And the output should contain "proj-0001"
And the output should contain "proj-0002"
And the output should not contain "proj-0003"

Scenario: Tree uses box-drawing characters
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And a ticket exists with ID "proj-0002" and title "Login page" with parent "proj-0001"
When I run "ticket tree proj-0001"
Then the command should succeed
And the output should match box-drawing tree format

Scenario: Tree shows status and title
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And a ticket exists with ID "proj-0002" and title "Login page" with parent "proj-0001"
And ticket "proj-0002" has status "in_progress"
When I run "ticket tree"
Then the command should succeed
And the output should contain "[open]"
And the output should contain "[in_progress]"
And the output should contain "Epic Auth"
And the output should contain "Login page"

Scenario: Tree handles deep nesting (grandchildren)
Given a ticket exists with ID "proj-0001" and title "Epic"
And a ticket exists with ID "proj-0002" and title "Feature" with parent "proj-0001"
And a ticket exists with ID "proj-0003" and title "Task" with parent "proj-0002"
When I run "ticket tree"
Then the command should succeed
And the output should contain "proj-0001"
And the output should contain "proj-0002"
And the output should contain "proj-0003"
And the tree output should have proj-0001 before proj-0002
And the tree output should have proj-0002 before proj-0003

Scenario: Tree handles leaf tickets (no children)
Given a ticket exists with ID "proj-0001" and title "Leaf task"
When I run "ticket tree proj-0001"
Then the command should succeed
And the output should contain "proj-0001"
And the output should contain "Leaf task"
And the output line count should be 1

Scenario: Tree sorts children by subtree depth then ID
Given a ticket exists with ID "proj-0001" and title "Root"
And a ticket exists with ID "proj-0005" and title "Child B shallow" with parent "proj-0001"
And a ticket exists with ID "proj-0003" and title "Child A shallow" with parent "proj-0001"
And a ticket exists with ID "proj-0004" and title "Child C deep" with parent "proj-0001"
And a ticket exists with ID "proj-0006" and title "Grandchild" with parent "proj-0004"
When I run "ticket tree proj-0001"
Then the command should succeed
And the tree output should have proj-0003 before proj-0005
And the tree output should have proj-0005 before proj-0004
And the tree output should have proj-0004 before proj-0006

Scenario: Tree sorts children by ID when same depth
Given a ticket exists with ID "proj-0001" and title "Root"
And a ticket exists with ID "proj-0005" and title "Child E" with parent "proj-0001"
And a ticket exists with ID "proj-0002" and title "Child B" with parent "proj-0001"
And a ticket exists with ID "proj-0004" and title "Child D" with parent "proj-0001"
And a ticket exists with ID "proj-0003" and title "Child C" with parent "proj-0001"
When I run "ticket tree proj-0001"
Then the command should succeed
And the tree output should have proj-0002 before proj-0003
And the tree output should have proj-0003 before proj-0004
And the tree output should have proj-0004 before proj-0005

Scenario: Tree with status filter shows matching tickets and ancestors
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And a ticket exists with ID "proj-0002" and title "Login page" with parent "proj-0001"
And a ticket exists with ID "proj-0003" and title "Signup page" with parent "proj-0001"
And ticket "proj-0001" has status "closed"
And ticket "proj-0002" has status "open"
And ticket "proj-0003" has status "closed"
When I run "ticket tree --status=open"
Then the command should succeed
And the output should contain "proj-0001"
And the output should contain "proj-0002"
And the output should not contain "proj-0003"

Scenario: Tree with assignee filter
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And a ticket exists with ID "proj-0002" and title "Login page" with parent "proj-0001"
And a ticket exists with ID "proj-0003" and title "Signup page" with parent "proj-0001"
And ticket "proj-0002" has assignee "alice"
And ticket "proj-0003" has assignee "bob"
When I run "ticket tree -a alice"
Then the command should succeed
And the output should contain "proj-0001"
And the output should contain "proj-0002"
And the output should not contain "proj-0003"

Scenario: Tree with tag filter
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And a ticket exists with ID "proj-0002" and title "Login page" with parent "proj-0001"
And a ticket exists with ID "proj-0003" and title "Signup page" with parent "proj-0001"
And ticket "proj-0002" has tags "frontend,ui"
And ticket "proj-0003" has tags "backend"
When I run "ticket tree -T frontend"
Then the command should succeed
And the output should contain "proj-0001"
And the output should contain "proj-0002"
And the output should not contain "proj-0003"

Scenario: Tree with no matching tickets produces no output
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And ticket "proj-0001" has status "closed"
When I run "ticket tree --status=in_progress"
Then the command should succeed
And the output should be empty

Scenario: Tree supports partial ID matching
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
And a ticket exists with ID "proj-0002" and title "Login page" with parent "proj-0001"
When I run "ticket tree 0001"
Then the command should succeed
And the output should contain "proj-0001"
And the output should contain "proj-0002"

Scenario: Tree with non-existent ID fails
Given a ticket exists with ID "proj-0001" and title "Epic Auth"
When I run "ticket tree nonexistent"
Then the command should fail
And the output should contain "Error: ticket nonexistent not found"

Scenario: Tree multiple roots sorted by subtree depth then ID
Given a ticket exists with ID "proj-0003" and title "Root C (deep)"
And a ticket exists with ID "proj-0001" and title "Root A (shallow)"
And a ticket exists with ID "proj-0002" and title "Root B (shallow)"
And a ticket exists with ID "proj-0004" and title "Child of C" with parent "proj-0003"
When I run "ticket tree"
Then the command should succeed
And the tree output should have proj-0001 before proj-0002
And the tree output should have proj-0002 before proj-0003
And the tree output should have proj-0003 before proj-0004
Loading