devforgeai 1.0.4 → 1.0.6

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 (134) hide show
  1. package/CLAUDE.md +120 -0
  2. package/package.json +9 -1
  3. package/src/CLAUDE.md +699 -0
  4. package/src/claude/scripts/README.md +396 -0
  5. package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
  6. package/src/claude/scripts/check-hooks-fast.sh +70 -0
  7. package/src/claude/scripts/devforgeai-validate +6 -0
  8. package/src/claude/scripts/devforgeai_cli/README.md +531 -0
  9. package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
  10. package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
  11. package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
  12. package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
  13. package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
  14. package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
  15. package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
  16. package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
  17. package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
  18. package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
  19. package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
  20. package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
  21. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
  22. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
  23. package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
  24. package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
  25. package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
  26. package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
  27. package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
  28. package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
  29. package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
  30. package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
  31. package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
  32. package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
  33. package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
  34. package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
  35. package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
  36. package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
  37. package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
  38. package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
  39. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
  40. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
  41. package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
  42. package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
  43. package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
  44. package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
  45. package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
  46. package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
  47. package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
  48. package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
  49. package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
  50. package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
  51. package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
  52. package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
  53. package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
  54. package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
  55. package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
  56. package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
  57. package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
  58. package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
  59. package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
  60. package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
  61. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
  62. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
  63. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
  64. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
  65. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
  66. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
  67. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
  68. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
  69. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
  70. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
  71. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
  72. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
  73. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
  74. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
  75. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
  76. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
  77. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
  78. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
  79. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
  80. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
  81. package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
  82. package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
  83. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
  84. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
  85. package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
  86. package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
  87. package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
  88. package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
  89. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
  90. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
  91. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
  92. package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
  93. package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
  94. package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
  95. package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
  96. package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
  97. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
  98. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
  99. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
  100. package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
  101. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
  102. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
  103. package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
  104. package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
  105. package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
  106. package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
  107. package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
  108. package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
  109. package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
  110. package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
  111. package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
  112. package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
  113. package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
  114. package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
  115. package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
  116. package/src/claude/scripts/install_hooks.sh +186 -0
  117. package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
  118. package/src/claude/scripts/migrate-ac-headers.sh +122 -0
  119. package/src/claude/scripts/plan_file_kb.sh +704 -0
  120. package/src/claude/scripts/requirements.txt +8 -0
  121. package/src/claude/scripts/session_catalog.sh +543 -0
  122. package/src/claude/scripts/setup.py +55 -0
  123. package/src/claude/scripts/start-devforgeai.sh +16 -0
  124. package/src/claude/scripts/statusline.sh +27 -0
  125. package/src/claude/scripts/validate_deferrals.py +344 -0
  126. package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
  127. package/src/claude/skills/researching-market/SKILL.md +2 -1
  128. package/src/cli/lib/copier.js +13 -1
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  130. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  131. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  132. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  133. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  134. package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
@@ -0,0 +1,539 @@
1
+ """
2
+ STORY-306: Subagent Enforcement Tests
3
+
4
+ Unit tests for SubagentEnforcementError and PHASE_REQUIRED_SUBAGENTS validation.
5
+ Tests the enforcement mechanism that prevents phases from completing without
6
+ required subagents being invoked.
7
+
8
+ Test Coverage:
9
+ - AC1: PHASE_REQUIRED_SUBAGENTS constant validation
10
+ - AC2: subagents_required population on state creation
11
+ - AC3: complete_phase blocking when missing subagents
12
+ - AC4: complete_phase success when all subagents invoked
13
+ - AC5: Escape hatch (checkpoint_passed=False)
14
+ - AC6: OR logic for Phase 03 subagents
15
+ - AC8: Backward compatibility for legacy state files
16
+ """
17
+
18
+ import json
19
+ import tempfile
20
+ from pathlib import Path
21
+ from typing import Generator
22
+
23
+ import pytest
24
+
25
+
26
+ # =============================================================================
27
+ # Test Setup and Fixtures
28
+ # =============================================================================
29
+
30
+
31
+ @pytest.fixture
32
+ def temp_project_root() -> Generator[Path, None, None]:
33
+ """Create a temporary project root directory for testing."""
34
+ with tempfile.TemporaryDirectory() as tmpdir:
35
+ project_root = Path(tmpdir)
36
+ yield project_root
37
+
38
+
39
+ @pytest.fixture
40
+ def phase_state(temp_project_root: Path):
41
+ """Create a PhaseState instance with temporary project root."""
42
+ from devforgeai_cli.phase_state import PhaseState
43
+ return PhaseState(project_root=temp_project_root)
44
+
45
+
46
+ # =============================================================================
47
+ # AC1: PHASE_REQUIRED_SUBAGENTS constant
48
+ # =============================================================================
49
+
50
+
51
+ class TestAC1PhaseRequiredSubagentsConstant:
52
+ """Tests for AC1: PHASE_REQUIRED_SUBAGENTS constant validation."""
53
+
54
+ def test_constant_exists(self):
55
+ """
56
+ Given: The phase_state module
57
+ When: PHASE_REQUIRED_SUBAGENTS is imported
58
+ Then: The constant exists and is a dictionary
59
+ """
60
+ from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
61
+
62
+ assert isinstance(PHASE_REQUIRED_SUBAGENTS, dict)
63
+
64
+ def test_constant_contains_all_12_phases(self):
65
+ """
66
+ Given: PHASE_REQUIRED_SUBAGENTS constant
67
+ When: Inspecting the keys
68
+ Then: Contains all 12 valid phases (01-10, 4.5, 5.5)
69
+ """
70
+ from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
71
+
72
+ expected_phases = ["01", "02", "03", "04", "4.5", "05", "5.5",
73
+ "06", "07", "08", "09", "10"]
74
+
75
+ for phase in expected_phases:
76
+ assert phase in PHASE_REQUIRED_SUBAGENTS, f"Missing phase: {phase}"
77
+
78
+ def test_phase_09_contains_framework_analyst(self):
79
+ """
80
+ Given: PHASE_REQUIRED_SUBAGENTS constant
81
+ When: Inspecting Phase 09 entry
82
+ Then: Contains 'framework-analyst'
83
+ """
84
+ from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
85
+
86
+ assert "framework-analyst" in PHASE_REQUIRED_SUBAGENTS["09"]
87
+
88
+ def test_phase_03_uses_tuple_for_or_logic(self):
89
+ """
90
+ Given: PHASE_REQUIRED_SUBAGENTS constant
91
+ When: Inspecting Phase 03 entry
92
+ Then: Uses tuple for OR logic (backend-architect, frontend-developer)
93
+ """
94
+ from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
95
+
96
+ phase_03_requirements = PHASE_REQUIRED_SUBAGENTS["03"]
97
+
98
+ # Find the OR group (tuple)
99
+ or_group = None
100
+ for requirement in phase_03_requirements:
101
+ if isinstance(requirement, tuple):
102
+ or_group = requirement
103
+ break
104
+
105
+ assert or_group is not None, "Phase 03 should have a tuple for OR logic"
106
+ assert "backend-architect" in or_group
107
+ assert "frontend-developer" in or_group
108
+
109
+ def test_phase_01_contains_git_validator_and_tech_stack_detector(self):
110
+ """
111
+ Given: PHASE_REQUIRED_SUBAGENTS constant
112
+ When: Inspecting Phase 01 entry
113
+ Then: Contains git-validator and tech-stack-detector
114
+ """
115
+ from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
116
+
117
+ assert "git-validator" in PHASE_REQUIRED_SUBAGENTS["01"]
118
+ assert "tech-stack-detector" in PHASE_REQUIRED_SUBAGENTS["01"]
119
+
120
+ def test_phase_02_contains_test_automator(self):
121
+ """
122
+ Given: PHASE_REQUIRED_SUBAGENTS constant
123
+ When: Inspecting Phase 02 entry
124
+ Then: Contains test-automator
125
+ """
126
+ from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
127
+
128
+ assert "test-automator" in PHASE_REQUIRED_SUBAGENTS["02"]
129
+
130
+
131
+ # =============================================================================
132
+ # AC2: subagents_required populated on state creation
133
+ # =============================================================================
134
+
135
+
136
+ class TestAC2SubagentsRequiredPopulation:
137
+ """Tests for AC2: subagents_required populated from PHASE_REQUIRED_SUBAGENTS."""
138
+
139
+ def test_new_state_has_populated_subagents_required(self, phase_state):
140
+ """
141
+ Given: A new phase state is created
142
+ When: create() is called
143
+ Then: Each phase has non-empty subagents_required (where applicable)
144
+ """
145
+ state = phase_state.create("STORY-001")
146
+
147
+ # Phase 02 should have test-automator
148
+ assert "test-automator" in state["phases"]["02"]["subagents_required"]
149
+
150
+ def test_phase_02_subagents_required_contains_test_automator(self, phase_state):
151
+ """
152
+ Given: A new phase state
153
+ When: Inspecting phases.02.subagents_required
154
+ Then: Contains 'test-automator'
155
+ """
156
+ state = phase_state.create("STORY-001")
157
+
158
+ assert "test-automator" in state["phases"]["02"]["subagents_required"]
159
+
160
+ def test_phase_09_subagents_required_contains_framework_analyst(self, phase_state):
161
+ """
162
+ Given: A new phase state
163
+ When: Inspecting phases.09.subagents_required
164
+ Then: Contains 'framework-analyst'
165
+ """
166
+ state = phase_state.create("STORY-001")
167
+
168
+ assert "framework-analyst" in state["phases"]["09"]["subagents_required"]
169
+
170
+ def test_phase_07_has_empty_subagents_required(self, phase_state):
171
+ """
172
+ Given: A new phase state
173
+ When: Inspecting Phase 07 (no required subagents)
174
+ Then: subagents_required is empty
175
+ """
176
+ state = phase_state.create("STORY-001")
177
+
178
+ assert state["phases"]["07"]["subagents_required"] == []
179
+
180
+
181
+ # =============================================================================
182
+ # AC3: complete_phase blocks when missing subagents
183
+ # =============================================================================
184
+
185
+
186
+ class TestAC3BlockingOnMissingSubagents:
187
+ """Tests for AC3: complete_phase blocks when required subagents not invoked."""
188
+
189
+ def test_raises_subagent_enforcement_error_when_missing(self, phase_state):
190
+ """
191
+ Given: Phase 02 requires test-automator, not invoked
192
+ When: complete_phase with checkpoint_passed=True
193
+ Then: SubagentEnforcementError is raised
194
+ """
195
+ from devforgeai_cli.phase_state import SubagentEnforcementError
196
+
197
+ phase_state.create("STORY-001")
198
+ # Complete phase 01 first
199
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
200
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
201
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
202
+
203
+ # Try to complete phase 02 without invoking test-automator
204
+ with pytest.raises(SubagentEnforcementError) as exc_info:
205
+ phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
206
+
207
+ assert "test-automator" in str(exc_info.value)
208
+
209
+ def test_error_message_identifies_missing_subagent(self, phase_state):
210
+ """
211
+ Given: Phase 02 missing test-automator
212
+ When: SubagentEnforcementError is raised
213
+ Then: Error message contains 'test-automator'
214
+ """
215
+ from devforgeai_cli.phase_state import SubagentEnforcementError
216
+
217
+ phase_state.create("STORY-001")
218
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
219
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
220
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
221
+
222
+ with pytest.raises(SubagentEnforcementError) as exc_info:
223
+ phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
224
+
225
+ error = exc_info.value
226
+ assert "test-automator" in error.missing_subagents
227
+
228
+ def test_error_has_story_id_phase_and_missing_attributes(self, phase_state):
229
+ """
230
+ Given: SubagentEnforcementError is raised
231
+ When: Inspecting exception attributes
232
+ Then: Contains story_id, phase, and missing_subagents
233
+ """
234
+ from devforgeai_cli.phase_state import SubagentEnforcementError
235
+
236
+ phase_state.create("STORY-001")
237
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
238
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
239
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
240
+
241
+ with pytest.raises(SubagentEnforcementError) as exc_info:
242
+ phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
243
+
244
+ error = exc_info.value
245
+ assert error.story_id == "STORY-001"
246
+ assert error.phase == "02"
247
+ assert isinstance(error.missing_subagents, list)
248
+
249
+
250
+ # =============================================================================
251
+ # AC4: complete_phase succeeds when all subagents invoked
252
+ # =============================================================================
253
+
254
+
255
+ class TestAC4SuccessWhenSubagentsInvoked:
256
+ """Tests for AC4: complete_phase succeeds when all required subagents invoked."""
257
+
258
+ def test_phase_completes_when_required_subagents_recorded(self, phase_state):
259
+ """
260
+ Given: Phase 02 requires test-automator, invoked
261
+ When: complete_phase with checkpoint_passed=True
262
+ Then: Phase status is 'completed'
263
+ """
264
+ phase_state.create("STORY-001")
265
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
266
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
267
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
268
+
269
+ # Record test-automator for phase 02
270
+ phase_state.record_subagent("STORY-001", "02", "test-automator")
271
+ state = phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
272
+
273
+ assert state["phases"]["02"]["status"] == "completed"
274
+
275
+ def test_current_phase_advances_after_success(self, phase_state):
276
+ """
277
+ Given: Phase 02 completed successfully
278
+ When: complete_phase returns
279
+ Then: current_phase advances to '03'
280
+ """
281
+ phase_state.create("STORY-001")
282
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
283
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
284
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
285
+
286
+ phase_state.record_subagent("STORY-001", "02", "test-automator")
287
+ state = phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
288
+
289
+ assert state["current_phase"] == "03"
290
+
291
+
292
+ # =============================================================================
293
+ # AC5: Escape hatch (checkpoint_passed=False)
294
+ # =============================================================================
295
+
296
+
297
+ class TestAC5EscapeHatch:
298
+ """Tests for AC5: Escape hatch allows completion without subagent validation."""
299
+
300
+ def test_completes_without_subagents_when_checkpoint_passed_false(self, phase_state):
301
+ """
302
+ Given: Phase 02 missing required subagents
303
+ When: complete_phase with checkpoint_passed=False
304
+ Then: Phase completes without error
305
+ """
306
+ phase_state.create("STORY-001")
307
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
308
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
309
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
310
+
311
+ # Complete phase 02 without invoking test-automator using escape hatch
312
+ state = phase_state.complete_phase("STORY-001", "02", checkpoint_passed=False)
313
+
314
+ assert state["phases"]["02"]["status"] == "completed"
315
+
316
+ def test_checkpoint_passed_stored_as_false(self, phase_state):
317
+ """
318
+ Given: Escape hatch used
319
+ When: Phase completed with checkpoint_passed=False
320
+ Then: checkpoint_passed is False in state file
321
+ """
322
+ phase_state.create("STORY-001")
323
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
324
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
325
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
326
+
327
+ state = phase_state.complete_phase("STORY-001", "02", checkpoint_passed=False)
328
+
329
+ assert state["phases"]["02"]["checkpoint_passed"] is False
330
+
331
+
332
+ # =============================================================================
333
+ # AC6: OR logic for Phase 03 subagents
334
+ # =============================================================================
335
+
336
+
337
+ class TestAC6ORLogicPhase03:
338
+ """Tests for AC6: OR logic for Phase 03 subagents."""
339
+
340
+ def test_completes_with_backend_architect_only(self, phase_state):
341
+ """
342
+ Given: Phase 03 requires backend-architect OR frontend-developer
343
+ When: Only backend-architect invoked (plus context-validator)
344
+ Then: Phase 03 completes successfully
345
+ """
346
+ phase_state.create("STORY-001")
347
+
348
+ # Complete phases 01-02 first
349
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
350
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
351
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
352
+
353
+ phase_state.record_subagent("STORY-001", "02", "test-automator")
354
+ phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
355
+
356
+ # Complete phase 03 with only backend-architect
357
+ phase_state.record_subagent("STORY-001", "03", "backend-architect")
358
+ phase_state.record_subagent("STORY-001", "03", "context-validator")
359
+ state = phase_state.complete_phase("STORY-001", "03", checkpoint_passed=True)
360
+
361
+ assert state["phases"]["03"]["status"] == "completed"
362
+
363
+ def test_completes_with_frontend_developer_only(self, phase_state):
364
+ """
365
+ Given: Phase 03 requires backend-architect OR frontend-developer
366
+ When: Only frontend-developer invoked (plus context-validator)
367
+ Then: Phase 03 completes successfully
368
+ """
369
+ phase_state.create("STORY-001")
370
+
371
+ # Complete phases 01-02 first
372
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
373
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
374
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
375
+
376
+ phase_state.record_subagent("STORY-001", "02", "test-automator")
377
+ phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
378
+
379
+ # Complete phase 03 with only frontend-developer
380
+ phase_state.record_subagent("STORY-001", "03", "frontend-developer")
381
+ phase_state.record_subagent("STORY-001", "03", "context-validator")
382
+ state = phase_state.complete_phase("STORY-001", "03", checkpoint_passed=True)
383
+
384
+ assert state["phases"]["03"]["status"] == "completed"
385
+
386
+ def test_blocks_with_neither_or_subagent(self, phase_state):
387
+ """
388
+ Given: Phase 03 requires backend-architect OR frontend-developer
389
+ When: Neither invoked (only context-validator)
390
+ Then: SubagentEnforcementError raised
391
+ """
392
+ from devforgeai_cli.phase_state import SubagentEnforcementError
393
+
394
+ phase_state.create("STORY-001")
395
+
396
+ # Complete phases 01-02 first
397
+ phase_state.record_subagent("STORY-001", "01", "git-validator")
398
+ phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
399
+ phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
400
+
401
+ phase_state.record_subagent("STORY-001", "02", "test-automator")
402
+ phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
403
+
404
+ # Try to complete phase 03 with only context-validator
405
+ phase_state.record_subagent("STORY-001", "03", "context-validator")
406
+
407
+ with pytest.raises(SubagentEnforcementError):
408
+ phase_state.complete_phase("STORY-001", "03", checkpoint_passed=True)
409
+
410
+
411
+ # =============================================================================
412
+ # AC8: Backward compatibility for legacy state files
413
+ # =============================================================================
414
+
415
+
416
+ class TestAC8BackwardCompatibility:
417
+ """Tests for AC8: Backward compatibility for legacy state files."""
418
+
419
+ def test_legacy_state_file_loads_successfully(self, temp_project_root):
420
+ """
421
+ Given: A legacy state file with empty subagents_required
422
+ When: Loaded via read()
423
+ Then: No error, state is returned
424
+ """
425
+ from devforgeai_cli.phase_state import PhaseState
426
+
427
+ # Create legacy state file manually
428
+ workflows_dir = temp_project_root / "devforgeai" / "workflows"
429
+ workflows_dir.mkdir(parents=True, exist_ok=True)
430
+
431
+ legacy_state = {
432
+ "story_id": "STORY-001",
433
+ "current_phase": "01",
434
+ "workflow_started": "2026-01-01T00:00:00Z",
435
+ "blocking_status": False,
436
+ "phases": {
437
+ f"{i:02d}": {
438
+ "status": "pending",
439
+ "subagents_required": [], # Empty - legacy format
440
+ "subagents_invoked": []
441
+ } for i in range(1, 11)
442
+ },
443
+ "validation_errors": [],
444
+ "observations": []
445
+ }
446
+
447
+ state_path = workflows_dir / "STORY-001-phase-state.json"
448
+ state_path.write_text(json.dumps(legacy_state, indent=2))
449
+
450
+ ps = PhaseState(project_root=temp_project_root)
451
+ state = ps.read("STORY-001")
452
+
453
+ assert state is not None
454
+ assert state["story_id"] == "STORY-001"
455
+
456
+ def test_legacy_state_file_gets_subagents_required_populated(self, temp_project_root):
457
+ """
458
+ Given: A legacy state file with empty subagents_required
459
+ When: Loaded via read()
460
+ Then: subagents_required is populated from PHASE_REQUIRED_SUBAGENTS
461
+ """
462
+ from devforgeai_cli.phase_state import PhaseState
463
+
464
+ # Create legacy state file manually
465
+ workflows_dir = temp_project_root / "devforgeai" / "workflows"
466
+ workflows_dir.mkdir(parents=True, exist_ok=True)
467
+
468
+ legacy_state = {
469
+ "story_id": "STORY-001",
470
+ "current_phase": "01",
471
+ "workflow_started": "2026-01-01T00:00:00Z",
472
+ "blocking_status": False,
473
+ "phases": {
474
+ f"{i:02d}": {
475
+ "status": "pending",
476
+ "subagents_required": [], # Empty - legacy format
477
+ "subagents_invoked": []
478
+ } for i in range(1, 11)
479
+ },
480
+ "validation_errors": [],
481
+ "observations": []
482
+ }
483
+
484
+ state_path = workflows_dir / "STORY-001-phase-state.json"
485
+ state_path.write_text(json.dumps(legacy_state, indent=2))
486
+
487
+ ps = PhaseState(project_root=temp_project_root)
488
+ state = ps.read("STORY-001")
489
+
490
+ # After loading, subagents_required should be populated
491
+ assert "test-automator" in state["phases"]["02"]["subagents_required"]
492
+ assert "framework-analyst" in state["phases"]["09"]["subagents_required"]
493
+
494
+
495
+ # =============================================================================
496
+ # SubagentEnforcementError Exception Tests
497
+ # =============================================================================
498
+
499
+
500
+ class TestSubagentEnforcementError:
501
+ """Tests for SubagentEnforcementError exception class."""
502
+
503
+ def test_exception_inherits_from_phase_state_error(self):
504
+ """SubagentEnforcementError should inherit from PhaseStateError."""
505
+ from devforgeai_cli.phase_state import (
506
+ PhaseStateError,
507
+ SubagentEnforcementError
508
+ )
509
+
510
+ assert issubclass(SubagentEnforcementError, PhaseStateError)
511
+
512
+ def test_exception_stores_attributes(self):
513
+ """SubagentEnforcementError stores story_id, phase, missing_subagents."""
514
+ from devforgeai_cli.phase_state import SubagentEnforcementError
515
+
516
+ error = SubagentEnforcementError(
517
+ story_id="STORY-001",
518
+ phase="02",
519
+ missing_subagents=["test-automator", "code-reviewer"]
520
+ )
521
+
522
+ assert error.story_id == "STORY-001"
523
+ assert error.phase == "02"
524
+ assert error.missing_subagents == ["test-automator", "code-reviewer"]
525
+
526
+ def test_exception_message_format(self):
527
+ """SubagentEnforcementError message includes all details."""
528
+ from devforgeai_cli.phase_state import SubagentEnforcementError
529
+
530
+ error = SubagentEnforcementError(
531
+ story_id="STORY-001",
532
+ phase="02",
533
+ missing_subagents=["test-automator"]
534
+ )
535
+
536
+ message = str(error)
537
+ assert "STORY-001" in message
538
+ assert "02" in message
539
+ assert "test-automator" in message