atdd 0.1.0__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.
- atdd/__init__.py +0 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.1.0.dist-info/METADATA +191 -0
- atdd-0.1.0.dist-info/RECORD +183 -0
- atdd-0.1.0.dist-info/WHEEL +5 -0
- atdd-0.1.0.dist-info/entry_points.txt +2 -0
- atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
- atdd-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent config file sync for ATDD managed blocks.
|
|
3
|
+
|
|
4
|
+
Syncs ATDD rules to agent config files (CLAUDE.md, AGENTS.md, etc.) using
|
|
5
|
+
managed blocks that preserve user content while keeping rules in sync.
|
|
6
|
+
|
|
7
|
+
Block format:
|
|
8
|
+
# --- ATDD:BEGIN (managed by atdd, do not edit) ---
|
|
9
|
+
<content from ATDD.md>
|
|
10
|
+
<optional overlay for that agent>
|
|
11
|
+
# --- ATDD:END ---
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
atdd sync # Sync all enabled agents from config
|
|
15
|
+
atdd sync --agent claude # Sync specific agent only
|
|
16
|
+
atdd sync --verify # Check if files are in sync (for CI)
|
|
17
|
+
|
|
18
|
+
Convention: src/atdd/coach/conventions/session.convention.yaml
|
|
19
|
+
"""
|
|
20
|
+
import re
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Dict, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentConfigSync:
|
|
28
|
+
"""Sync managed ATDD blocks to agent config files."""
|
|
29
|
+
|
|
30
|
+
AGENT_FILES = {
|
|
31
|
+
"claude": "CLAUDE.md",
|
|
32
|
+
"codex": "AGENTS.md",
|
|
33
|
+
"gemini": "GEMINI.md",
|
|
34
|
+
"qwen": "QWEN.md",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
BLOCK_BEGIN = "# --- ATDD:BEGIN (managed by atdd, do not edit) ---"
|
|
38
|
+
BLOCK_END = "# --- ATDD:END ---"
|
|
39
|
+
|
|
40
|
+
def __init__(self, target_dir: Optional[Path] = None):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the AgentConfigSync.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
target_dir: Target directory for agent config files. Defaults to cwd.
|
|
46
|
+
"""
|
|
47
|
+
self.target_dir = target_dir or Path.cwd()
|
|
48
|
+
self.atdd_config_dir = self.target_dir / ".atdd"
|
|
49
|
+
self.config_file = self.atdd_config_dir / "config.yaml"
|
|
50
|
+
|
|
51
|
+
# Package resource locations
|
|
52
|
+
self.package_root = Path(__file__).parent.parent # src/atdd/coach
|
|
53
|
+
self.templates_dir = self.package_root / "templates"
|
|
54
|
+
self.overlays_dir = self.package_root / "overlays"
|
|
55
|
+
self.atdd_template = self.templates_dir / "ATDD.md"
|
|
56
|
+
|
|
57
|
+
def sync(self, agents: Optional[List[str]] = None) -> int:
|
|
58
|
+
"""
|
|
59
|
+
Sync managed blocks to agent config files.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
agents: List of agents to sync. If None, read from config.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
0 on success, 1 on error.
|
|
66
|
+
"""
|
|
67
|
+
# Determine which agents to sync
|
|
68
|
+
if agents is None:
|
|
69
|
+
agents = self._get_enabled_agents()
|
|
70
|
+
|
|
71
|
+
if not agents:
|
|
72
|
+
print("No agents configured for sync.")
|
|
73
|
+
print("Add agents to .atdd/config.yaml or use --agent flag.")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
# Validate agent names
|
|
77
|
+
invalid_agents = [a for a in agents if a not in self.AGENT_FILES]
|
|
78
|
+
if invalid_agents:
|
|
79
|
+
print(f"Error: Unknown agent(s): {', '.join(invalid_agents)}")
|
|
80
|
+
print(f"Valid agents: {', '.join(sorted(self.AGENT_FILES.keys()))}")
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
# Load base content
|
|
84
|
+
base_content = self._load_base_content()
|
|
85
|
+
if base_content is None:
|
|
86
|
+
print(f"Error: ATDD template not found: {self.atdd_template}")
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
synced_count = 0
|
|
90
|
+
unchanged_count = 0
|
|
91
|
+
|
|
92
|
+
for agent in agents:
|
|
93
|
+
target_file = self.AGENT_FILES[agent]
|
|
94
|
+
target_path = self.target_dir / target_file
|
|
95
|
+
|
|
96
|
+
# Generate new managed block
|
|
97
|
+
new_block = self._generate_block(agent, base_content)
|
|
98
|
+
|
|
99
|
+
# Read existing content
|
|
100
|
+
existing_content = self._read_target(agent)
|
|
101
|
+
|
|
102
|
+
# Update content
|
|
103
|
+
if self._has_managed_block(existing_content):
|
|
104
|
+
updated_content = self._replace_managed_block(existing_content, new_block)
|
|
105
|
+
else:
|
|
106
|
+
updated_content = self._append_managed_block(existing_content, new_block)
|
|
107
|
+
|
|
108
|
+
# Write only if changed
|
|
109
|
+
if updated_content != existing_content:
|
|
110
|
+
target_path.write_text(updated_content)
|
|
111
|
+
print(f"Synced: {target_file}")
|
|
112
|
+
synced_count += 1
|
|
113
|
+
else:
|
|
114
|
+
print(f"Up to date: {target_file}")
|
|
115
|
+
unchanged_count += 1
|
|
116
|
+
|
|
117
|
+
print(f"\nSync complete: {synced_count} updated, {unchanged_count} unchanged")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
def verify(self) -> int:
|
|
121
|
+
"""
|
|
122
|
+
Verify that agent config files are in sync with ATDD template.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
0 if all files are in sync, 1 if any file is out of sync.
|
|
126
|
+
"""
|
|
127
|
+
agents = self._get_enabled_agents()
|
|
128
|
+
|
|
129
|
+
if not agents:
|
|
130
|
+
print("No agents configured for verification.")
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
base_content = self._load_base_content()
|
|
134
|
+
if base_content is None:
|
|
135
|
+
print(f"Error: ATDD template not found: {self.atdd_template}")
|
|
136
|
+
return 1
|
|
137
|
+
|
|
138
|
+
out_of_sync = []
|
|
139
|
+
missing = []
|
|
140
|
+
|
|
141
|
+
for agent in agents:
|
|
142
|
+
target_file = self.AGENT_FILES[agent]
|
|
143
|
+
target_path = self.target_dir / target_file
|
|
144
|
+
|
|
145
|
+
if not target_path.exists():
|
|
146
|
+
missing.append(target_file)
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Generate expected block
|
|
150
|
+
expected_block = self._generate_block(agent, base_content)
|
|
151
|
+
|
|
152
|
+
# Read existing content
|
|
153
|
+
existing_content = target_path.read_text()
|
|
154
|
+
|
|
155
|
+
# Extract existing managed block
|
|
156
|
+
existing_block, _, _ = self._extract_managed_block(existing_content)
|
|
157
|
+
|
|
158
|
+
if existing_block is None:
|
|
159
|
+
out_of_sync.append((target_file, "missing managed block"))
|
|
160
|
+
elif existing_block.strip() != expected_block.strip():
|
|
161
|
+
out_of_sync.append((target_file, "content mismatch"))
|
|
162
|
+
|
|
163
|
+
# Report results
|
|
164
|
+
if missing:
|
|
165
|
+
print("Missing files:")
|
|
166
|
+
for f in missing:
|
|
167
|
+
print(f" - {f}")
|
|
168
|
+
|
|
169
|
+
if out_of_sync:
|
|
170
|
+
print("Out of sync:")
|
|
171
|
+
for f, reason in out_of_sync:
|
|
172
|
+
print(f" - {f}: {reason}")
|
|
173
|
+
|
|
174
|
+
if missing or out_of_sync:
|
|
175
|
+
print(f"\nRun 'atdd sync' to fix.")
|
|
176
|
+
return 1
|
|
177
|
+
|
|
178
|
+
print("All agent config files are in sync.")
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
def status(self) -> int:
|
|
182
|
+
"""
|
|
183
|
+
Show sync status for all agent config files.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
0 on success.
|
|
187
|
+
"""
|
|
188
|
+
agents = self._get_enabled_agents()
|
|
189
|
+
|
|
190
|
+
print("\n" + "=" * 60)
|
|
191
|
+
print("ATDD Agent Config Sync Status")
|
|
192
|
+
print("=" * 60)
|
|
193
|
+
|
|
194
|
+
print(f"\nConfig file: {self.config_file}")
|
|
195
|
+
print(f"ATDD template: {self.atdd_template}")
|
|
196
|
+
print(f"Overlays dir: {self.overlays_dir}")
|
|
197
|
+
|
|
198
|
+
print(f"\n{'Agent':<10} {'File':<15} {'Status':<20}")
|
|
199
|
+
print("-" * 50)
|
|
200
|
+
|
|
201
|
+
for agent, target_file in sorted(self.AGENT_FILES.items()):
|
|
202
|
+
target_path = self.target_dir / target_file
|
|
203
|
+
enabled = agent in agents
|
|
204
|
+
|
|
205
|
+
if not enabled:
|
|
206
|
+
status = "disabled"
|
|
207
|
+
elif not target_path.exists():
|
|
208
|
+
status = "missing"
|
|
209
|
+
elif not self._has_managed_block(target_path.read_text()):
|
|
210
|
+
status = "no managed block"
|
|
211
|
+
else:
|
|
212
|
+
status = "synced"
|
|
213
|
+
|
|
214
|
+
enabled_marker = "*" if enabled else " "
|
|
215
|
+
print(f"{enabled_marker} {agent:<8} {target_file:<15} {status:<20}")
|
|
216
|
+
|
|
217
|
+
print("-" * 50)
|
|
218
|
+
print("* = enabled in config")
|
|
219
|
+
|
|
220
|
+
# Show overlay status
|
|
221
|
+
print("\nOverlays:")
|
|
222
|
+
for agent in sorted(self.AGENT_FILES.keys()):
|
|
223
|
+
overlay_path = self.overlays_dir / f"{agent}.md"
|
|
224
|
+
if overlay_path.exists():
|
|
225
|
+
print(f" - {agent}.md (found)")
|
|
226
|
+
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
# --- Private helpers ---
|
|
230
|
+
|
|
231
|
+
def _load_config(self) -> Dict:
|
|
232
|
+
"""
|
|
233
|
+
Read .atdd/config.yaml.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Config dict or empty dict if file doesn't exist.
|
|
237
|
+
"""
|
|
238
|
+
if not self.config_file.exists():
|
|
239
|
+
return {}
|
|
240
|
+
|
|
241
|
+
with open(self.config_file) as f:
|
|
242
|
+
return yaml.safe_load(f) or {}
|
|
243
|
+
|
|
244
|
+
def _get_enabled_agents(self) -> List[str]:
|
|
245
|
+
"""
|
|
246
|
+
Return agents from config.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of agent names enabled for sync.
|
|
250
|
+
"""
|
|
251
|
+
config = self._load_config()
|
|
252
|
+
sync_config = config.get("sync", {})
|
|
253
|
+
return sync_config.get("agents", [])
|
|
254
|
+
|
|
255
|
+
def _load_base_content(self) -> Optional[str]:
|
|
256
|
+
"""
|
|
257
|
+
Read ATDD.md from package.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Content of ATDD.md or None if not found.
|
|
261
|
+
"""
|
|
262
|
+
if not self.atdd_template.exists():
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
return self.atdd_template.read_text()
|
|
266
|
+
|
|
267
|
+
def _load_overlay(self, agent: str) -> Optional[str]:
|
|
268
|
+
"""
|
|
269
|
+
Read overlays/<agent>.md if exists.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
agent: Agent name.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Overlay content or None if not found.
|
|
276
|
+
"""
|
|
277
|
+
overlay_path = self.overlays_dir / f"{agent}.md"
|
|
278
|
+
if not overlay_path.exists():
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
return overlay_path.read_text()
|
|
282
|
+
|
|
283
|
+
def _generate_block(self, agent: str, base_content: str) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Combine base + overlay into managed block.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
agent: Agent name.
|
|
289
|
+
base_content: Content from ATDD.md.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Complete managed block with markers.
|
|
293
|
+
"""
|
|
294
|
+
parts = [self.BLOCK_BEGIN, "", base_content.strip()]
|
|
295
|
+
|
|
296
|
+
overlay = self._load_overlay(agent)
|
|
297
|
+
if overlay:
|
|
298
|
+
parts.append("")
|
|
299
|
+
parts.append(f"# Agent-specific: {agent}")
|
|
300
|
+
parts.append(overlay.strip())
|
|
301
|
+
|
|
302
|
+
parts.append("")
|
|
303
|
+
parts.append(self.BLOCK_END)
|
|
304
|
+
|
|
305
|
+
return "\n".join(parts)
|
|
306
|
+
|
|
307
|
+
def _read_target(self, agent: str) -> str:
|
|
308
|
+
"""
|
|
309
|
+
Read existing agent config file or return empty string.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
agent: Agent name.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
File content or empty string if file doesn't exist.
|
|
316
|
+
"""
|
|
317
|
+
target_file = self.AGENT_FILES[agent]
|
|
318
|
+
target_path = self.target_dir / target_file
|
|
319
|
+
|
|
320
|
+
if not target_path.exists():
|
|
321
|
+
return ""
|
|
322
|
+
|
|
323
|
+
return target_path.read_text()
|
|
324
|
+
|
|
325
|
+
def _has_managed_block(self, content: str) -> bool:
|
|
326
|
+
"""
|
|
327
|
+
Check if content has a managed ATDD block.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
content: File content.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
True if managed block exists.
|
|
334
|
+
"""
|
|
335
|
+
return self.BLOCK_BEGIN in content and self.BLOCK_END in content
|
|
336
|
+
|
|
337
|
+
def _extract_managed_block(self, content: str) -> Tuple[Optional[str], int, int]:
|
|
338
|
+
"""
|
|
339
|
+
Extract managed block from content.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
content: File content.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Tuple of (block_content, start_index, end_index).
|
|
346
|
+
Returns (None, -1, -1) if block not found.
|
|
347
|
+
"""
|
|
348
|
+
begin_idx = content.find(self.BLOCK_BEGIN)
|
|
349
|
+
if begin_idx == -1:
|
|
350
|
+
return (None, -1, -1)
|
|
351
|
+
|
|
352
|
+
end_idx = content.find(self.BLOCK_END, begin_idx)
|
|
353
|
+
if end_idx == -1:
|
|
354
|
+
# Malformed: BEGIN without END
|
|
355
|
+
print(f"Warning: Malformed block (BEGIN without END)")
|
|
356
|
+
return (None, -1, -1)
|
|
357
|
+
|
|
358
|
+
# Include the END marker
|
|
359
|
+
end_idx += len(self.BLOCK_END)
|
|
360
|
+
|
|
361
|
+
block = content[begin_idx:end_idx]
|
|
362
|
+
return (block, begin_idx, end_idx)
|
|
363
|
+
|
|
364
|
+
def _replace_managed_block(self, content: str, new_block: str) -> str:
|
|
365
|
+
"""
|
|
366
|
+
Replace existing managed block with new block.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
content: Existing file content.
|
|
370
|
+
new_block: New managed block content.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Updated content.
|
|
374
|
+
"""
|
|
375
|
+
block, start_idx, end_idx = self._extract_managed_block(content)
|
|
376
|
+
|
|
377
|
+
if block is None:
|
|
378
|
+
# No block found, append instead
|
|
379
|
+
return self._append_managed_block(content, new_block)
|
|
380
|
+
|
|
381
|
+
# Check for multiple blocks (warn but only update first)
|
|
382
|
+
remaining = content[end_idx:]
|
|
383
|
+
if self.BLOCK_BEGIN in remaining:
|
|
384
|
+
print("Warning: Multiple managed blocks found, updating first only")
|
|
385
|
+
|
|
386
|
+
return content[:start_idx] + new_block + content[end_idx:]
|
|
387
|
+
|
|
388
|
+
def _append_managed_block(self, content: str, new_block: str) -> str:
|
|
389
|
+
"""
|
|
390
|
+
Append managed block to content.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
content: Existing file content.
|
|
394
|
+
new_block: New managed block content.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Content with block appended.
|
|
398
|
+
"""
|
|
399
|
+
if content and not content.endswith("\n"):
|
|
400
|
+
content += "\n"
|
|
401
|
+
|
|
402
|
+
if content:
|
|
403
|
+
content += "\n"
|
|
404
|
+
|
|
405
|
+
return content + new_block + "\n"
|