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,394 @@
1
+ """
2
+ Feedback indexer module for DevForgeAI.
3
+
4
+ Scans all feedback data sources in devforgeai/feedback/ and builds
5
+ a unified index at devforgeai/feedback/index.json.
6
+
7
+ Uses Python stdlib only: json, pathlib, os, re, datetime.
8
+ """
9
+
10
+ import datetime
11
+ import json
12
+ import os
13
+ import re
14
+ from pathlib import Path
15
+
16
+
17
+ # Files at the root of devforgeai/feedback/ that are NOT feedback data
18
+ _EXCLUDED_ROOT_FILES = {
19
+ "USER-GUIDE.md",
20
+ "MAINTAINER-GUIDE.md",
21
+ "GRACEFUL-DEGRADATION.md",
22
+ "RETENTION-POLICY.md",
23
+ "IMPLEMENTATION-COMPLETE.md",
24
+ "QUESTION-BANK-COMPLETION-SUMMARY.md",
25
+ "questions.md",
26
+ "config.yaml",
27
+ "schema.json",
28
+ "questions.yaml",
29
+ "question-defaults.yaml",
30
+ "index.json",
31
+ "feedback-index.json",
32
+ "feedback-register.md",
33
+ }
34
+
35
+ # Directories under devforgeai/feedback/ that are NOT feedback data
36
+ _EXCLUDED_DIRS = {"question-bank", "logs", "imported"}
37
+
38
+ # Subdirectories under ai-analysis/ to exclude
39
+ _EXCLUDED_AI_ANALYSIS_SUBDIRS = {"aggregated", "imports"}
40
+
41
+ # Regex pattern to identify artifact folder names (STORY-NNN, EPIC-NNN, RCA-NNN)
42
+ _ARTIFACT_PATTERN = re.compile(r"^(STORY|EPIC|RCA)-\d+$")
43
+
44
+ # Regex patterns for root-level report filenames
45
+ _REPORT_PATTERNS = [
46
+ re.compile(r"^code-review-.*\.md$"),
47
+ re.compile(r"^integration-test-report-.*\.md$"),
48
+ re.compile(r".*-coverage-analysis\.md$"),
49
+ re.compile(r".*-dev-complete-summary\.md$"),
50
+ ]
51
+
52
+ # Regex to extract story ID from filenames
53
+ _STORY_ID_PATTERN = re.compile(r"(STORY-\d+)")
54
+
55
+
56
+ def _file_mtime_iso(filepath: Path) -> str:
57
+ """Get file modification time as ISO 8601 string in UTC."""
58
+ mtime = os.path.getmtime(str(filepath))
59
+ dt = datetime.datetime.fromtimestamp(mtime, tz=datetime.timezone.utc)
60
+ return dt.isoformat()
61
+
62
+
63
+ def _extract_timestamp_from_json(data: dict) -> str:
64
+ """Extract timestamp from JSON data, checking multiple field locations."""
65
+ # Direct timestamp field
66
+ if "timestamp" in data and isinstance(data["timestamp"], str):
67
+ return data["timestamp"]
68
+ # analysis_date field
69
+ if "analysis_date" in data and isinstance(data["analysis_date"], str):
70
+ return data["analysis_date"]
71
+ # Nested ai_analysis.timestamp
72
+ if "ai_analysis" in data and isinstance(data["ai_analysis"], dict):
73
+ nested = data["ai_analysis"]
74
+ if "timestamp" in nested and isinstance(nested["timestamp"], str):
75
+ return nested["timestamp"]
76
+ return ""
77
+
78
+
79
+ def _is_report_filename(filename: str) -> bool:
80
+ """Check if a filename matches root-level report patterns."""
81
+ for pattern in _REPORT_PATTERNS:
82
+ if pattern.match(filename):
83
+ return True
84
+ return False
85
+
86
+
87
+ def _scan_ai_analysis(feedback_dir: Path, entries: list, errors: list) -> int:
88
+ """
89
+ Scan ai-analysis directory for JSON and MD files in artifact folders.
90
+
91
+ Returns count of files processed (including errors).
92
+ """
93
+ ai_dir = feedback_dir / "ai-analysis"
94
+ if not ai_dir.is_dir():
95
+ return 0
96
+
97
+ count = 0
98
+ for artifact_dir in sorted(ai_dir.iterdir()):
99
+ if not artifact_dir.is_dir():
100
+ continue
101
+ # Skip excluded subdirectories
102
+ if artifact_dir.name in _EXCLUDED_AI_ANALYSIS_SUBDIRS:
103
+ continue
104
+ # Only process artifact folders matching STORY-NNN, EPIC-NNN, RCA-NNN
105
+ if not _ARTIFACT_PATTERN.match(artifact_dir.name):
106
+ continue
107
+
108
+ artifact_id = artifact_dir.name
109
+
110
+ # Walk all files recursively in this artifact folder
111
+ for root, _dirs, files in os.walk(str(artifact_dir)):
112
+ root_path = Path(root)
113
+ for filename in sorted(files):
114
+ filepath = root_path / filename
115
+
116
+ # Skip index.json at any level within ai-analysis
117
+ if filename == "index.json":
118
+ continue
119
+
120
+ if filename.endswith(".json"):
121
+ count += 1
122
+ try:
123
+ with open(filepath, "r", encoding="utf-8") as f:
124
+ data = json.load(f)
125
+ except (json.JSONDecodeError, OSError) as exc:
126
+ rel_path = filepath.relative_to(feedback_dir)
127
+ errors.append(
128
+ f"Failed to parse: {rel_path}: {type(exc).__name__}"
129
+ )
130
+ continue
131
+
132
+ timestamp = _extract_timestamp_from_json(data)
133
+ if not timestamp:
134
+ timestamp = _file_mtime_iso(filepath)
135
+
136
+ rel_path = filepath.relative_to(feedback_dir)
137
+ stem = filepath.stem
138
+ entry_id = f"{artifact_id}-{stem}"
139
+
140
+ entries.append({
141
+ "id": entry_id,
142
+ "timestamp": timestamp,
143
+ "source_type": "ai-analysis",
144
+ "story-id": artifact_id,
145
+ "file-path": str(rel_path).replace("\\", "/"),
146
+ "tags": ["ai-analysis"],
147
+ })
148
+
149
+ elif filename.endswith(".md"):
150
+ count += 1
151
+ timestamp = _file_mtime_iso(filepath)
152
+ rel_path = filepath.relative_to(feedback_dir)
153
+ stem = filepath.stem
154
+ entry_id = f"{artifact_id}-{stem}"
155
+
156
+ entries.append({
157
+ "id": entry_id,
158
+ "timestamp": timestamp,
159
+ "source_type": "ai-analysis",
160
+ "story-id": artifact_id,
161
+ "file-path": str(rel_path).replace("\\", "/"),
162
+ "tags": ["ai-analysis"],
163
+ })
164
+
165
+ return count
166
+
167
+
168
+ def _scan_code_reviews(feedback_dir: Path, entries: list, errors: list) -> int:
169
+ """
170
+ Scan code-review/ and code-reviews/ directories for markdown files.
171
+
172
+ Returns count of files processed.
173
+ """
174
+ count = 0
175
+ for dir_name in ("code-review", "code-reviews"):
176
+ cr_dir = feedback_dir / dir_name
177
+ if not cr_dir.is_dir():
178
+ continue
179
+
180
+ for filepath in sorted(cr_dir.iterdir()):
181
+ if not filepath.is_file():
182
+ continue
183
+ if not filepath.name.endswith(".md"):
184
+ continue
185
+
186
+ count += 1
187
+ timestamp = _file_mtime_iso(filepath)
188
+ rel_path = filepath.relative_to(feedback_dir)
189
+ stem = filepath.stem
190
+ entry_id = f"code-review-{stem}"
191
+
192
+ # Extract story ID from filename
193
+ story_match = _STORY_ID_PATTERN.search(filepath.name)
194
+ story_id = story_match.group(1) if story_match else ""
195
+
196
+ entry = {
197
+ "id": entry_id,
198
+ "timestamp": timestamp,
199
+ "source_type": "code-review",
200
+ "file-path": str(rel_path).replace("\\", "/"),
201
+ "tags": ["code-review"],
202
+ }
203
+ if story_id:
204
+ entry["story-id"] = story_id
205
+
206
+ entries.append(entry)
207
+
208
+ return count
209
+
210
+
211
+ def _scan_root_reports(feedback_dir: Path, entries: list, errors: list) -> int:
212
+ """
213
+ Scan root-level feedback directory for report files.
214
+
215
+ Returns count of files processed.
216
+ """
217
+ count = 0
218
+ if not feedback_dir.is_dir():
219
+ return 0
220
+
221
+ for filepath in sorted(feedback_dir.iterdir()):
222
+ if not filepath.is_file():
223
+ continue
224
+ if filepath.name in _EXCLUDED_ROOT_FILES:
225
+ continue
226
+ if not filepath.name.endswith(".md"):
227
+ continue
228
+ if not _is_report_filename(filepath.name):
229
+ continue
230
+
231
+ count += 1
232
+ timestamp = _file_mtime_iso(filepath)
233
+ rel_path = filepath.relative_to(feedback_dir)
234
+ stem = filepath.stem
235
+ entry_id = f"report-{stem}"
236
+
237
+ # Extract story ID from filename
238
+ story_match = _STORY_ID_PATTERN.search(filepath.name)
239
+ story_id = story_match.group(1) if story_match else ""
240
+
241
+ entry = {
242
+ "id": entry_id,
243
+ "timestamp": timestamp,
244
+ "source_type": "report",
245
+ "file-path": str(rel_path).replace("\\", "/"),
246
+ "tags": ["report"],
247
+ }
248
+ if story_id:
249
+ entry["story-id"] = story_id
250
+
251
+ entries.append(entry)
252
+
253
+ return count
254
+
255
+
256
+ def _scan_sessions(feedback_dir: Path, entries: list, errors: list) -> int:
257
+ """
258
+ Scan sessions/ directory for markdown files (backward compatibility).
259
+
260
+ Returns count of files processed. Silently skips if directory doesn't exist.
261
+ """
262
+ sessions_dir = feedback_dir / "sessions"
263
+ if not sessions_dir.is_dir():
264
+ return 0
265
+
266
+ count = 0
267
+ for filepath in sorted(sessions_dir.iterdir()):
268
+ if not filepath.is_file():
269
+ continue
270
+ if not filepath.name.endswith(".md"):
271
+ continue
272
+
273
+ count += 1
274
+ timestamp = _file_mtime_iso(filepath)
275
+ rel_path = filepath.relative_to(feedback_dir)
276
+ stem = filepath.stem
277
+ entry_id = f"session-{stem}"
278
+
279
+ entries.append({
280
+ "id": entry_id,
281
+ "timestamp": timestamp,
282
+ "source_type": "session",
283
+ "file-path": str(rel_path).replace("\\", "/"),
284
+ "tags": ["session"],
285
+ })
286
+
287
+ return count
288
+
289
+
290
+ def reindex_all_feedback(project_root: str, output_format: str = "text") -> int:
291
+ """
292
+ Scan all feedback sources and build unified index.
293
+
294
+ Args:
295
+ project_root: Path to project root directory.
296
+ output_format: 'text' or 'json'.
297
+
298
+ Returns:
299
+ Exit code: 0 for success, 1 for error.
300
+
301
+ Side effects:
302
+ - Writes devforgeai/feedback/index.json
303
+ - Prints results to stdout
304
+ """
305
+ root = Path(project_root)
306
+ feedback_dir = root / "devforgeai" / "feedback"
307
+
308
+ if not feedback_dir.is_dir():
309
+ if output_format == "json":
310
+ print(json.dumps({
311
+ "status": "error",
312
+ "message": f"Feedback directory not found: {feedback_dir}",
313
+ }))
314
+ else:
315
+ print(f"Error: Feedback directory not found: {feedback_dir}")
316
+ return 1
317
+
318
+ entries: list = []
319
+ errors: list = []
320
+
321
+ # Scan all sources
322
+ ai_count = _scan_ai_analysis(feedback_dir, entries, errors)
323
+ cr_count = _scan_code_reviews(feedback_dir, entries, errors)
324
+ report_count = _scan_root_reports(feedback_dir, entries, errors)
325
+ session_count = _scan_sessions(feedback_dir, entries, errors)
326
+
327
+ # Sort entries by timestamp (newest first)
328
+ entries.sort(key=lambda e: e.get("timestamp", ""), reverse=True)
329
+
330
+ # Build source summary
331
+ source_summary = {
332
+ "ai-analysis": sum(1 for e in entries if e["source_type"] == "ai-analysis"),
333
+ "session": sum(1 for e in entries if e["source_type"] == "session"),
334
+ "code-review": sum(1 for e in entries if e["source_type"] == "code-review"),
335
+ "report": sum(1 for e in entries if e["source_type"] == "report"),
336
+ }
337
+
338
+ total_files = ai_count + cr_count + report_count + session_count
339
+ indexed_count = len(entries)
340
+ error_count = len(errors)
341
+
342
+ # Build index
343
+ now_utc = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
344
+ index = {
345
+ "version": "2.0",
346
+ "last-updated": now_utc,
347
+ "feedback-sessions": entries,
348
+ "source_summary": source_summary,
349
+ }
350
+
351
+ # Write index file
352
+ index_path = feedback_dir / "index.json"
353
+ try:
354
+ with open(index_path, "w", encoding="utf-8") as f:
355
+ json.dump(index, f, indent=2)
356
+ except OSError as exc:
357
+ if output_format == "json":
358
+ print(json.dumps({
359
+ "status": "error",
360
+ "message": f"Failed to write index: {exc}",
361
+ }))
362
+ else:
363
+ print(f"Error: Failed to write index: {exc}")
364
+ return 1
365
+
366
+ # Print results
367
+ if output_format == "json":
368
+ result = {
369
+ "status": "success",
370
+ "total_files": total_files,
371
+ "indexed_count": indexed_count,
372
+ "error_count": error_count,
373
+ "sources_scanned": source_summary,
374
+ "errors": errors,
375
+ "version": "2.0",
376
+ }
377
+ print(json.dumps(result))
378
+ else:
379
+ print("\u2705 Reindex completed successfully")
380
+ print()
381
+ print(f"Total files processed: {total_files}")
382
+ print(f"Successfully indexed: {indexed_count}")
383
+ print(f"Errors encountered: {error_count}")
384
+ print()
385
+ print("Sources scanned:")
386
+ print(f" AI analysis files: {source_summary['ai-analysis']}")
387
+ print(f" Code review files: {source_summary['code-review']}")
388
+ print(f" Root report files: {source_summary['report']}")
389
+ print(f" Session files: {source_summary['session']}")
390
+ print()
391
+ print(f"Index file: devforgeai/feedback/index.json")
392
+ print(f"Index version: 2.0")
393
+
394
+ return 0
@@ -0,0 +1,226 @@
1
+ """
2
+ Hot-reload system for feedback configuration.
3
+
4
+ This module provides file watching and automatic configuration reloading
5
+ when the configuration file changes.
6
+ """
7
+
8
+ import threading
9
+ import time
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Optional, Callable, Any, NamedTuple
13
+ from datetime import datetime
14
+
15
+
16
+ class FileInfo(NamedTuple):
17
+ """File information (modification time and size)."""
18
+ mtime: Optional[float]
19
+ size: Optional[int]
20
+
21
+
22
+ class ConfigFileWatcher:
23
+ """Watches configuration file for changes and triggers reloads.
24
+
25
+ Monitors devforgeai/config/feedback.yaml for changes and calls
26
+ a callback function when modifications are detected.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ config_file: Path,
32
+ on_change_callback: Callable[[Path], None],
33
+ poll_interval: float = 0.5,
34
+ detection_timeout: float = 5.0
35
+ ):
36
+ """Initialize the file watcher.
37
+
38
+ Args:
39
+ config_file: Path to the configuration file to watch.
40
+ on_change_callback: Function to call when file changes.
41
+ Called with Path to the changed file.
42
+ poll_interval: How often to check for changes (seconds).
43
+ detection_timeout: Maximum time to detect change (seconds).
44
+ """
45
+ self.config_file = config_file
46
+ self.on_change_callback = on_change_callback
47
+ self.poll_interval = poll_interval
48
+ self.detection_timeout = detection_timeout
49
+ self._last_file_info: Optional[FileInfo] = None
50
+ self._watch_thread: Optional[threading.Thread] = None
51
+ self._stop_event = threading.Event()
52
+ self._is_running = False
53
+ self._lock = threading.Lock()
54
+
55
+ def _get_file_info(self) -> FileInfo:
56
+ """Get file modification time and size.
57
+
58
+ Returns:
59
+ FileInfo with mtime and size, or (None, None) if file doesn't exist.
60
+ """
61
+ try:
62
+ if self.config_file.exists():
63
+ stat = self.config_file.stat()
64
+ return FileInfo(stat.st_mtime, stat.st_size)
65
+ except (OSError, IOError):
66
+ pass
67
+ return FileInfo(None, None)
68
+
69
+ def _watch_loop(self) -> None:
70
+ """Main file watching loop (runs in thread)."""
71
+ # Initialize baseline
72
+ self._last_file_info = self._get_file_info()
73
+
74
+ while not self._stop_event.is_set():
75
+ try:
76
+ time.sleep(self.poll_interval)
77
+
78
+ current_file_info = self._get_file_info()
79
+
80
+ # Check if file changed
81
+ if self._has_file_changed(current_file_info):
82
+ # File changed - trigger callback
83
+ try:
84
+ self.on_change_callback(self.config_file)
85
+ except Exception:
86
+ # Silently ignore callback errors
87
+ pass
88
+ self._last_file_info = current_file_info
89
+
90
+ except Exception:
91
+ # Silently ignore watch loop errors
92
+ pass
93
+
94
+ def _has_file_changed(self, current_info: FileInfo) -> bool:
95
+ """Check if file information has changed.
96
+
97
+ Args:
98
+ current_info: Current file information.
99
+
100
+ Returns:
101
+ True if file has changed, False otherwise.
102
+ """
103
+ if self._last_file_info is None:
104
+ return False
105
+
106
+ return (current_info.mtime is not None and
107
+ self._last_file_info.mtime is not None and
108
+ (current_info.mtime != self._last_file_info.mtime or
109
+ current_info.size != self._last_file_info.size))
110
+
111
+ def start(self) -> None:
112
+ """Start watching the configuration file."""
113
+ with self._lock:
114
+ if self._is_running:
115
+ return
116
+
117
+ self._stop_event.clear()
118
+ self._watch_thread = threading.Thread(target=self._watch_loop, daemon=True)
119
+ self._watch_thread.start()
120
+ self._is_running = True
121
+
122
+ def stop(self) -> None:
123
+ """Stop watching the configuration file."""
124
+ with self._lock:
125
+ if not self._is_running:
126
+ return
127
+
128
+ self._stop_event.set()
129
+ if self._watch_thread and self._watch_thread.is_alive():
130
+ self._watch_thread.join(timeout=1.0)
131
+ self._is_running = False
132
+
133
+ def is_running(self) -> bool:
134
+ """Check if watcher is currently running.
135
+
136
+ Returns:
137
+ True if watcher is active, False otherwise.
138
+ """
139
+ with self._lock:
140
+ return self._is_running
141
+
142
+
143
+ class HotReloadManager:
144
+ """Manages configuration hot-reload lifecycle.
145
+
146
+ Coordinates file watching and configuration updates.
147
+ """
148
+
149
+ def __init__(
150
+ self,
151
+ config_file: Path,
152
+ load_config_callback: Callable[[], Any]
153
+ ):
154
+ """Initialize hot-reload manager.
155
+
156
+ Args:
157
+ config_file: Path to the configuration file.
158
+ load_config_callback: Function that loads and returns new configuration.
159
+ Called when file changes are detected.
160
+ """
161
+ self.config_file = config_file
162
+ self.load_config_callback = load_config_callback
163
+ self._watcher: Optional[ConfigFileWatcher] = None
164
+ self._lock = threading.Lock()
165
+ self._current_config: Optional[Any] = None
166
+
167
+ def _on_config_change(self, changed_file: Path) -> None:
168
+ """Handle configuration file change.
169
+
170
+ Args:
171
+ changed_file: Path to the changed file.
172
+ """
173
+ try:
174
+ with self._lock:
175
+ # Load new configuration
176
+ new_config = self.load_config_callback()
177
+ self._current_config = new_config
178
+ except Exception:
179
+ # Keep previous valid configuration on error
180
+ pass
181
+
182
+ def start(self) -> None:
183
+ """Start the hot-reload manager."""
184
+ with self._lock:
185
+ if self._watcher is not None:
186
+ return
187
+
188
+ self._watcher = ConfigFileWatcher(
189
+ self.config_file,
190
+ self._on_config_change
191
+ )
192
+ self._watcher.start()
193
+
194
+ def stop(self) -> None:
195
+ """Stop the hot-reload manager."""
196
+ with self._lock:
197
+ if self._watcher is not None:
198
+ self._watcher.stop()
199
+ self._watcher = None
200
+
201
+ def is_running(self) -> bool:
202
+ """Check if hot-reload is active.
203
+
204
+ Returns:
205
+ True if hot-reload manager is running, False otherwise.
206
+ """
207
+ with self._lock:
208
+ return self._watcher is not None and self._watcher.is_running()
209
+
210
+ def get_current_config(self) -> Optional[Any]:
211
+ """Get the current loaded configuration.
212
+
213
+ Returns:
214
+ Current configuration object or None if not loaded.
215
+ """
216
+ with self._lock:
217
+ return self._current_config
218
+
219
+ def set_current_config(self, config: Any) -> None:
220
+ """Set the current configuration (used during initialization).
221
+
222
+ Args:
223
+ config: Configuration object to set as current.
224
+ """
225
+ with self._lock:
226
+ self._current_config = config