devforgeai 1.0.5 → 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 (131) 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/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  127. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  128. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  130. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  131. package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
@@ -0,0 +1,731 @@
1
+ """
2
+ Phase Validation CLI Commands.
3
+
4
+ Provides CLI commands for phase state management in the
5
+ Phase Execution Enforcement System.
6
+
7
+ Commands:
8
+ - phase-init: Create state file (exit 0=created, 1=exists, 2=invalid ID)
9
+ - phase-check: Validate transition (exit 0=allowed, 1=blocked, 2=missing subagents)
10
+ - phase-complete: Mark phase done (exit 0=success, 1=incomplete)
11
+ - phase-status: Show current state (exit 0=success, 1=not found)
12
+ - phase-record: Record subagent invocation (exit 0=recorded, 1=not found, 2=error)
13
+ """
14
+
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+
19
+
20
+ def _get_valid_phases():
21
+ """Get VALID_PHASES constant from phase_state module."""
22
+ from ..phase_state import PhaseState
23
+ return PhaseState.VALID_PHASES
24
+
25
+
26
+ def _get_phase_state(project_root: str):
27
+ """
28
+ Get PhaseState instance with graceful error handling.
29
+
30
+ PhaseState is co-located in the same package for simple imports.
31
+
32
+ Args:
33
+ project_root: Path to the project root directory
34
+
35
+ Returns:
36
+ PhaseState instance for phase tracking
37
+
38
+ Raises:
39
+ ImportError: If phase_state.py module cannot be imported, with
40
+ helpful diagnostic message including:
41
+ - Original error details
42
+ - Expected module location
43
+ - Fix instructions
44
+ - Note about /dev workflow continuation
45
+ """
46
+ try:
47
+ from ..phase_state import PhaseState
48
+ return PhaseState(project_root=Path(project_root))
49
+ except ImportError as e:
50
+ raise ImportError(
51
+ f"PhaseState module not found: {e}\n\n"
52
+ "The phase_state.py module is required for phase tracking.\n"
53
+ "Expected location: .claude/scripts/devforgeai_cli/phase_state.py\n\n"
54
+ "To fix:\n"
55
+ " 1. Ensure STORY-253 (PhaseState module) is implemented\n"
56
+ " 2. Reinstall CLI using one of these methods:\n\n"
57
+ " # Using pipx (recommended for CLI tools):\n"
58
+ " pipx install -e .claude/scripts/ --force\n\n"
59
+ " # Using virtual environment:\n"
60
+ " python3 -m venv .venv && source .venv/bin/activate\n"
61
+ " pip install -e .claude/scripts/\n\n"
62
+ " # Direct pip (if not externally-managed):\n"
63
+ " pip install -e .claude/scripts/\n\n"
64
+ " 3. Retry your command\n\n"
65
+ "Note: The /dev workflow can continue without CLI-based phase\n"
66
+ "enforcement if this module is unavailable. Phase tracking is\n"
67
+ "optional and does not block story development."
68
+ ) from e
69
+
70
+
71
+ def phase_init_command(
72
+ story_id: str,
73
+ project_root: str,
74
+ format: str = "text",
75
+ workflow: str = "dev"
76
+ ) -> int:
77
+ """
78
+ Initialize phase state file for a story.
79
+
80
+ Args:
81
+ story_id: Story identifier (e.g., "STORY-001")
82
+ project_root: Project root directory
83
+ format: Output format ("text" or "json")
84
+ workflow: Workflow type ("dev" or "qa")
85
+
86
+ Returns:
87
+ Exit code: 0=created, 1=exists, 2=invalid ID or invalid workflow
88
+ """
89
+ try:
90
+ # Validate workflow parameter (STORY-517, STORY-521)
91
+ from ..phase_state import VALID_WORKFLOWS, WORKFLOW_SCHEMAS
92
+ if workflow not in VALID_WORKFLOWS:
93
+ if format == "json":
94
+ print(json.dumps({
95
+ "success": False,
96
+ "error": f"Invalid workflow: '{workflow}'. Must be one of: {VALID_WORKFLOWS}",
97
+ "story_id": story_id
98
+ }))
99
+ else:
100
+ print(f"ERROR: Invalid workflow: '{workflow}'. Must be one of: {VALID_WORKFLOWS}")
101
+ return 2
102
+
103
+ ps = _get_phase_state(project_root)
104
+
105
+ # Determine state file path based on workflow type (STORY-521)
106
+ if workflow == "dev":
107
+ state_path = ps._get_state_path(story_id)
108
+ elif workflow == "qa":
109
+ state_path = ps._get_qa_state_path(story_id)
110
+ else:
111
+ state_path = ps.workflows_dir / f"{story_id}-{workflow}-phase-state.json"
112
+
113
+ if state_path.exists():
114
+ label = "QA state file" if workflow == "qa" else "State file"
115
+ if format == "json":
116
+ print(json.dumps({
117
+ "success": False,
118
+ "error": f"{label} already exists",
119
+ "story_id": story_id,
120
+ "path": str(state_path)
121
+ }))
122
+ else:
123
+ print(f"{label} already exists for {story_id}")
124
+ print(f" Path: {state_path}")
125
+ return 1
126
+
127
+ # Use unified create_workflow for all workflow types (STORY-521)
128
+ state = ps.create_workflow(story_id, workflow)
129
+
130
+ if format == "json":
131
+ result_data = {
132
+ "success": True,
133
+ "story_id": story_id,
134
+ "path": str(state_path),
135
+ "current_phase": state["current_phase"]
136
+ }
137
+ if workflow != "dev":
138
+ result_data["workflow"] = workflow
139
+ print(json.dumps(result_data))
140
+ else:
141
+ if workflow == "qa":
142
+ label = f"Created QA phase state for {story_id}"
143
+ elif workflow == "dev":
144
+ label = f"Created phase state for {story_id}"
145
+ else:
146
+ label = f"Created {workflow} phase state for {story_id}"
147
+ print(label)
148
+ print(f" Path: {state_path}")
149
+ print(f" Current phase: {state['current_phase']}")
150
+
151
+ return 0
152
+
153
+ except ValueError as e:
154
+ if format == "json":
155
+ print(json.dumps({
156
+ "success": False,
157
+ "error": str(e),
158
+ "story_id": story_id
159
+ }))
160
+ else:
161
+ print(f"ERROR: {e}")
162
+ return 2
163
+
164
+ except Exception as e:
165
+ if format == "json":
166
+ print(json.dumps({
167
+ "success": False,
168
+ "error": str(e),
169
+ "story_id": story_id
170
+ }))
171
+ else:
172
+ print(f"ERROR: {e}")
173
+ return 2
174
+
175
+
176
+ def phase_check_command(
177
+ story_id: str,
178
+ from_phase: str,
179
+ to_phase: str,
180
+ project_root: str,
181
+ format: str = "text"
182
+ ) -> int:
183
+ """
184
+ Check if phase transition is allowed.
185
+
186
+ Args:
187
+ story_id: Story identifier
188
+ from_phase: Source phase (e.g., "01")
189
+ to_phase: Target phase (e.g., "02")
190
+ project_root: Project root directory
191
+ format: Output format
192
+
193
+ Returns:
194
+ Exit code: 0=allowed, 1=blocked, 2=missing subagents
195
+ """
196
+ try:
197
+ ps = _get_phase_state(project_root)
198
+ state = ps.read(story_id)
199
+
200
+ if state is None:
201
+ if format == "json":
202
+ print(json.dumps({
203
+ "allowed": False,
204
+ "error": "State file not found",
205
+ "story_id": story_id
206
+ }))
207
+ else:
208
+ print(f"State file not found for {story_id}")
209
+ return 1
210
+
211
+ # Rule 1: Previous phase must be completed
212
+ if state["phases"][from_phase]["status"] != "completed":
213
+ if format == "json":
214
+ print(json.dumps({
215
+ "allowed": False,
216
+ "error": f"Phase {from_phase} not completed",
217
+ "story_id": story_id,
218
+ "from_phase": from_phase,
219
+ "to_phase": to_phase
220
+ }))
221
+ else:
222
+ print(f"Phase {from_phase} not completed")
223
+ return 1
224
+
225
+ # Rule 2: Must be sequential (no skipping)
226
+ # Use ordered VALID_PHASES list to handle decimal phases (4.5, 5.5)
227
+ valid_phases = _get_valid_phases()
228
+ try:
229
+ from_idx = valid_phases.index(from_phase)
230
+ to_idx = valid_phases.index(to_phase)
231
+ except ValueError:
232
+ if format == "json":
233
+ print(json.dumps({
234
+ "allowed": False,
235
+ "error": f"Invalid phase: from='{from_phase}' or to='{to_phase}'",
236
+ "story_id": story_id
237
+ }))
238
+ else:
239
+ print(f"Invalid phase: from='{from_phase}' or to='{to_phase}'")
240
+ return 1
241
+
242
+ if to_idx != from_idx + 1:
243
+ expected = valid_phases[from_idx + 1] if from_idx + 1 < len(valid_phases) else "N/A"
244
+ if format == "json":
245
+ print(json.dumps({
246
+ "allowed": False,
247
+ "error": f"Cannot skip phases: {from_phase} -> {to_phase}, expected {expected}",
248
+ "story_id": story_id
249
+ }))
250
+ else:
251
+ print(f"Cannot skip phases: {from_phase} -> {to_phase}")
252
+ return 1
253
+
254
+ # Rule 3: All required subagents must be invoked (supports OR-groups per STORY-306)
255
+ # Fix: STORY-464 - nested lists (OR-groups) are unhashable, cannot use set()
256
+ required = state["phases"][from_phase].get("subagents_required", [])
257
+ invoked = set(state["phases"][from_phase].get("subagents_invoked", []))
258
+ missing = []
259
+
260
+ for requirement in required:
261
+ if isinstance(requirement, list):
262
+ # OR logic (STORY-306): any one subagent in list satisfies requirement
263
+ if not any(subagent_name in invoked for subagent_name in requirement):
264
+ missing.append(f"({' OR '.join(requirement)})")
265
+ else:
266
+ # Simple requirement: subagent must be in invoked set
267
+ if requirement not in invoked:
268
+ missing.append(requirement)
269
+
270
+ if missing:
271
+ if format == "json":
272
+ print(json.dumps({
273
+ "allowed": False,
274
+ "error": f"Missing subagents: {missing}",
275
+ "story_id": story_id,
276
+ "missing_subagents": missing
277
+ }))
278
+ else:
279
+ print(f"Missing subagents for phase {from_phase}:")
280
+ for agent in missing:
281
+ print(f" - {agent}")
282
+ return 2
283
+
284
+ # Transition allowed
285
+ if format == "json":
286
+ print(json.dumps({
287
+ "allowed": True,
288
+ "story_id": story_id,
289
+ "from_phase": from_phase,
290
+ "to_phase": to_phase
291
+ }))
292
+ else:
293
+ print(f"Transition allowed: {from_phase} -> {to_phase}")
294
+
295
+ return 0
296
+
297
+ except Exception as e:
298
+ if format == "json":
299
+ print(json.dumps({
300
+ "allowed": False,
301
+ "error": str(e),
302
+ "story_id": story_id
303
+ }))
304
+ else:
305
+ print(f"ERROR: {e}")
306
+ return 1
307
+
308
+
309
+ def phase_complete_command(
310
+ story_id: str,
311
+ phase: str,
312
+ checkpoint_passed: bool,
313
+ project_root: str,
314
+ format: str = "text",
315
+ workflow: str = "dev"
316
+ ) -> int:
317
+ """
318
+ Mark a phase as complete.
319
+
320
+ Args:
321
+ story_id: Story identifier
322
+ phase: Phase to complete (e.g., "02")
323
+ checkpoint_passed: Whether checkpoint validation passed
324
+ project_root: Project root directory
325
+ format: Output format
326
+ workflow: Workflow type ("dev" or "qa")
327
+
328
+ Returns:
329
+ Exit code: 0=success, 1=incomplete/error
330
+ """
331
+ try:
332
+ ps = _get_phase_state(project_root)
333
+
334
+ # Use unified complete_workflow_phase for all workflow types (STORY-521)
335
+ state = ps.complete_workflow_phase(story_id, workflow, phase, checkpoint_passed)
336
+
337
+ if format == "json":
338
+ result_data = {
339
+ "success": True,
340
+ "story_id": story_id,
341
+ "completed_phase": phase,
342
+ "current_phase": state["current_phase"],
343
+ "checkpoint_passed": checkpoint_passed
344
+ }
345
+ if workflow != "dev":
346
+ result_data["workflow"] = workflow
347
+ print(json.dumps(result_data))
348
+ else:
349
+ if workflow == "qa":
350
+ print(f"QA phase {phase} completed for {story_id}")
351
+ elif workflow == "dev":
352
+ print(f"Phase {phase} completed for {story_id}")
353
+ else:
354
+ print(f"{workflow.capitalize()} phase {phase} completed for {story_id}")
355
+ print(f" Current phase: {state['current_phase']}")
356
+ print(f" Checkpoint passed: {checkpoint_passed}")
357
+
358
+ return 0
359
+
360
+ except ValueError as e:
361
+ # Step validation failure (STORY-517) - exit code 1
362
+ if format == "json":
363
+ print(json.dumps({
364
+ "success": False,
365
+ "error": str(e),
366
+ "story_id": story_id
367
+ }))
368
+ else:
369
+ print(f"ERROR: {e}")
370
+ return 1
371
+
372
+ except Exception as e:
373
+ if format == "json":
374
+ print(json.dumps({
375
+ "success": False,
376
+ "error": str(e),
377
+ "story_id": story_id
378
+ }))
379
+ else:
380
+ print(f"ERROR: {e}")
381
+ return 1
382
+
383
+
384
+ def phase_status_command(
385
+ story_id: str,
386
+ project_root: str,
387
+ format: str = "text"
388
+ ) -> int:
389
+ """
390
+ Display current phase status.
391
+
392
+ Args:
393
+ story_id: Story identifier
394
+ project_root: Project root directory
395
+ format: Output format
396
+
397
+ Returns:
398
+ Exit code: 0=success, 1=not found
399
+ """
400
+ try:
401
+ ps = _get_phase_state(project_root)
402
+ state = ps.read(story_id)
403
+
404
+ if state is None:
405
+ if format == "json":
406
+ print(json.dumps({
407
+ "found": False,
408
+ "error": "State file not found",
409
+ "story_id": story_id
410
+ }))
411
+ else:
412
+ print(f"State file not found for {story_id}")
413
+ return 1
414
+
415
+ if format == "json":
416
+ print(json.dumps(state, indent=2))
417
+ else:
418
+ print(f"Story: {state['story_id']}")
419
+ print(f"Started: {state['workflow_started']}")
420
+ print(f"Current Phase: {state['current_phase']}")
421
+ print(f"Blocking: {state['blocking_status']}")
422
+ print()
423
+ print("Phase Status:")
424
+ for phase_id, phase_data in state["phases"].items():
425
+ status = phase_data["status"]
426
+ marker = "x" if status == "completed" else " "
427
+ print(f" [{marker}] Phase {phase_id}: {status}")
428
+ if phase_data.get("subagents_invoked"):
429
+ print(f" Subagents: {', '.join(phase_data['subagents_invoked'])}")
430
+
431
+ return 0
432
+
433
+ except Exception as e:
434
+ if format == "json":
435
+ print(json.dumps({
436
+ "found": False,
437
+ "error": str(e),
438
+ "story_id": story_id
439
+ }))
440
+ else:
441
+ print(f"ERROR: {e}")
442
+ return 1
443
+
444
+
445
+ def phase_record_command(
446
+ story_id: str,
447
+ phase: str,
448
+ subagent: str,
449
+ project_root: str,
450
+ format: str = "text"
451
+ ) -> int:
452
+ """
453
+ Record a subagent invocation for a phase.
454
+
455
+ Args:
456
+ story_id: Story identifier (e.g., "STORY-001")
457
+ phase: Phase ID (e.g., "02")
458
+ subagent: Subagent name that was invoked
459
+ project_root: Project root directory
460
+ format: Output format ("text" or "json")
461
+
462
+ Returns:
463
+ Exit code: 0=recorded, 1=not found, 2=error
464
+ """
465
+ try:
466
+ ps = _get_phase_state(project_root)
467
+ ps.record_subagent(story_id, phase, subagent)
468
+
469
+ if format == "json":
470
+ print(json.dumps({
471
+ "success": True,
472
+ "story_id": story_id,
473
+ "phase": phase,
474
+ "subagent": subagent
475
+ }))
476
+ else:
477
+ print(f"Recorded subagent '{subagent}' for {story_id} phase {phase}")
478
+
479
+ return 0
480
+
481
+ except Exception as e:
482
+ if format == "json":
483
+ print(json.dumps({
484
+ "success": False,
485
+ "error": str(e),
486
+ "story_id": story_id
487
+ }))
488
+ else:
489
+ print(f"ERROR: {e}")
490
+ return 2
491
+
492
+
493
+ # =============================================================================
494
+ # STORY-525: Phase Record Step Command
495
+ # =============================================================================
496
+
497
+
498
+ def phase_record_step_command(
499
+ story_id: str,
500
+ phase: str,
501
+ step_id: str,
502
+ project_root: str,
503
+ format: str = "text"
504
+ ) -> int:
505
+ """
506
+ Record a step completion for a phase.
507
+
508
+ Validates step_id against the registry before recording.
509
+
510
+ Args:
511
+ story_id: Story identifier (e.g., "STORY-525")
512
+ phase: Phase ID (e.g., "02")
513
+ step_id: Step identifier (e.g., "02.1")
514
+ project_root: Project root directory
515
+ format: Output format ("text" or "json")
516
+
517
+ Returns:
518
+ Exit code: 0=recorded, 1=error
519
+ """
520
+ try:
521
+ ps = _get_phase_state(project_root)
522
+
523
+ # Validate step_id against registry (hard error if missing)
524
+ registry_path = ps._get_registry_path()
525
+ if not registry_path.exists():
526
+ msg = f"Registry not found at {registry_path}"
527
+ print(f"ERROR: {msg}", file=sys.stderr)
528
+ return 1
529
+ registry_content = registry_path.read_text(encoding="utf-8")
530
+ registry = json.loads(registry_content)
531
+ phase_data = registry.get(phase, {})
532
+ valid_step_ids = [s["id"] for s in phase_data.get("steps", [])]
533
+ if step_id not in valid_step_ids:
534
+ msg = f"Unknown step_id '{step_id}' for phase {phase}"
535
+ print(msg, file=sys.stderr)
536
+ return 1
537
+
538
+ ps.record_step(story_id, phase, step_id)
539
+
540
+ if format == "json":
541
+ print(json.dumps({
542
+ "success": True,
543
+ "story_id": story_id,
544
+ "phase": phase,
545
+ "step_id": step_id
546
+ }))
547
+ else:
548
+ print(f"Recorded step '{step_id}' for {story_id} phase {phase}")
549
+
550
+ return 0
551
+
552
+ except ValueError as e:
553
+ print(f"ERROR: {e}", file=sys.stderr)
554
+ return 1
555
+
556
+ except Exception as e:
557
+ print(f"ERROR: {e}", file=sys.stderr)
558
+ return 1
559
+
560
+
561
+ # =============================================================================
562
+ # STORY-517: QA Marker Cleanup (RCA-045 REC-3)
563
+ # =============================================================================
564
+
565
+
566
+ def cleanup_qa_markers(
567
+ story_id: str,
568
+ project_root: str,
569
+ ) -> int:
570
+ """
571
+ Remove legacy .qa-phase-N.marker files after QA completes.
572
+
573
+ Per RCA-045 REC-3: qa-phase-state.json supersedes marker files.
574
+ Old markers should be deleted during Phase 4 cleanup.
575
+
576
+ Args:
577
+ story_id: Story identifier (e.g., "STORY-517")
578
+ project_root: Project root directory
579
+
580
+ Returns:
581
+ Number of marker files deleted
582
+ """
583
+ from pathlib import Path
584
+ reports_dir = Path(project_root) / "devforgeai" / "qa" / "reports" / story_id
585
+ deleted = 0
586
+
587
+ if reports_dir.exists():
588
+ for marker in reports_dir.glob(".qa-phase-*.marker"):
589
+ marker.unlink()
590
+ deleted += 1
591
+
592
+ return deleted
593
+
594
+
595
+ # =============================================================================
596
+ # STORY-188: Observation Constants
597
+ # =============================================================================
598
+
599
+ # Observation categories (AC-4)
600
+ VALID_CATEGORIES = ["friction", "gap", "success", "pattern"]
601
+
602
+ # Observation severities (AC-5)
603
+ VALID_SEVERITIES = ["low", "medium", "high"]
604
+
605
+
606
+ def phase_observe_command(
607
+ story_id: str,
608
+ phase: str,
609
+ category: str,
610
+ note: str,
611
+ severity: str = "medium",
612
+ project_root: str = ".",
613
+ format: str = "text"
614
+ ) -> int:
615
+ """
616
+ Record a workflow observation for a phase.
617
+
618
+ Captures friction, gaps, successes, and patterns during
619
+ TDD workflow execution for AI analysis.
620
+
621
+ Args:
622
+ story_id: Story identifier (e.g., "STORY-188")
623
+ phase: Phase ID (e.g., "04")
624
+ category: Observation category (friction, gap, success, pattern)
625
+ note: Description of the observation
626
+ severity: Severity level (low, medium, high). Default: medium
627
+ project_root: Project root directory
628
+ format: Output format ("text" or "json")
629
+
630
+ Returns:
631
+ Exit code: 0=recorded, 1=not found, 2=invalid input
632
+ """
633
+ try:
634
+ # Validate category
635
+ if category not in VALID_CATEGORIES:
636
+ if format == "json":
637
+ print(json.dumps({
638
+ "success": False,
639
+ "error": f"Invalid category: '{category}'. Must be one of: {VALID_CATEGORIES}",
640
+ "story_id": story_id
641
+ }))
642
+ else:
643
+ print(f"ERROR: Invalid category '{category}'")
644
+ print(f" Valid categories: {', '.join(VALID_CATEGORIES)}")
645
+ return 2
646
+
647
+ # Validate severity
648
+ if severity not in VALID_SEVERITIES:
649
+ if format == "json":
650
+ print(json.dumps({
651
+ "success": False,
652
+ "error": f"Invalid severity: '{severity}'. Must be one of: {VALID_SEVERITIES}",
653
+ "story_id": story_id
654
+ }))
655
+ else:
656
+ print(f"ERROR: Invalid severity '{severity}'")
657
+ print(f" Valid severities: {', '.join(VALID_SEVERITIES)}")
658
+ return 2
659
+
660
+ # Validate note is not empty
661
+ if not note or not note.strip():
662
+ if format == "json":
663
+ print(json.dumps({
664
+ "success": False,
665
+ "error": "Observation note cannot be empty",
666
+ "story_id": story_id
667
+ }))
668
+ else:
669
+ print("ERROR: Observation note cannot be empty")
670
+ return 2
671
+
672
+ ps = _get_phase_state(project_root)
673
+
674
+ # Add observation
675
+ observation_id = ps.add_observation(
676
+ story_id=story_id,
677
+ phase_id=phase,
678
+ category=category,
679
+ note=note,
680
+ severity=severity
681
+ )
682
+
683
+ if observation_id is None:
684
+ if format == "json":
685
+ print(json.dumps({
686
+ "success": False,
687
+ "error": "State file not found",
688
+ "story_id": story_id
689
+ }))
690
+ else:
691
+ print(f"State file not found for {story_id}")
692
+ return 1
693
+
694
+ if format == "json":
695
+ print(json.dumps({
696
+ "success": True,
697
+ "story_id": story_id,
698
+ "phase": phase,
699
+ "category": category,
700
+ "severity": severity,
701
+ "observation_id": observation_id
702
+ }))
703
+ else:
704
+ print(f"Recorded observation for {story_id} phase {phase}")
705
+ print(f" Category: {category}")
706
+ print(f" Severity: {severity}")
707
+ print(f" ID: {observation_id}")
708
+
709
+ return 0
710
+
711
+ except ValueError as e:
712
+ if format == "json":
713
+ print(json.dumps({
714
+ "success": False,
715
+ "error": str(e),
716
+ "story_id": story_id
717
+ }))
718
+ else:
719
+ print(f"ERROR: {e}")
720
+ return 2
721
+
722
+ except Exception as e:
723
+ if format == "json":
724
+ print(json.dumps({
725
+ "success": False,
726
+ "error": str(e),
727
+ "story_id": story_id
728
+ }))
729
+ else:
730
+ print(f"ERROR: {e}")
731
+ return 2