doit-toolkit-cli 0.1.10__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.

Potentially problematic release.


This version of doit-toolkit-cli might be problematic. Click here for more details.

Files changed (135) hide show
  1. doit_cli/__init__.py +1356 -0
  2. doit_cli/cli/__init__.py +26 -0
  3. doit_cli/cli/analytics_command.py +616 -0
  4. doit_cli/cli/context_command.py +213 -0
  5. doit_cli/cli/diagram_command.py +304 -0
  6. doit_cli/cli/fixit_command.py +641 -0
  7. doit_cli/cli/hooks_command.py +211 -0
  8. doit_cli/cli/init_command.py +613 -0
  9. doit_cli/cli/memory_command.py +293 -0
  10. doit_cli/cli/roadmapit_command.py +10 -0
  11. doit_cli/cli/status_command.py +117 -0
  12. doit_cli/cli/sync_prompts_command.py +248 -0
  13. doit_cli/cli/validate_command.py +196 -0
  14. doit_cli/cli/verify_command.py +204 -0
  15. doit_cli/cli/workflow_mixin.py +224 -0
  16. doit_cli/cli/xref_command.py +555 -0
  17. doit_cli/formatters/__init__.py +8 -0
  18. doit_cli/formatters/base.py +38 -0
  19. doit_cli/formatters/json_formatter.py +126 -0
  20. doit_cli/formatters/markdown_formatter.py +97 -0
  21. doit_cli/formatters/rich_formatter.py +257 -0
  22. doit_cli/main.py +51 -0
  23. doit_cli/models/__init__.py +139 -0
  24. doit_cli/models/agent.py +74 -0
  25. doit_cli/models/analytics_models.py +384 -0
  26. doit_cli/models/context_config.py +464 -0
  27. doit_cli/models/crossref_models.py +182 -0
  28. doit_cli/models/diagram_models.py +363 -0
  29. doit_cli/models/fixit_models.py +355 -0
  30. doit_cli/models/hook_config.py +125 -0
  31. doit_cli/models/project.py +91 -0
  32. doit_cli/models/results.py +121 -0
  33. doit_cli/models/search_models.py +228 -0
  34. doit_cli/models/status_models.py +195 -0
  35. doit_cli/models/sync_models.py +146 -0
  36. doit_cli/models/template.py +77 -0
  37. doit_cli/models/validation_models.py +175 -0
  38. doit_cli/models/workflow_models.py +319 -0
  39. doit_cli/prompts/__init__.py +5 -0
  40. doit_cli/prompts/fixit_prompts.py +344 -0
  41. doit_cli/prompts/interactive.py +390 -0
  42. doit_cli/rules/__init__.py +5 -0
  43. doit_cli/rules/builtin_rules.py +160 -0
  44. doit_cli/services/__init__.py +79 -0
  45. doit_cli/services/agent_detector.py +168 -0
  46. doit_cli/services/analytics_service.py +218 -0
  47. doit_cli/services/architecture_generator.py +290 -0
  48. doit_cli/services/backup_service.py +204 -0
  49. doit_cli/services/config_loader.py +113 -0
  50. doit_cli/services/context_loader.py +1123 -0
  51. doit_cli/services/coverage_calculator.py +142 -0
  52. doit_cli/services/crossref_service.py +237 -0
  53. doit_cli/services/cycle_time_calculator.py +134 -0
  54. doit_cli/services/date_inferrer.py +349 -0
  55. doit_cli/services/diagram_service.py +337 -0
  56. doit_cli/services/drift_detector.py +109 -0
  57. doit_cli/services/entity_parser.py +301 -0
  58. doit_cli/services/er_diagram_generator.py +197 -0
  59. doit_cli/services/fixit_service.py +699 -0
  60. doit_cli/services/github_service.py +192 -0
  61. doit_cli/services/hook_manager.py +258 -0
  62. doit_cli/services/hook_validator.py +528 -0
  63. doit_cli/services/input_validator.py +322 -0
  64. doit_cli/services/memory_search.py +527 -0
  65. doit_cli/services/mermaid_validator.py +334 -0
  66. doit_cli/services/prompt_transformer.py +91 -0
  67. doit_cli/services/prompt_writer.py +133 -0
  68. doit_cli/services/query_interpreter.py +428 -0
  69. doit_cli/services/report_exporter.py +219 -0
  70. doit_cli/services/report_generator.py +256 -0
  71. doit_cli/services/requirement_parser.py +112 -0
  72. doit_cli/services/roadmap_summarizer.py +209 -0
  73. doit_cli/services/rule_engine.py +443 -0
  74. doit_cli/services/scaffolder.py +215 -0
  75. doit_cli/services/score_calculator.py +172 -0
  76. doit_cli/services/section_parser.py +204 -0
  77. doit_cli/services/spec_scanner.py +327 -0
  78. doit_cli/services/state_manager.py +355 -0
  79. doit_cli/services/status_reporter.py +143 -0
  80. doit_cli/services/task_parser.py +347 -0
  81. doit_cli/services/template_manager.py +710 -0
  82. doit_cli/services/template_reader.py +158 -0
  83. doit_cli/services/user_journey_generator.py +214 -0
  84. doit_cli/services/user_story_parser.py +232 -0
  85. doit_cli/services/validation_service.py +188 -0
  86. doit_cli/services/validator.py +232 -0
  87. doit_cli/services/velocity_tracker.py +173 -0
  88. doit_cli/services/workflow_engine.py +405 -0
  89. doit_cli/templates/agent-file-template.md +28 -0
  90. doit_cli/templates/checklist-template.md +39 -0
  91. doit_cli/templates/commands/doit.checkin.md +363 -0
  92. doit_cli/templates/commands/doit.constitution.md +187 -0
  93. doit_cli/templates/commands/doit.documentit.md +485 -0
  94. doit_cli/templates/commands/doit.fixit.md +181 -0
  95. doit_cli/templates/commands/doit.implementit.md +265 -0
  96. doit_cli/templates/commands/doit.planit.md +262 -0
  97. doit_cli/templates/commands/doit.reviewit.md +355 -0
  98. doit_cli/templates/commands/doit.roadmapit.md +389 -0
  99. doit_cli/templates/commands/doit.scaffoldit.md +458 -0
  100. doit_cli/templates/commands/doit.specit.md +521 -0
  101. doit_cli/templates/commands/doit.taskit.md +304 -0
  102. doit_cli/templates/commands/doit.testit.md +277 -0
  103. doit_cli/templates/config/context.yaml +134 -0
  104. doit_cli/templates/config/hooks.yaml +93 -0
  105. doit_cli/templates/config/validation-rules.yaml +64 -0
  106. doit_cli/templates/github-issue-templates/epic.yml +78 -0
  107. doit_cli/templates/github-issue-templates/feature.yml +116 -0
  108. doit_cli/templates/github-issue-templates/task.yml +129 -0
  109. doit_cli/templates/hooks/.gitkeep +0 -0
  110. doit_cli/templates/hooks/post-commit.sh +25 -0
  111. doit_cli/templates/hooks/post-merge.sh +75 -0
  112. doit_cli/templates/hooks/pre-commit.sh +17 -0
  113. doit_cli/templates/hooks/pre-push.sh +18 -0
  114. doit_cli/templates/memory/completed_roadmap.md +50 -0
  115. doit_cli/templates/memory/constitution.md +125 -0
  116. doit_cli/templates/memory/roadmap.md +61 -0
  117. doit_cli/templates/plan-template.md +146 -0
  118. doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
  119. doit_cli/templates/scripts/bash/common.sh +156 -0
  120. doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
  121. doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
  122. doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
  123. doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
  124. doit_cli/templates/scripts/powershell/common.ps1 +137 -0
  125. doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
  126. doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
  127. doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
  128. doit_cli/templates/spec-template.md +159 -0
  129. doit_cli/templates/tasks-template.md +313 -0
  130. doit_cli/templates/vscode-settings.json +14 -0
  131. doit_toolkit_cli-0.1.10.dist-info/METADATA +324 -0
  132. doit_toolkit_cli-0.1.10.dist-info/RECORD +135 -0
  133. doit_toolkit_cli-0.1.10.dist-info/WHEEL +4 -0
  134. doit_toolkit_cli-0.1.10.dist-info/entry_points.txt +2 -0
  135. doit_toolkit_cli-0.1.10.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,443 @@
1
+ """Rule engine for evaluating validation rules against spec content."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ from ..models.validation_models import (
8
+ Severity,
9
+ ValidationConfig,
10
+ ValidationIssue,
11
+ ValidationRule,
12
+ )
13
+ from ..rules.builtin_rules import get_builtin_rules
14
+
15
+ if TYPE_CHECKING:
16
+ from .crossref_service import CrossReferenceService
17
+
18
+
19
+ class RuleEngine:
20
+ """Evaluates validation rules against spec content."""
21
+
22
+ def __init__(self, config: Optional[ValidationConfig] = None) -> None:
23
+ """Initialize rule engine.
24
+
25
+ Args:
26
+ config: Validation configuration. Uses defaults if None.
27
+ """
28
+ self.config = config or ValidationConfig.default()
29
+ self._rules: list[ValidationRule] = []
30
+ self._load_rules()
31
+
32
+ def _load_rules(self) -> None:
33
+ """Load and configure all rules."""
34
+ # Start with builtin rules
35
+ self._rules = get_builtin_rules()
36
+
37
+ # Apply disabled rules
38
+ for rule in self._rules:
39
+ if rule.id in self.config.disabled_rules:
40
+ rule.enabled = False
41
+
42
+ # Apply severity overrides
43
+ for override in self.config.overrides:
44
+ for rule in self._rules:
45
+ if rule.id == override.rule:
46
+ rule.severity = Severity(override.severity)
47
+ break
48
+
49
+ # Add custom rules
50
+ for custom in self.config.custom_rules:
51
+ custom_rule = ValidationRule(
52
+ id=custom.name,
53
+ name=custom.name.replace("-", " ").title(),
54
+ description=custom.description,
55
+ severity=Severity(custom.severity),
56
+ category=custom.category,
57
+ pattern=custom.pattern,
58
+ enabled=True,
59
+ builtin=False,
60
+ )
61
+ self._rules.append(custom_rule)
62
+
63
+ def get_rules(self) -> list[ValidationRule]:
64
+ """Get all active rules (builtin + custom, minus disabled).
65
+
66
+ Returns:
67
+ List of ValidationRule to apply.
68
+ """
69
+ return [rule for rule in self._rules if rule.enabled]
70
+
71
+ def evaluate(
72
+ self,
73
+ content: str,
74
+ spec_path: Path,
75
+ ) -> list[ValidationIssue]:
76
+ """Evaluate all rules against spec content.
77
+
78
+ Args:
79
+ content: Full text content of spec file.
80
+ spec_path: Path to spec (for context in messages).
81
+
82
+ Returns:
83
+ List of ValidationIssue for all violations found.
84
+ """
85
+ issues: list[ValidationIssue] = []
86
+
87
+ for rule in self.get_rules():
88
+ rule_issues = self.evaluate_rule(rule, content, spec_path)
89
+ issues.extend(rule_issues)
90
+
91
+ return issues
92
+
93
+ def evaluate_rule(
94
+ self,
95
+ rule: ValidationRule,
96
+ content: str,
97
+ spec_path: Path,
98
+ ) -> list[ValidationIssue]:
99
+ """Evaluate a single rule against content.
100
+
101
+ Args:
102
+ rule: The rule to evaluate.
103
+ content: Spec file content.
104
+ spec_path: Path for context.
105
+
106
+ Returns:
107
+ List of issues found (empty if rule passes).
108
+ """
109
+ issues: list[ValidationIssue] = []
110
+
111
+ # Traceability rules have no pattern and require cross-file analysis
112
+ if rule.category == "traceability":
113
+ issues = self._check_traceability(rule, spec_path)
114
+ return issues
115
+
116
+ if not rule.pattern:
117
+ return issues
118
+
119
+ # Determine rule type based on category and pattern
120
+ if rule.category == "structure":
121
+ # Structure rules check for presence of sections
122
+ issues = self._check_section_present(rule, content)
123
+ elif rule.category in ("requirements", "naming"):
124
+ # These rules check that patterns ARE followed where applicable
125
+ issues = self._check_pattern_compliance(rule, content)
126
+ elif rule.category == "acceptance":
127
+ # Check user stories have acceptance scenarios
128
+ issues = self._check_acceptance_scenarios(rule, content)
129
+ elif rule.category == "clarity":
130
+ # Clarity rules check for absence of problematic patterns
131
+ issues = self._check_pattern_absent(rule, content)
132
+
133
+ return issues
134
+
135
+ def _check_section_present(
136
+ self,
137
+ rule: ValidationRule,
138
+ content: str,
139
+ ) -> list[ValidationIssue]:
140
+ """Check if a required section is present.
141
+
142
+ Args:
143
+ rule: The structure rule to check.
144
+ content: Spec content.
145
+
146
+ Returns:
147
+ List with one issue if section missing, empty otherwise.
148
+ """
149
+ if not rule.pattern:
150
+ return []
151
+
152
+ if not re.search(rule.pattern, content, re.MULTILINE | re.IGNORECASE):
153
+ return [
154
+ ValidationIssue(
155
+ rule_id=rule.id,
156
+ severity=rule.severity,
157
+ line_number=0,
158
+ message=f"{rule.name}: {rule.description}",
159
+ suggestion=f"Add a '## {rule.name.replace('Missing ', '')}' section to your spec",
160
+ )
161
+ ]
162
+ return []
163
+
164
+ def _check_pattern_compliance(
165
+ self,
166
+ rule: ValidationRule,
167
+ content: str,
168
+ ) -> list[ValidationIssue]:
169
+ """Check that patterns are followed where applicable.
170
+
171
+ For FR/SC naming, we check if there ARE requirements/criteria
172
+ that don't follow the naming convention.
173
+
174
+ Args:
175
+ rule: The pattern rule to check.
176
+ content: Spec content.
177
+
178
+ Returns:
179
+ List of issues for non-compliant patterns.
180
+ """
181
+ issues: list[ValidationIssue] = []
182
+
183
+ if not rule.pattern:
184
+ return issues
185
+
186
+ # Find the relevant section
187
+ lines = content.split("\n")
188
+
189
+ # For FR naming, look in Requirements section
190
+ if rule.id == "fr-naming-convention":
191
+ in_section = False
192
+ for i, line in enumerate(lines):
193
+ if re.match(r"^##\s+Requirements", line, re.IGNORECASE):
194
+ in_section = True
195
+ continue
196
+ if in_section and line.startswith("##"):
197
+ in_section = False
198
+ if in_section and line.strip().startswith("- **FR-"):
199
+ # Check if it follows the pattern
200
+ if not re.match(rule.pattern, line):
201
+ issues.append(
202
+ ValidationIssue(
203
+ rule_id=rule.id,
204
+ severity=rule.severity,
205
+ line_number=i + 1,
206
+ message=f"Line {i + 1}: {rule.description}",
207
+ suggestion="Use format: - **FR-XXX**: Description",
208
+ )
209
+ )
210
+
211
+ # For SC naming, look in Success Criteria section
212
+ elif rule.id == "sc-naming-convention":
213
+ in_section = False
214
+ for i, line in enumerate(lines):
215
+ if re.match(r"^##\s+Success\s+Criteria", line, re.IGNORECASE):
216
+ in_section = True
217
+ continue
218
+ if in_section and line.startswith("##"):
219
+ in_section = False
220
+ if in_section and line.strip().startswith("- **SC-"):
221
+ if not re.match(rule.pattern, line):
222
+ issues.append(
223
+ ValidationIssue(
224
+ rule_id=rule.id,
225
+ severity=rule.severity,
226
+ line_number=i + 1,
227
+ message=f"Line {i + 1}: {rule.description}",
228
+ suggestion="Use format: - **SC-XXX**: Description",
229
+ )
230
+ )
231
+
232
+ # For feature branch format
233
+ elif rule.id == "feature-branch-format":
234
+ if not re.search(rule.pattern, content, re.MULTILINE):
235
+ # Check if there's a feature branch line at all
236
+ if re.search(r"\*\*Feature\s+Branch\*\*:", content):
237
+ issues.append(
238
+ ValidationIssue(
239
+ rule_id=rule.id,
240
+ severity=rule.severity,
241
+ line_number=0,
242
+ message=rule.description,
243
+ suggestion="Use format: `NNN-feature-name` (e.g., `029-spec-validation`)",
244
+ )
245
+ )
246
+
247
+ return issues
248
+
249
+ def _check_acceptance_scenarios(
250
+ self,
251
+ rule: ValidationRule,
252
+ content: str,
253
+ ) -> list[ValidationIssue]:
254
+ """Check that user stories have acceptance scenarios.
255
+
256
+ Args:
257
+ rule: The acceptance rule to check.
258
+ content: Spec content.
259
+
260
+ Returns:
261
+ List of issues for user stories without scenarios.
262
+ """
263
+ issues: list[ValidationIssue] = []
264
+
265
+ if rule.id != "missing-acceptance-scenarios":
266
+ return issues
267
+
268
+ # Find user story sections
269
+ lines = content.split("\n")
270
+ current_story = None
271
+ story_line = 0
272
+ has_scenarios = False
273
+
274
+ for i, line in enumerate(lines):
275
+ # Detect user story headers
276
+ if re.match(r"^###\s+User\s+Story\s+\d+", line, re.IGNORECASE):
277
+ # Check previous story
278
+ if current_story and not has_scenarios:
279
+ issues.append(
280
+ ValidationIssue(
281
+ rule_id=rule.id,
282
+ severity=rule.severity,
283
+ line_number=story_line,
284
+ message=f"{current_story} has no acceptance scenarios",
285
+ suggestion="Add **Given/When/Then** scenarios under the user story",
286
+ )
287
+ )
288
+ current_story = line.strip().lstrip("#").strip()
289
+ story_line = i + 1
290
+ has_scenarios = False
291
+
292
+ # Detect acceptance scenarios
293
+ if current_story and re.search(
294
+ r"\*\*Given\*\*.*\*\*When\*\*.*\*\*Then\*\*", line
295
+ ):
296
+ has_scenarios = True
297
+
298
+ # Detect next section (non-user story)
299
+ if current_story and re.match(r"^##[^#]", line):
300
+ if not has_scenarios:
301
+ issues.append(
302
+ ValidationIssue(
303
+ rule_id=rule.id,
304
+ severity=rule.severity,
305
+ line_number=story_line,
306
+ message=f"{current_story} has no acceptance scenarios",
307
+ suggestion="Add **Given/When/Then** scenarios under the user story",
308
+ )
309
+ )
310
+ current_story = None
311
+ has_scenarios = False
312
+
313
+ # Check last story
314
+ if current_story and not has_scenarios:
315
+ issues.append(
316
+ ValidationIssue(
317
+ rule_id=rule.id,
318
+ severity=rule.severity,
319
+ line_number=story_line,
320
+ message=f"{current_story} has no acceptance scenarios",
321
+ suggestion="Add **Given/When/Then** scenarios under the user story",
322
+ )
323
+ )
324
+
325
+ return issues
326
+
327
+ def _check_pattern_absent(
328
+ self,
329
+ rule: ValidationRule,
330
+ content: str,
331
+ ) -> list[ValidationIssue]:
332
+ """Check that problematic patterns are absent.
333
+
334
+ Args:
335
+ rule: The clarity rule to check.
336
+ content: Spec content.
337
+
338
+ Returns:
339
+ List of issues where pattern is found.
340
+ """
341
+ issues: list[ValidationIssue] = []
342
+
343
+ if not rule.pattern:
344
+ return issues
345
+
346
+ # Special handling for TODO/FIXME in approved specs
347
+ if rule.id == "todo-in-approved-spec":
348
+ # Check if spec is in draft status
349
+ if re.search(r"\*\*Status\*\*:\s*Draft", content, re.IGNORECASE):
350
+ return [] # Don't flag TODOs in draft specs
351
+
352
+ lines = content.split("\n")
353
+ for i, line in enumerate(lines):
354
+ matches = re.findall(rule.pattern, line, re.IGNORECASE)
355
+ if matches:
356
+ issues.append(
357
+ ValidationIssue(
358
+ rule_id=rule.id,
359
+ severity=rule.severity,
360
+ line_number=i + 1,
361
+ message=f"Line {i + 1}: {rule.description}",
362
+ suggestion=self._get_clarity_suggestion(rule.id),
363
+ )
364
+ )
365
+
366
+ return issues
367
+
368
+ def _get_clarity_suggestion(self, rule_id: str) -> str:
369
+ """Get suggestion text for clarity rules.
370
+
371
+ Args:
372
+ rule_id: The rule ID.
373
+
374
+ Returns:
375
+ Suggestion text.
376
+ """
377
+ suggestions = {
378
+ "unresolved-clarification": "Resolve the ambiguity and remove the [NEEDS CLARIFICATION] marker",
379
+ "todo-in-approved-spec": "Complete the TODO item or change spec status to Draft",
380
+ }
381
+ return suggestions.get(rule_id, "Review and address this issue")
382
+
383
+ def _check_traceability(
384
+ self,
385
+ rule: ValidationRule,
386
+ spec_path: Path,
387
+ ) -> list[ValidationIssue]:
388
+ """Check cross-reference traceability between spec.md and tasks.md.
389
+
390
+ Args:
391
+ rule: The traceability rule to check.
392
+ spec_path: Path to the spec file.
393
+
394
+ Returns:
395
+ List of issues found.
396
+ """
397
+ issues: list[ValidationIssue] = []
398
+
399
+ # Determine the feature directory from spec path
400
+ feature_dir = spec_path.parent
401
+ tasks_path = feature_dir / "tasks.md"
402
+
403
+ # Skip traceability checks if tasks.md doesn't exist
404
+ if not tasks_path.exists():
405
+ return issues
406
+
407
+ # Import here to avoid circular imports
408
+ from .crossref_service import CrossReferenceService
409
+
410
+ try:
411
+ service = CrossReferenceService()
412
+ uncovered, orphaned = service.validate_references(spec_path=spec_path)
413
+
414
+ if rule.id == "orphaned-task-reference":
415
+ for task, ref_id in orphaned:
416
+ issues.append(
417
+ ValidationIssue(
418
+ rule_id=rule.id,
419
+ severity=rule.severity,
420
+ line_number=task.line_number,
421
+ message=f"Task references non-existent requirement {ref_id}",
422
+ suggestion=f"Verify {ref_id} exists in spec.md or remove the reference",
423
+ )
424
+ )
425
+
426
+ elif rule.id == "uncovered-requirement":
427
+ for req_id in uncovered:
428
+ issues.append(
429
+ ValidationIssue(
430
+ rule_id=rule.id,
431
+ severity=rule.severity,
432
+ line_number=0,
433
+ message=f"Requirement {req_id} has no linked tasks",
434
+ suggestion=f"Add [task description] [{req_id}] to tasks.md",
435
+ )
436
+ )
437
+
438
+ except Exception:
439
+ # If cross-reference validation fails, skip silently
440
+ # (e.g., spec not found, parsing errors)
441
+ pass
442
+
443
+ return issues
@@ -0,0 +1,215 @@
1
+ """Scaffolder service for creating doit project structure."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from ..models.agent import Agent
7
+ from ..models.project import Project
8
+ from ..models.results import InitResult
9
+
10
+
11
+ class Scaffolder:
12
+ """Service for creating doit project directory structure."""
13
+
14
+ # Subdirectories to create under .doit/
15
+ DOIT_SUBDIRS = ["memory", "templates", "scripts", "config", "logs"]
16
+
17
+ def __init__(self, project: Project):
18
+ self.project = project
19
+ self.created_directories: list[Path] = []
20
+ self.created_files: list[Path] = []
21
+
22
+ def create_doit_structure(self) -> InitResult:
23
+ """Create the .doit/ directory structure.
24
+
25
+ Creates:
26
+ - .doit/
27
+ - .doit/memory/
28
+ - .doit/templates/
29
+ - .doit/scripts/
30
+ """
31
+ try:
32
+ # Create main .doit directory
33
+ if not self.project.doit_folder.exists():
34
+ self.project.doit_folder.mkdir(parents=True, exist_ok=True)
35
+ self.created_directories.append(self.project.doit_folder)
36
+
37
+ # Create subdirectories
38
+ for subdir in self.DOIT_SUBDIRS:
39
+ subdir_path = self.project.doit_folder / subdir
40
+ if not subdir_path.exists():
41
+ subdir_path.mkdir(parents=True, exist_ok=True)
42
+ self.created_directories.append(subdir_path)
43
+
44
+ return InitResult(
45
+ success=True,
46
+ project=self.project,
47
+ created_directories=self.created_directories.copy(),
48
+ created_files=self.created_files.copy(),
49
+ )
50
+
51
+ except PermissionError as e:
52
+ return InitResult(
53
+ success=False,
54
+ project=self.project,
55
+ error_message=f"Permission denied: {e}",
56
+ )
57
+ except OSError as e:
58
+ return InitResult(
59
+ success=False,
60
+ project=self.project,
61
+ error_message=f"Failed to create directory: {e}",
62
+ )
63
+
64
+ def create_agent_directory(self, agent: Agent) -> bool:
65
+ """Create the command directory for a specific agent.
66
+
67
+ Args:
68
+ agent: The agent to create directory for
69
+
70
+ Returns:
71
+ True if directory was created, False if it already existed
72
+ """
73
+ cmd_dir = self.project.command_directory(agent)
74
+
75
+ if agent == Agent.COPILOT:
76
+ # For Copilot, also ensure .github/ exists
77
+ github_dir = self.project.path / ".github"
78
+ if not github_dir.exists():
79
+ github_dir.mkdir(parents=True, exist_ok=True)
80
+ self.created_directories.append(github_dir)
81
+
82
+ if not cmd_dir.exists():
83
+ cmd_dir.mkdir(parents=True, exist_ok=True)
84
+ self.created_directories.append(cmd_dir)
85
+ return True
86
+
87
+ return False
88
+
89
+ def create_backup_directory(self, timestamp: str) -> Path:
90
+ """Create a timestamped backup directory.
91
+
92
+ Args:
93
+ timestamp: Timestamp string for the backup folder name
94
+
95
+ Returns:
96
+ Path to the created backup directory
97
+ """
98
+ backup_dir = self.project.backups_folder / timestamp
99
+
100
+ if not backup_dir.exists():
101
+ backup_dir.mkdir(parents=True, exist_ok=True)
102
+ self.created_directories.append(backup_dir)
103
+
104
+ return backup_dir
105
+
106
+ def is_doit_file(self, path: Path, agent: Agent) -> bool:
107
+ """Check if a file is a doit-managed file for the given agent.
108
+
109
+ Args:
110
+ path: Path to check
111
+ agent: Agent to check against
112
+
113
+ Returns:
114
+ True if file is doit-managed (matches doit.* or doit-* pattern)
115
+ """
116
+ filename = path.name
117
+ if agent == Agent.CLAUDE:
118
+ return filename.startswith("doit.") and filename.endswith(".md")
119
+ else: # COPILOT
120
+ return filename.startswith("doit.") and filename.endswith(".prompt.md")
121
+
122
+ def get_doit_files(self, agent: Agent) -> list[Path]:
123
+ """Get all doit-managed files for an agent.
124
+
125
+ Args:
126
+ agent: Agent to get files for
127
+
128
+ Returns:
129
+ List of paths to doit-managed files
130
+ """
131
+ cmd_dir = self.project.command_directory(agent)
132
+ if not cmd_dir.exists():
133
+ return []
134
+
135
+ return [f for f in cmd_dir.iterdir() if f.is_file() and self.is_doit_file(f, agent)]
136
+
137
+ def get_custom_files(self, agent: Agent) -> list[Path]:
138
+ """Get all custom (non-doit) files for an agent.
139
+
140
+ Args:
141
+ agent: Agent to get files for
142
+
143
+ Returns:
144
+ List of paths to custom files
145
+ """
146
+ cmd_dir = self.project.command_directory(agent)
147
+ if not cmd_dir.exists():
148
+ return []
149
+
150
+ return [f for f in cmd_dir.iterdir() if f.is_file() and not self.is_doit_file(f, agent)]
151
+
152
+ def get_preserved_paths(self) -> list[Path]:
153
+ """Get list of paths that should be preserved during updates.
154
+
155
+ These include:
156
+ - .doit/memory/ (user-managed content)
157
+ - Custom (non-doit-prefixed) command files
158
+
159
+ Returns:
160
+ List of paths to preserve
161
+ """
162
+ preserved = []
163
+
164
+ # Always preserve memory folder contents
165
+ memory_dir = self.project.memory_folder
166
+ if memory_dir.exists():
167
+ preserved.extend(f for f in memory_dir.rglob("*") if f.is_file())
168
+
169
+ # Preserve custom command files for each agent
170
+ for agent in [Agent.CLAUDE, Agent.COPILOT]:
171
+ preserved.extend(self.get_custom_files(agent))
172
+
173
+ return preserved
174
+
175
+ def get_files_to_update(self, agent: Agent) -> list[Path]:
176
+ """Get list of doit-managed files that can be updated.
177
+
178
+ These are doit-prefixed files that will be overwritten during update.
179
+
180
+ Args:
181
+ agent: Agent to get files for
182
+
183
+ Returns:
184
+ List of paths that can be updated
185
+ """
186
+ return self.get_doit_files(agent)
187
+
188
+ def should_preserve(self, path: Path) -> bool:
189
+ """Check if a file should be preserved during update.
190
+
191
+ Args:
192
+ path: Path to check
193
+
194
+ Returns:
195
+ True if file should be preserved
196
+ """
197
+ # Preserve anything in memory folder
198
+ try:
199
+ path.relative_to(self.project.memory_folder)
200
+ return True
201
+ except ValueError:
202
+ pass
203
+
204
+ # Preserve custom (non-doit) command files
205
+ for agent in [Agent.CLAUDE, Agent.COPILOT]:
206
+ cmd_dir = self.project.command_directory(agent)
207
+ try:
208
+ path.relative_to(cmd_dir)
209
+ # File is in command directory - check if it's doit-managed
210
+ if not self.is_doit_file(path, agent):
211
+ return True
212
+ except ValueError:
213
+ continue
214
+
215
+ return False