htmlgraph 0.2.2__tar.gz → 0.3.0__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 (46) hide show
  1. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/PKG-INFO +1 -1
  2. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/pyproject.toml +1 -1
  3. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/__init__.py +1 -1
  4. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/converter.py +17 -7
  5. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/file_watcher.py +3 -1
  6. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/graph.py +16 -12
  7. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/models.py +4 -1
  8. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/sdk.py +2 -1
  9. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/server.py +48 -51
  10. htmlgraph-0.3.0/src/python/htmlgraph/track_builder.py +261 -0
  11. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/.gitignore +0 -0
  12. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/README.md +0 -0
  13. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/create_auth_track_demo.py +0 -0
  14. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/create_htmlgraph_dev_track.py +0 -0
  15. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/sdk_demo.py +0 -0
  16. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/demo.py +0 -0
  17. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/index.html +0 -0
  18. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/styles.css +0 -0
  19. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-001.html +0 -0
  20. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-002.html +0 -0
  21. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-003.html +0 -0
  22. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-004.html +0 -0
  23. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/examples/todo-list/task-005.html +0 -0
  24. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/agents.py +0 -0
  25. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/analytics_index.py +0 -0
  26. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/cli.py +0 -0
  27. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/dashboard.html +0 -0
  28. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/event_log.py +0 -0
  29. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/event_migration.py +0 -0
  30. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/git_events.py +0 -0
  31. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/mcp_server.py +0 -0
  32. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/parser.py +0 -0
  33. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/planning.py +0 -0
  34. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/session_manager.py +0 -0
  35. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/setup.py +0 -0
  36. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/styles.css +0 -0
  37. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/track_manager.py +0 -0
  38. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/src/python/htmlgraph/watch.py +0 -0
  39. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/__init__.py +0 -0
  40. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_cli_commands.py +0 -0
  41. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_dashboard_ui.py +0 -0
  42. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_event_index.py +0 -0
  43. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_git_events.py +0 -0
  44. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_mcp_server.py +0 -0
  45. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_mcp_stdio_transport.py +0 -0
  46. {htmlgraph-0.2.2 → htmlgraph-0.3.0}/tests/python/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlgraph
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: HTML is All You Need - Graph database on web standards
5
5
  Project-URL: Homepage, https://github.com/Shakes-tzd/htmlgraph
6
6
  Project-URL: Documentation, https://github.com/Shakes-tzd/htmlgraph#readme
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "htmlgraph"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "HTML is All You Need - Graph database on web standards"
5
5
  authors = [{name = "Shakes", email = "shakestzd@gmail.com"}]
6
6
  readme = "README.md"
@@ -12,7 +12,7 @@ from htmlgraph.server import serve
12
12
  from htmlgraph.session_manager import SessionManager
13
13
  from htmlgraph.sdk import SDK
14
14
 
15
- __version__ = "0.1.3"
15
+ __version__ = "0.3.0"
16
16
  __all__ = [
17
17
  "Node",
18
18
  "Edge",
@@ -253,14 +253,24 @@ class NodeConverter:
253
253
  return html_to_node(filepath)
254
254
  return None
255
255
 
256
- def load_all(self, pattern: str = "*.html") -> list[Node]:
257
- """Load all nodes matching pattern."""
256
+ def load_all(self, pattern: str | list[str] = "*.html") -> list[Node]:
257
+ """
258
+ Load all nodes matching pattern(s).
259
+
260
+ Args:
261
+ pattern: Glob pattern(s) to match. Can be a single pattern or list of patterns.
262
+ Examples: "*.html", ["*.html", "*/index.html"]
263
+ """
258
264
  nodes = []
259
- for filepath in self.directory.glob(pattern):
260
- try:
261
- nodes.append(html_to_node(filepath))
262
- except (ValueError, KeyError):
263
- continue # Skip malformed files
265
+ patterns = [pattern] if isinstance(pattern, str) else pattern
266
+
267
+ for pat in patterns:
268
+ for filepath in self.directory.glob(pat):
269
+ if filepath.is_file(): # Skip directories
270
+ try:
271
+ nodes.append(html_to_node(filepath))
272
+ except (ValueError, KeyError):
273
+ continue # Skip malformed files
264
274
  return nodes
265
275
 
266
276
  def save(self, node: Node) -> Path:
@@ -99,7 +99,9 @@ class GraphWatcher:
99
99
  self.handlers[collection] = handler
100
100
 
101
101
  # Watch the collection directory
102
- self.observer.schedule(handler, str(collection_dir), recursive=False)
102
+ # Use recursive=True for tracks since they're stored in subdirectories
103
+ recursive = (collection == "tracks")
104
+ self.observer.schedule(handler, str(collection_dir), recursive=recursive)
103
105
 
104
106
  self.observer.start()
105
107
  print(f"[FileWatcher] Watching {self.graph_dir} for changes...")
@@ -36,7 +36,7 @@ class HtmlGraph:
36
36
  directory: Path | str,
37
37
  stylesheet_path: str = "../styles.css",
38
38
  auto_load: bool = True,
39
- pattern: str = "*.html"
39
+ pattern: str | list[str] = "*.html"
40
40
  ):
41
41
  """
42
42
  Initialize graph from a directory.
@@ -45,7 +45,8 @@ class HtmlGraph:
45
45
  directory: Directory containing HTML node files
46
46
  stylesheet_path: Default stylesheet path for new files
47
47
  auto_load: Whether to load all nodes on init
48
- pattern: Glob pattern for node files
48
+ pattern: Glob pattern(s) for node files. Can be a single pattern or list.
49
+ Examples: "*.html", ["*.html", "*/index.html"]
49
50
  """
50
51
  self.directory = Path(directory)
51
52
  self.directory.mkdir(parents=True, exist_ok=True)
@@ -195,16 +196,19 @@ class HtmlGraph:
195
196
  """
196
197
  matching = []
197
198
 
198
- for filepath in self.directory.glob(self.pattern):
199
- try:
200
- parser = HtmlParser.from_file(filepath)
201
- # Query for article matching selector
202
- if parser.query(f"article{selector}"):
203
- node_id = parser.get_node_id()
204
- if node_id and node_id in self._nodes:
205
- matching.append(self._nodes[node_id])
206
- except Exception:
207
- continue
199
+ patterns = [self.pattern] if isinstance(self.pattern, str) else self.pattern
200
+ for pat in patterns:
201
+ for filepath in self.directory.glob(pat):
202
+ if filepath.is_file():
203
+ try:
204
+ parser = HtmlParser.from_file(filepath)
205
+ # Query for article matching selector
206
+ if parser.query(f"article{selector}"):
207
+ node_id = parser.get_node_id()
208
+ if node_id and node_id in self._nodes:
209
+ matching.append(self._nodes[node_id])
210
+ except Exception:
211
+ continue
208
212
 
209
213
  return matching
210
214
 
@@ -235,6 +235,9 @@ class Node(BaseModel):
235
235
  # Agent attribute
236
236
  agent_attr = f' data-agent-assigned="{self.agent_assigned}"' if self.agent_assigned else ""
237
237
 
238
+ # Track ID attribute
239
+ track_attr = f' data-track-id="{self.track_id}"' if self.track_id else ""
240
+
238
241
  return f'''<!DOCTYPE html>
239
242
  <html lang="en">
240
243
  <head>
@@ -250,7 +253,7 @@ class Node(BaseModel):
250
253
  data-status="{self.status}"
251
254
  data-priority="{self.priority}"
252
255
  data-created="{self.created.isoformat()}"
253
- data-updated="{self.updated.isoformat()}"{agent_attr}>
256
+ data-updated="{self.updated.isoformat()}"{agent_attr}{track_attr}>
254
257
 
255
258
  <header>
256
259
  <h1>{self.title}</h1>
@@ -47,6 +47,7 @@ from dataclasses import dataclass
47
47
  from htmlgraph.models import Node, Step, Edge
48
48
  from htmlgraph.graph import HtmlGraph
49
49
  from htmlgraph.agents import AgentInterface
50
+ from htmlgraph.track_builder import TrackCollection
50
51
 
51
52
 
52
53
  @dataclass
@@ -409,7 +410,7 @@ class SDK:
409
410
 
410
411
  # Non-work collections
411
412
  self.sessions = Collection(self, "sessions", "session")
412
- self.tracks = Collection(self, "tracks", "track")
413
+ self.tracks = TrackCollection(self) # Use specialized collection with builder support
413
414
  self.agents = Collection(self, "agents", "agent")
414
415
 
415
416
  @staticmethod
@@ -54,65 +54,62 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
54
54
  # Tracks support both file-based (track-xxx.html) and directory-based (track-xxx/index.html)
55
55
  if collection == "tracks":
56
56
  from htmlgraph.planning import Track
57
+ from htmlgraph.converter import html_to_node
57
58
 
58
59
  graph = HtmlGraph(
59
60
  collection_dir,
60
61
  stylesheet_path="../styles.css",
61
- auto_load=False # Manual load to handle both patterns
62
+ auto_load=False, # Manual load to convert to Track objects
63
+ pattern=["*.html", "*/index.html"]
62
64
  )
63
65
 
64
- # Helper function to convert Node to Track with file existence checks
66
+ # Helper to convert Node to Track with has_spec/has_plan detection
65
67
  def node_to_track(node: Node, filepath: Path) -> Track:
66
- """Convert a Node to a Track with has_spec/has_plan detection."""
67
- # Determine track directory
68
- if filepath.name == "index.html":
69
- # Directory-based: track-id/index.html
70
- track_dir = filepath.parent
71
- else:
72
- # File-based: track-id.html (no spec/plan support)
73
- track_dir = None
74
-
75
- # Check for spec and plan files
76
- has_spec = track_dir and (track_dir / "spec.html").exists() if track_dir else False
77
- has_plan = track_dir and (track_dir / "plan.html").exists() if track_dir else False
78
-
79
- # Create Track object from Node data
80
- track_data = {
81
- "id": node.id,
82
- "title": node.title,
83
- "description": node.content or "",
84
- "status": node.status if node.status in ["planned", "active", "completed", "abandoned"] else "planned",
85
- "priority": node.priority,
86
- "created": node.created,
87
- "updated": node.updated,
88
- "has_spec": has_spec,
89
- "has_plan": has_plan,
90
- "features": [], # Will be populated from properties if present
91
- "sessions": [], # Will be populated from properties if present
92
- }
93
-
94
- return Track(**track_data)
95
-
96
- # Load file-based tracks (track-xxx.html)
97
- for filepath in collection_dir.glob("*.html"):
98
- try:
99
- from htmlgraph.converter import html_to_node
100
- node = html_to_node(filepath)
101
- track = node_to_track(node, filepath)
102
- graph._nodes[track.id] = track
103
- except Exception:
104
- continue
105
-
106
- # Load directory-based tracks (track-xxx/index.html)
107
- for filepath in collection_dir.glob("*/index.html"):
108
- try:
109
- from htmlgraph.converter import html_to_node
110
- node = html_to_node(filepath)
111
- track = node_to_track(node, filepath)
112
- graph._nodes[track.id] = track
113
- except Exception:
114
- continue
68
+ track_dir = filepath.parent if filepath.name == "index.html" else None
69
+ has_spec = (track_dir / "spec.html").exists() if track_dir else False
70
+ has_plan = (track_dir / "plan.html").exists() if track_dir else False
71
+
72
+ return Track(
73
+ id=node.id,
74
+ title=node.title,
75
+ description=node.content or "",
76
+ status=node.status if node.status in ["planned", "active", "completed", "abandoned"] else "planned",
77
+ priority=node.priority,
78
+ created=node.created,
79
+ updated=node.updated,
80
+ has_spec=has_spec,
81
+ has_plan=has_plan,
82
+ features=[],
83
+ sessions=[]
84
+ )
115
85
 
86
+ # Load and convert tracks
87
+ patterns = graph.pattern if isinstance(graph.pattern, list) else [graph.pattern]
88
+ for pat in patterns:
89
+ for filepath in collection_dir.glob(pat):
90
+ if filepath.is_file():
91
+ try:
92
+ node = html_to_node(filepath)
93
+ track = node_to_track(node, filepath)
94
+ graph._nodes[track.id] = track
95
+ except Exception:
96
+ continue
97
+
98
+ # Override reload to maintain Track conversion
99
+ def reload_tracks():
100
+ graph._nodes.clear()
101
+ for pat in patterns:
102
+ for filepath in collection_dir.glob(pat):
103
+ if filepath.is_file():
104
+ try:
105
+ node = html_to_node(filepath)
106
+ track = node_to_track(node, filepath)
107
+ graph._nodes[track.id] = track
108
+ except Exception:
109
+ continue
110
+ return len(graph._nodes)
111
+
112
+ graph.reload = reload_tracks
116
113
  self.graphs[collection] = graph
117
114
  else:
118
115
  self.graphs[collection] = HtmlGraph(
@@ -0,0 +1,261 @@
1
+ """
2
+ Track Builder for agent-friendly track creation.
3
+ """
4
+ from __future__ import annotations
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+ import re
9
+
10
+ if TYPE_CHECKING:
11
+ from htmlgraph.sdk import SDK
12
+
13
+ from htmlgraph.planning import Track, Spec, Plan, Phase, Task, Requirement, AcceptanceCriterion
14
+
15
+
16
+ class TrackBuilder:
17
+ """
18
+ Fluent builder for creating tracks with spec and plan.
19
+
20
+ Example:
21
+ track = sdk.tracks.builder() \\
22
+ .title("Multi-Agent Collaboration") \\
23
+ .description("Enable seamless agent collaboration") \\
24
+ .priority("high") \\
25
+ .with_spec(
26
+ overview="Agents can work together...",
27
+ requirements=[
28
+ ("Add assigned_agent field", "must-have"),
29
+ ("Implement claim CLI", "must-have")
30
+ ]
31
+ ) \\
32
+ .with_plan_phases([
33
+ ("Phase 1", ["Add field (1h)", "Implement CLI (2h)"]),
34
+ ("Phase 2", ["Add notes (1h)", "Update hooks (2h)"])
35
+ ]) \\
36
+ .create()
37
+ """
38
+
39
+ def __init__(self, sdk: SDK):
40
+ self.sdk = sdk
41
+ self._title = None
42
+ self._description = ""
43
+ self._priority = "medium"
44
+ self._spec_data = {}
45
+ self._plan_phases = []
46
+
47
+ def title(self, title: str) -> 'TrackBuilder':
48
+ """Set track title."""
49
+ self._title = title
50
+ return self
51
+
52
+ def description(self, desc: str) -> 'TrackBuilder':
53
+ """Set track description."""
54
+ self._description = desc
55
+ return self
56
+
57
+ def priority(self, priority: str) -> 'TrackBuilder':
58
+ """Set track priority (low/medium/high/critical)."""
59
+ self._priority = priority
60
+ return self
61
+
62
+ def with_spec(
63
+ self,
64
+ overview: str = "",
65
+ context: str = "",
66
+ requirements: list = None,
67
+ acceptance_criteria: list = None
68
+ ) -> 'TrackBuilder':
69
+ """
70
+ Add spec content to track.
71
+
72
+ Args:
73
+ overview: High-level summary
74
+ context: Background and current state
75
+ requirements: List of (description, priority) tuples or strings
76
+ acceptance_criteria: List of strings or (description, test_case) tuples
77
+ """
78
+ self._spec_data = {
79
+ "overview": overview,
80
+ "context": context,
81
+ "requirements": requirements or [],
82
+ "acceptance_criteria": acceptance_criteria or []
83
+ }
84
+ return self
85
+
86
+ def with_plan_phases(self, phases: list[tuple[str, list[str]]]) -> 'TrackBuilder':
87
+ """
88
+ Add plan phases with tasks.
89
+
90
+ Args:
91
+ phases: List of (phase_name, [task_descriptions]) tuples
92
+ Task descriptions can include estimates like "Task name (2h)"
93
+ """
94
+ self._plan_phases = phases
95
+ return self
96
+
97
+ def _generate_track_html(self, track: Track, track_dir: Path) -> str:
98
+ """Generate track index.html content."""
99
+ spec_link = '<li><a href="spec.html">📝 Specification</a></li>' if track.has_spec else ''
100
+ plan_link = '<li><a href="plan.html">📋 Implementation Plan</a></li>' if track.has_plan else ''
101
+
102
+ return f'''<!DOCTYPE html>
103
+ <html lang="en">
104
+ <head>
105
+ <meta charset="UTF-8">
106
+ <title>{track.title}</title>
107
+ <link rel="stylesheet" href="../../styles.css">
108
+ </head>
109
+ <body>
110
+ <article id="{track.id}" data-type="track" data-status="{track.status}" data-priority="{track.priority}">
111
+ <header>
112
+ <h1>{track.title}</h1>
113
+ <div class="metadata">
114
+ <span class="badge status-{track.status}">{track.status.title()}</span>
115
+ <span class="badge priority-{track.priority}">{track.priority.title()} Priority</span>
116
+ </div>
117
+ </header>
118
+
119
+ <section data-description>
120
+ <p>{track.description}</p>
121
+ </section>
122
+
123
+ <nav data-track-components>
124
+ <h2>Components</h2>
125
+ <ul>
126
+ {spec_link}
127
+ {plan_link}
128
+ </ul>
129
+ </nav>
130
+ </article>
131
+ </body>
132
+ </html>'''
133
+
134
+ def create(self) -> Track:
135
+ """Execute the build and create track+spec+plan."""
136
+ if not self._title:
137
+ raise ValueError("Track title is required")
138
+
139
+ # Generate track ID
140
+ track_id = f"track-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
141
+
142
+ # Create track
143
+ track = Track(
144
+ id=track_id,
145
+ title=f"Track: {self._title}",
146
+ description=self._description,
147
+ priority=self._priority,
148
+ has_spec=bool(self._spec_data),
149
+ has_plan=bool(self._plan_phases)
150
+ )
151
+
152
+ # Save track index.html
153
+ track_dir = Path(".htmlgraph") / "tracks" / track_id
154
+ track_dir.mkdir(parents=True, exist_ok=True)
155
+
156
+ # Generate track index HTML
157
+ track_html = self._generate_track_html(track, track_dir)
158
+ (track_dir / "index.html").write_text(track_html, encoding="utf-8")
159
+
160
+ # Create spec if provided
161
+ requirements = []
162
+ if self._spec_data:
163
+ for i, req in enumerate(self._spec_data.get("requirements", [])):
164
+ if isinstance(req, tuple):
165
+ desc, priority = req
166
+ else:
167
+ desc, priority = req, "must-have"
168
+
169
+ requirements.append(Requirement(
170
+ id=f"req-{i+1}",
171
+ description=desc,
172
+ priority=priority
173
+ ))
174
+
175
+ criteria = []
176
+ for crit in self._spec_data.get("acceptance_criteria", []):
177
+ if isinstance(crit, tuple):
178
+ desc, test_case = crit
179
+ criteria.append(AcceptanceCriterion(description=desc, test_case=test_case))
180
+ else:
181
+ criteria.append(AcceptanceCriterion(description=crit))
182
+
183
+ spec = Spec(
184
+ id=f"{track_id}-spec",
185
+ title=f"{self._title} Specification",
186
+ track_id=track_id,
187
+ overview=self._spec_data.get("overview", ""),
188
+ context=self._spec_data.get("context", ""),
189
+ requirements=requirements,
190
+ acceptance_criteria=criteria
191
+ )
192
+ (track_dir / "spec.html").write_text(spec.to_html(), encoding="utf-8")
193
+
194
+ # Create plan if provided
195
+ if self._plan_phases:
196
+ phases = []
197
+ for i, (phase_name, tasks) in enumerate(self._plan_phases):
198
+ phase_tasks = []
199
+ for j, task_desc in enumerate(tasks):
200
+ # Parse estimate from task description
201
+ estimate = None
202
+ if "(" in task_desc and "h)" in task_desc:
203
+ match = re.search(r'\((\d+(?:\.\d+)?)\s*h\)', task_desc)
204
+ if match:
205
+ estimate = float(match.group(1))
206
+ task_desc = re.sub(r'\s*\(\d+(?:\.\d+)?\s*h\)', '', task_desc).strip()
207
+
208
+ phase_tasks.append(Task(
209
+ id=f"task-{i+1}-{j+1}",
210
+ description=task_desc,
211
+ estimate_hours=estimate
212
+ ))
213
+
214
+ phases.append(Phase(
215
+ id=f"phase-{i+1}",
216
+ name=phase_name,
217
+ tasks=phase_tasks
218
+ ))
219
+
220
+ plan = Plan(
221
+ id=f"{track_id}-plan",
222
+ title=f"{self._title} Implementation Plan",
223
+ track_id=track_id,
224
+ phases=phases
225
+ )
226
+ (track_dir / "plan.html").write_text(plan.to_html(), encoding="utf-8")
227
+
228
+ print(f"✓ Created track: {track_id}")
229
+ if self._spec_data:
230
+ print(f" - Spec with {len(requirements)} requirements")
231
+ if self._plan_phases:
232
+ total_tasks = sum(len(tasks) for _, tasks in self._plan_phases)
233
+ print(f" - Plan with {len(self._plan_phases)} phases, {total_tasks} tasks")
234
+
235
+ return track
236
+
237
+
238
+ class TrackCollection:
239
+ """Collection interface for tracks with builder support."""
240
+
241
+ def __init__(self, sdk: 'SDK'):
242
+ self._sdk = sdk
243
+ self.collection_name = "tracks"
244
+ self.id_prefix = "track"
245
+
246
+ def builder(self) -> TrackBuilder:
247
+ """
248
+ Create a new track builder with fluent interface.
249
+
250
+ Returns:
251
+ TrackBuilder for method chaining
252
+
253
+ Example:
254
+ track = sdk.tracks.builder() \\
255
+ .title("Multi-Agent Collaboration") \\
256
+ .priority("high") \\
257
+ .with_spec(overview="...") \\
258
+ .with_plan_phases([...]) \\
259
+ .create()
260
+ """
261
+ return TrackBuilder(self._sdk)
File without changes
File without changes