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.
- package/CLAUDE.md +120 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/scripts/README.md +396 -0
- package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
- package/src/claude/scripts/check-hooks-fast.sh +70 -0
- package/src/claude/scripts/devforgeai-validate +6 -0
- package/src/claude/scripts/devforgeai_cli/README.md +531 -0
- package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
- package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
- package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
- package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
- package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
- package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
- package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
- package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
- package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
- package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
- package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
- package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
- package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
- package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
- package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
- package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
- package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
- package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
- package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
- package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
- package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
- package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
- package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
- package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
- package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
- package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
- package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
- package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
- package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
- package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
- package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
- package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
- package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
- package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
- package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
- package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
- package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
- package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
- package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
- package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
- package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
- package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
- package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
- package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
- package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
- package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
- package/src/claude/scripts/install_hooks.sh +186 -0
- package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
- package/src/claude/scripts/migrate-ac-headers.sh +122 -0
- package/src/claude/scripts/plan_file_kb.sh +704 -0
- package/src/claude/scripts/requirements.txt +8 -0
- package/src/claude/scripts/session_catalog.sh +543 -0
- package/src/claude/scripts/setup.py +55 -0
- package/src/claude/scripts/start-devforgeai.sh +16 -0
- package/src/claude/scripts/statusline.sh +27 -0
- package/src/claude/scripts/validate_deferrals.py +344 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
- 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
|