nc1709 1.15.4__py3-none-any.whl → 1.18.8__py3-none-any.whl
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.
- nc1709/__init__.py +1 -1
- nc1709/agent/core.py +172 -19
- nc1709/agent/permissions.py +2 -2
- nc1709/agent/tools/bash_tool.py +295 -8
- nc1709/cli.py +435 -19
- nc1709/cli_ui.py +137 -52
- nc1709/conversation_logger.py +416 -0
- nc1709/llm_adapter.py +62 -4
- nc1709/plugins/agents/database_agent.py +695 -0
- nc1709/plugins/agents/django_agent.py +11 -4
- nc1709/plugins/agents/docker_agent.py +11 -4
- nc1709/plugins/agents/fastapi_agent.py +11 -4
- nc1709/plugins/agents/git_agent.py +11 -4
- nc1709/plugins/agents/nextjs_agent.py +11 -4
- nc1709/plugins/agents/ollama_agent.py +574 -0
- nc1709/plugins/agents/test_agent.py +702 -0
- nc1709/prompts/unified_prompt.py +156 -14
- nc1709/requirements_tracker.py +526 -0
- nc1709/thinking_messages.py +337 -0
- nc1709/version_check.py +6 -2
- nc1709/web/server.py +63 -3
- nc1709/web/templates/index.html +819 -140
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/METADATA +10 -7
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/RECORD +28 -22
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/WHEEL +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/entry_points.txt +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/licenses/LICENSE +0 -0
- {nc1709-1.15.4.dist-info → nc1709-1.18.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Requirements Tracker
|
|
3
|
+
Persistent tracking of project requirements and user requests.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
10
|
+
from dataclasses import dataclass, field, asdict
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RequirementStatus(Enum):
|
|
16
|
+
"""Status of a requirement"""
|
|
17
|
+
PENDING = "pending" # Not started
|
|
18
|
+
IN_PROGRESS = "in_progress" # Currently working on
|
|
19
|
+
COMPLETED = "completed" # Done
|
|
20
|
+
DEFERRED = "deferred" # Postponed
|
|
21
|
+
CANCELLED = "cancelled" # No longer needed
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RequirementPriority(Enum):
|
|
25
|
+
"""Priority levels"""
|
|
26
|
+
HIGH = "high"
|
|
27
|
+
MEDIUM = "medium"
|
|
28
|
+
LOW = "low"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Requirement:
|
|
33
|
+
"""A single project requirement"""
|
|
34
|
+
id: str
|
|
35
|
+
title: str
|
|
36
|
+
description: str
|
|
37
|
+
status: str = RequirementStatus.PENDING.value
|
|
38
|
+
priority: str = RequirementPriority.MEDIUM.value
|
|
39
|
+
created_at: str = ""
|
|
40
|
+
updated_at: str = ""
|
|
41
|
+
completed_at: str = ""
|
|
42
|
+
notes: List[str] = field(default_factory=list)
|
|
43
|
+
sub_tasks: List[str] = field(default_factory=list)
|
|
44
|
+
tags: List[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
def __post_init__(self):
|
|
47
|
+
if not self.created_at:
|
|
48
|
+
self.created_at = datetime.now().isoformat()
|
|
49
|
+
if not self.updated_at:
|
|
50
|
+
self.updated_at = self.created_at
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> Dict:
|
|
53
|
+
return asdict(self)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(cls, data: Dict) -> "Requirement":
|
|
57
|
+
return cls(**data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Project:
|
|
62
|
+
"""Project containing requirements"""
|
|
63
|
+
name: str
|
|
64
|
+
description: str = ""
|
|
65
|
+
created_at: str = ""
|
|
66
|
+
updated_at: str = ""
|
|
67
|
+
requirements: List[Requirement] = field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
def __post_init__(self):
|
|
70
|
+
if not self.created_at:
|
|
71
|
+
self.created_at = datetime.now().isoformat()
|
|
72
|
+
if not self.updated_at:
|
|
73
|
+
self.updated_at = self.created_at
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> Dict:
|
|
76
|
+
return {
|
|
77
|
+
"name": self.name,
|
|
78
|
+
"description": self.description,
|
|
79
|
+
"created_at": self.created_at,
|
|
80
|
+
"updated_at": self.updated_at,
|
|
81
|
+
"requirements": [r.to_dict() for r in self.requirements]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_dict(cls, data: Dict) -> "Project":
|
|
86
|
+
reqs = [Requirement.from_dict(r) for r in data.get("requirements", [])]
|
|
87
|
+
return cls(
|
|
88
|
+
name=data["name"],
|
|
89
|
+
description=data.get("description", ""),
|
|
90
|
+
created_at=data.get("created_at", ""),
|
|
91
|
+
updated_at=data.get("updated_at", ""),
|
|
92
|
+
requirements=reqs
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RequirementsTracker:
|
|
97
|
+
"""
|
|
98
|
+
Manages project requirements with persistence.
|
|
99
|
+
|
|
100
|
+
Features:
|
|
101
|
+
- Add/update/remove requirements
|
|
102
|
+
- Track status and priority
|
|
103
|
+
- Persist to file
|
|
104
|
+
- Query and filter requirements
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# Storage location for requirements
|
|
108
|
+
STORAGE_DIR = ".nc1709"
|
|
109
|
+
STORAGE_FILE = "requirements.json"
|
|
110
|
+
|
|
111
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
112
|
+
"""
|
|
113
|
+
Initialize the tracker.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
project_root: Root directory of the project (defaults to cwd)
|
|
117
|
+
"""
|
|
118
|
+
self.project_root = project_root or Path.cwd()
|
|
119
|
+
self.storage_path = self.project_root / self.STORAGE_DIR / self.STORAGE_FILE
|
|
120
|
+
self.project: Optional[Project] = None
|
|
121
|
+
self._load()
|
|
122
|
+
|
|
123
|
+
def _ensure_storage_dir(self) -> None:
|
|
124
|
+
"""Ensure storage directory exists"""
|
|
125
|
+
storage_dir = self.project_root / self.STORAGE_DIR
|
|
126
|
+
storage_dir.mkdir(exist_ok=True)
|
|
127
|
+
|
|
128
|
+
def _load(self) -> None:
|
|
129
|
+
"""Load requirements from disk"""
|
|
130
|
+
if self.storage_path.exists():
|
|
131
|
+
try:
|
|
132
|
+
data = json.loads(self.storage_path.read_text())
|
|
133
|
+
self.project = Project.from_dict(data)
|
|
134
|
+
except (json.JSONDecodeError, KeyError):
|
|
135
|
+
self.project = None
|
|
136
|
+
else:
|
|
137
|
+
self.project = None
|
|
138
|
+
|
|
139
|
+
def _save(self) -> None:
|
|
140
|
+
"""Save requirements to disk"""
|
|
141
|
+
if self.project:
|
|
142
|
+
self._ensure_storage_dir()
|
|
143
|
+
self.project.updated_at = datetime.now().isoformat()
|
|
144
|
+
self.storage_path.write_text(
|
|
145
|
+
json.dumps(self.project.to_dict(), indent=2)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _generate_id(self) -> str:
|
|
149
|
+
"""Generate a unique requirement ID"""
|
|
150
|
+
import hashlib
|
|
151
|
+
timestamp = str(time.time()).encode()
|
|
152
|
+
return f"REQ-{hashlib.md5(timestamp).hexdigest()[:8].upper()}"
|
|
153
|
+
|
|
154
|
+
def init_project(self, name: str, description: str = "") -> Project:
|
|
155
|
+
"""
|
|
156
|
+
Initialize a new project.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
name: Project name
|
|
160
|
+
description: Project description
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
The created project
|
|
164
|
+
"""
|
|
165
|
+
self.project = Project(name=name, description=description)
|
|
166
|
+
self._save()
|
|
167
|
+
return self.project
|
|
168
|
+
|
|
169
|
+
def has_project(self) -> bool:
|
|
170
|
+
"""Check if a project is initialized"""
|
|
171
|
+
return self.project is not None
|
|
172
|
+
|
|
173
|
+
def get_project(self) -> Optional[Project]:
|
|
174
|
+
"""Get the current project"""
|
|
175
|
+
return self.project
|
|
176
|
+
|
|
177
|
+
def add_requirement(
|
|
178
|
+
self,
|
|
179
|
+
title: str,
|
|
180
|
+
description: str = "",
|
|
181
|
+
priority: str = RequirementPriority.MEDIUM.value,
|
|
182
|
+
tags: List[str] = None
|
|
183
|
+
) -> Requirement:
|
|
184
|
+
"""
|
|
185
|
+
Add a new requirement.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
title: Short requirement title
|
|
189
|
+
description: Detailed description
|
|
190
|
+
priority: Priority level
|
|
191
|
+
tags: Tags for categorization
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The created requirement
|
|
195
|
+
"""
|
|
196
|
+
if not self.project:
|
|
197
|
+
# Auto-create project based on directory name
|
|
198
|
+
self.init_project(self.project_root.name)
|
|
199
|
+
|
|
200
|
+
req = Requirement(
|
|
201
|
+
id=self._generate_id(),
|
|
202
|
+
title=title,
|
|
203
|
+
description=description,
|
|
204
|
+
priority=priority,
|
|
205
|
+
tags=tags or []
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
self.project.requirements.append(req)
|
|
209
|
+
self._save()
|
|
210
|
+
return req
|
|
211
|
+
|
|
212
|
+
def update_requirement(
|
|
213
|
+
self,
|
|
214
|
+
req_id: str,
|
|
215
|
+
title: Optional[str] = None,
|
|
216
|
+
description: Optional[str] = None,
|
|
217
|
+
status: Optional[str] = None,
|
|
218
|
+
priority: Optional[str] = None,
|
|
219
|
+
notes: Optional[List[str]] = None,
|
|
220
|
+
sub_tasks: Optional[List[str]] = None,
|
|
221
|
+
tags: Optional[List[str]] = None
|
|
222
|
+
) -> Optional[Requirement]:
|
|
223
|
+
"""
|
|
224
|
+
Update an existing requirement.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
req_id: Requirement ID
|
|
228
|
+
title: New title (if changing)
|
|
229
|
+
description: New description (if changing)
|
|
230
|
+
status: New status (if changing)
|
|
231
|
+
priority: New priority (if changing)
|
|
232
|
+
notes: New notes (if changing)
|
|
233
|
+
sub_tasks: New sub-tasks (if changing)
|
|
234
|
+
tags: New tags (if changing)
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Updated requirement or None if not found
|
|
238
|
+
"""
|
|
239
|
+
if not self.project:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
req = self.get_requirement(req_id)
|
|
243
|
+
if not req:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
if title is not None:
|
|
247
|
+
req.title = title
|
|
248
|
+
if description is not None:
|
|
249
|
+
req.description = description
|
|
250
|
+
if status is not None:
|
|
251
|
+
req.status = status
|
|
252
|
+
if status == RequirementStatus.COMPLETED.value:
|
|
253
|
+
req.completed_at = datetime.now().isoformat()
|
|
254
|
+
if priority is not None:
|
|
255
|
+
req.priority = priority
|
|
256
|
+
if notes is not None:
|
|
257
|
+
req.notes = notes
|
|
258
|
+
if sub_tasks is not None:
|
|
259
|
+
req.sub_tasks = sub_tasks
|
|
260
|
+
if tags is not None:
|
|
261
|
+
req.tags = tags
|
|
262
|
+
|
|
263
|
+
req.updated_at = datetime.now().isoformat()
|
|
264
|
+
self._save()
|
|
265
|
+
return req
|
|
266
|
+
|
|
267
|
+
def add_note(self, req_id: str, note: str) -> Optional[Requirement]:
|
|
268
|
+
"""Add a note to a requirement"""
|
|
269
|
+
if not self.project:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
req = self.get_requirement(req_id)
|
|
273
|
+
if not req:
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
277
|
+
req.notes.append(f"[{timestamp}] {note}")
|
|
278
|
+
req.updated_at = datetime.now().isoformat()
|
|
279
|
+
self._save()
|
|
280
|
+
return req
|
|
281
|
+
|
|
282
|
+
def add_sub_task(self, req_id: str, sub_task: str) -> Optional[Requirement]:
|
|
283
|
+
"""Add a sub-task to a requirement"""
|
|
284
|
+
if not self.project:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
req = self.get_requirement(req_id)
|
|
288
|
+
if not req:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
req.sub_tasks.append(sub_task)
|
|
292
|
+
req.updated_at = datetime.now().isoformat()
|
|
293
|
+
self._save()
|
|
294
|
+
return req
|
|
295
|
+
|
|
296
|
+
def set_status(self, req_id: str, status: RequirementStatus) -> Optional[Requirement]:
|
|
297
|
+
"""Set requirement status"""
|
|
298
|
+
return self.update_requirement(req_id, status=status.value)
|
|
299
|
+
|
|
300
|
+
def remove_requirement(self, req_id: str) -> bool:
|
|
301
|
+
"""
|
|
302
|
+
Remove a requirement.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
req_id: Requirement ID
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
True if removed, False if not found
|
|
309
|
+
"""
|
|
310
|
+
if not self.project:
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
for i, req in enumerate(self.project.requirements):
|
|
314
|
+
if req.id == req_id:
|
|
315
|
+
self.project.requirements.pop(i)
|
|
316
|
+
self._save()
|
|
317
|
+
return True
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
def get_requirement(self, req_id: str) -> Optional[Requirement]:
|
|
321
|
+
"""Get a requirement by ID"""
|
|
322
|
+
if not self.project:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
for req in self.project.requirements:
|
|
326
|
+
if req.id == req_id:
|
|
327
|
+
return req
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def get_requirements(
|
|
331
|
+
self,
|
|
332
|
+
status: Optional[str] = None,
|
|
333
|
+
priority: Optional[str] = None,
|
|
334
|
+
tag: Optional[str] = None
|
|
335
|
+
) -> List[Requirement]:
|
|
336
|
+
"""
|
|
337
|
+
Get filtered requirements.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
status: Filter by status
|
|
341
|
+
priority: Filter by priority
|
|
342
|
+
tag: Filter by tag
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
List of matching requirements
|
|
346
|
+
"""
|
|
347
|
+
if not self.project:
|
|
348
|
+
return []
|
|
349
|
+
|
|
350
|
+
reqs = self.project.requirements
|
|
351
|
+
|
|
352
|
+
if status:
|
|
353
|
+
reqs = [r for r in reqs if r.status == status]
|
|
354
|
+
if priority:
|
|
355
|
+
reqs = [r for r in reqs if r.priority == priority]
|
|
356
|
+
if tag:
|
|
357
|
+
reqs = [r for r in reqs if tag in r.tags]
|
|
358
|
+
|
|
359
|
+
return reqs
|
|
360
|
+
|
|
361
|
+
def get_pending(self) -> List[Requirement]:
|
|
362
|
+
"""Get all pending requirements"""
|
|
363
|
+
return self.get_requirements(status=RequirementStatus.PENDING.value)
|
|
364
|
+
|
|
365
|
+
def get_in_progress(self) -> List[Requirement]:
|
|
366
|
+
"""Get all in-progress requirements"""
|
|
367
|
+
return self.get_requirements(status=RequirementStatus.IN_PROGRESS.value)
|
|
368
|
+
|
|
369
|
+
def get_completed(self) -> List[Requirement]:
|
|
370
|
+
"""Get all completed requirements"""
|
|
371
|
+
return self.get_requirements(status=RequirementStatus.COMPLETED.value)
|
|
372
|
+
|
|
373
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
374
|
+
"""
|
|
375
|
+
Get a summary of requirements.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Dictionary with counts and stats
|
|
379
|
+
"""
|
|
380
|
+
if not self.project:
|
|
381
|
+
return {
|
|
382
|
+
"project": None,
|
|
383
|
+
"total": 0,
|
|
384
|
+
"pending": 0,
|
|
385
|
+
"in_progress": 0,
|
|
386
|
+
"completed": 0,
|
|
387
|
+
"deferred": 0,
|
|
388
|
+
"cancelled": 0,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
reqs = self.project.requirements
|
|
392
|
+
return {
|
|
393
|
+
"project": self.project.name,
|
|
394
|
+
"description": self.project.description,
|
|
395
|
+
"total": len(reqs),
|
|
396
|
+
"pending": len([r for r in reqs if r.status == RequirementStatus.PENDING.value]),
|
|
397
|
+
"in_progress": len([r for r in reqs if r.status == RequirementStatus.IN_PROGRESS.value]),
|
|
398
|
+
"completed": len([r for r in reqs if r.status == RequirementStatus.COMPLETED.value]),
|
|
399
|
+
"deferred": len([r for r in reqs if r.status == RequirementStatus.DEFERRED.value]),
|
|
400
|
+
"cancelled": len([r for r in reqs if r.status == RequirementStatus.CANCELLED.value]),
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
def format_requirement(self, req: Requirement, verbose: bool = False) -> str:
|
|
404
|
+
"""
|
|
405
|
+
Format a requirement for display.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
req: The requirement
|
|
409
|
+
verbose: Include full details
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Formatted string
|
|
413
|
+
"""
|
|
414
|
+
# Status icons
|
|
415
|
+
status_icons = {
|
|
416
|
+
RequirementStatus.PENDING.value: "○",
|
|
417
|
+
RequirementStatus.IN_PROGRESS.value: "●",
|
|
418
|
+
RequirementStatus.COMPLETED.value: "✓",
|
|
419
|
+
RequirementStatus.DEFERRED.value: "◐",
|
|
420
|
+
RequirementStatus.CANCELLED.value: "✗",
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# Priority colors (ANSI)
|
|
424
|
+
priority_colors = {
|
|
425
|
+
RequirementPriority.HIGH.value: "\033[91m", # Red
|
|
426
|
+
RequirementPriority.MEDIUM.value: "\033[93m", # Yellow
|
|
427
|
+
RequirementPriority.LOW.value: "\033[92m", # Green
|
|
428
|
+
}
|
|
429
|
+
reset = "\033[0m"
|
|
430
|
+
|
|
431
|
+
icon = status_icons.get(req.status, "○")
|
|
432
|
+
priority_color = priority_colors.get(req.priority, "")
|
|
433
|
+
|
|
434
|
+
line = f"{icon} [{req.id}] {priority_color}{req.title}{reset}"
|
|
435
|
+
|
|
436
|
+
if req.priority == RequirementPriority.HIGH.value:
|
|
437
|
+
line += " 🔥"
|
|
438
|
+
|
|
439
|
+
if verbose:
|
|
440
|
+
line += f"\n Status: {req.status}"
|
|
441
|
+
line += f"\n Priority: {req.priority}"
|
|
442
|
+
if req.description:
|
|
443
|
+
line += f"\n Description: {req.description}"
|
|
444
|
+
if req.tags:
|
|
445
|
+
line += f"\n Tags: {', '.join(req.tags)}"
|
|
446
|
+
if req.sub_tasks:
|
|
447
|
+
line += f"\n Sub-tasks:"
|
|
448
|
+
for task in req.sub_tasks:
|
|
449
|
+
line += f"\n - {task}"
|
|
450
|
+
if req.notes:
|
|
451
|
+
line += f"\n Notes:"
|
|
452
|
+
for note in req.notes[-3:]: # Show last 3 notes
|
|
453
|
+
line += f"\n {note}"
|
|
454
|
+
line += f"\n Created: {req.created_at[:16]}"
|
|
455
|
+
if req.completed_at:
|
|
456
|
+
line += f"\n Completed: {req.completed_at[:16]}"
|
|
457
|
+
|
|
458
|
+
return line
|
|
459
|
+
|
|
460
|
+
def format_all(self, verbose: bool = False, include_completed: bool = False) -> str:
|
|
461
|
+
"""
|
|
462
|
+
Format all requirements for display.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
verbose: Include full details
|
|
466
|
+
include_completed: Include completed requirements
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Formatted string of all requirements
|
|
470
|
+
"""
|
|
471
|
+
if not self.project:
|
|
472
|
+
return "No project initialized. Use /requirements init <name> to start."
|
|
473
|
+
|
|
474
|
+
lines = []
|
|
475
|
+
lines.append(f"📋 Project: {self.project.name}")
|
|
476
|
+
if self.project.description:
|
|
477
|
+
lines.append(f" {self.project.description}")
|
|
478
|
+
lines.append("")
|
|
479
|
+
|
|
480
|
+
# Group by status
|
|
481
|
+
statuses = [
|
|
482
|
+
(RequirementStatus.IN_PROGRESS.value, "In Progress"),
|
|
483
|
+
(RequirementStatus.PENDING.value, "Pending"),
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
if include_completed:
|
|
487
|
+
statuses.extend([
|
|
488
|
+
(RequirementStatus.COMPLETED.value, "Completed"),
|
|
489
|
+
(RequirementStatus.DEFERRED.value, "Deferred"),
|
|
490
|
+
])
|
|
491
|
+
|
|
492
|
+
for status_value, status_name in statuses:
|
|
493
|
+
reqs = [r for r in self.project.requirements if r.status == status_value]
|
|
494
|
+
if reqs:
|
|
495
|
+
lines.append(f"── {status_name} ({len(reqs)}) ──")
|
|
496
|
+
for req in reqs:
|
|
497
|
+
lines.append(self.format_requirement(req, verbose))
|
|
498
|
+
lines.append("")
|
|
499
|
+
|
|
500
|
+
# Summary
|
|
501
|
+
summary = self.get_summary()
|
|
502
|
+
lines.append(f"Total: {summary['total']} | "
|
|
503
|
+
f"Pending: {summary['pending']} | "
|
|
504
|
+
f"In Progress: {summary['in_progress']} | "
|
|
505
|
+
f"Completed: {summary['completed']}")
|
|
506
|
+
|
|
507
|
+
return "\n".join(lines)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# Global instance
|
|
511
|
+
_tracker: Optional[RequirementsTracker] = None
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def get_tracker() -> RequirementsTracker:
|
|
515
|
+
"""Get the global requirements tracker"""
|
|
516
|
+
global _tracker
|
|
517
|
+
if _tracker is None:
|
|
518
|
+
_tracker = RequirementsTracker()
|
|
519
|
+
return _tracker
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def reset_tracker(project_root: Optional[Path] = None) -> RequirementsTracker:
|
|
523
|
+
"""Reset the tracker with a new project root"""
|
|
524
|
+
global _tracker
|
|
525
|
+
_tracker = RequirementsTracker(project_root)
|
|
526
|
+
return _tracker
|