@kennethsolomon/shipkit 1.0.0
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/README.md +321 -0
- package/bin/shipkit.js +146 -0
- package/commands/sk/brainstorm.md +63 -0
- package/commands/sk/branch.md +35 -0
- package/commands/sk/config.md +96 -0
- package/commands/sk/execute-plan.md +85 -0
- package/commands/sk/features.md +238 -0
- package/commands/sk/finish-feature.md +154 -0
- package/commands/sk/help.md +103 -0
- package/commands/sk/hotfix.md +61 -0
- package/commands/sk/plan.md +30 -0
- package/commands/sk/release.md +72 -0
- package/commands/sk/security-check.md +188 -0
- package/commands/sk/set-profile.md +71 -0
- package/commands/sk/status.md +25 -0
- package/commands/sk/update-task.md +35 -0
- package/commands/sk/write-plan.md +72 -0
- package/package.json +23 -0
- package/skills/sk:accessibility/LICENSE.txt +177 -0
- package/skills/sk:accessibility/SKILL.md +150 -0
- package/skills/sk:api-design/LICENSE.txt +177 -0
- package/skills/sk:api-design/SKILL.md +158 -0
- package/skills/sk:brainstorming/SKILL.md +124 -0
- package/skills/sk:debug/SKILL.md +252 -0
- package/skills/sk:debug/debug_conductor.py +177 -0
- package/skills/sk:debug/lib/__init__.py +1 -0
- package/skills/sk:debug/lib/bug_gatherer.py +55 -0
- package/skills/sk:debug/lib/context_reader.py +139 -0
- package/skills/sk:debug/lib/findings_writer.py +76 -0
- package/skills/sk:debug/lib/lessons_writer.py +165 -0
- package/skills/sk:debug/lib/step_runner.py +326 -0
- package/skills/sk:features/SKILL.md +238 -0
- package/skills/sk:frontend-design/LICENSE.txt +177 -0
- package/skills/sk:frontend-design/SKILL.md +191 -0
- package/skills/sk:laravel-init/SKILL.md +37 -0
- package/skills/sk:laravel-new/SKILL.md +68 -0
- package/skills/sk:lint/SKILL.md +113 -0
- package/skills/sk:perf/LICENSE.txt +177 -0
- package/skills/sk:perf/SKILL.md +188 -0
- package/skills/sk:release/SKILL.md +113 -0
- package/skills/sk:release/references/android-checklist.md +269 -0
- package/skills/sk:release/references/ios-checklist.md +339 -0
- package/skills/sk:release/release.sh +378 -0
- package/skills/sk:review/SKILL.md +346 -0
- package/skills/sk:review/references/security-checklist.md +223 -0
- package/skills/sk:schema-migrate/SKILL.md +125 -0
- package/skills/sk:schema-migrate/orms/drizzle.md +546 -0
- package/skills/sk:schema-migrate/orms/laravel.md +367 -0
- package/skills/sk:schema-migrate/orms/prisma.md +357 -0
- package/skills/sk:schema-migrate/orms/rails.md +351 -0
- package/skills/sk:schema-migrate/orms/sqlalchemy.md +385 -0
- package/skills/sk:schema-migrate/references/detection.md +110 -0
- package/skills/sk:setup-claude/SKILL.md +365 -0
- package/skills/sk:setup-claude/references/detection.md +6 -0
- package/skills/sk:setup-claude/references/templates.md +11 -0
- package/skills/sk:setup-claude/scripts/apply_setup_claude.py +443 -0
- package/skills/sk:setup-claude/scripts/detect_arch_changes.py +437 -0
- package/skills/sk:setup-claude/templates/.claude/docs/arch-changelog-guide.md.template +6 -0
- package/skills/sk:setup-claude/templates/.claude/docs/changelog-guide.md.template +12 -0
- package/skills/sk:setup-claude/templates/CHANGELOG.md.template +21 -0
- package/skills/sk:setup-claude/templates/CLAUDE.md.template +299 -0
- package/skills/sk:setup-claude/templates/arch-changelog-guide.md.template +3 -0
- package/skills/sk:setup-claude/templates/changelog-guide.md.template +3 -0
- package/skills/sk:setup-claude/templates/commands/brainstorm.md.template +74 -0
- package/skills/sk:setup-claude/templates/commands/execute-plan.md.template +57 -0
- package/skills/sk:setup-claude/templates/commands/features.md.template +238 -0
- package/skills/sk:setup-claude/templates/commands/finish-feature.md.template +155 -0
- package/skills/sk:setup-claude/templates/commands/plan.md.template +30 -0
- package/skills/sk:setup-claude/templates/commands/re-setup.md.template +38 -0
- package/skills/sk:setup-claude/templates/commands/release.md.template +74 -0
- package/skills/sk:setup-claude/templates/commands/security-check.md.template +172 -0
- package/skills/sk:setup-claude/templates/commands/status.md.template +17 -0
- package/skills/sk:setup-claude/templates/commands/write-plan.md.template +34 -0
- package/skills/sk:setup-claude/templates/finish-feature.md.template +3 -0
- package/skills/sk:setup-claude/templates/plan.md.template +3 -0
- package/skills/sk:setup-claude/templates/status.md.template +3 -0
- package/skills/sk:setup-claude/templates/tasks/findings.md.template +19 -0
- package/skills/sk:setup-claude/templates/tasks/lessons.md.template +26 -0
- package/skills/sk:setup-claude/templates/tasks/progress.md.template +20 -0
- package/skills/sk:setup-claude/templates/tasks/security-findings.md.template +5 -0
- package/skills/sk:setup-claude/templates/tasks/todo.md.template +26 -0
- package/skills/sk:setup-claude/templates/tasks/workflow-status.md.template +31 -0
- package/skills/sk:setup-claude/templates/tasks-findings.md.template +3 -0
- package/skills/sk:setup-claude/templates/tasks-lessons.md.template +3 -0
- package/skills/sk:setup-claude/templates/tasks-progress.md.template +3 -0
- package/skills/sk:setup-claude/templates/tasks-todo.md.template +3 -0
- package/skills/sk:setup-claude/tests/test_apply_setup_claude.py +193 -0
- package/skills/sk:setup-optimizer/SKILL.md +184 -0
- package/skills/sk:setup-optimizer/lib/__init__.py +24 -0
- package/skills/sk:setup-optimizer/lib/detect.py +205 -0
- package/skills/sk:setup-optimizer/lib/discover.py +221 -0
- package/skills/sk:setup-optimizer/lib/enrich.py +163 -0
- package/skills/sk:setup-optimizer/lib/merge.py +277 -0
- package/skills/sk:setup-optimizer/lib/sidecar.py +129 -0
- package/skills/sk:setup-optimizer/optimize_claude.py +174 -0
- package/skills/sk:setup-optimizer/templates/CLAUDE.md.template +105 -0
- package/skills/sk:skill-creator/LICENSE.txt +202 -0
- package/skills/sk:skill-creator/SKILL.md +479 -0
- package/skills/sk:skill-creator/agents/analyzer.md +274 -0
- package/skills/sk:skill-creator/agents/comparator.md +202 -0
- package/skills/sk:skill-creator/agents/grader.md +223 -0
- package/skills/sk:skill-creator/assets/eval_review.html +146 -0
- package/skills/sk:skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/sk:skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/sk:skill-creator/references/schemas.md +430 -0
- package/skills/sk:skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/sk:skill-creator/scripts/generate_report.py +326 -0
- package/skills/sk:skill-creator/scripts/improve_description.py +248 -0
- package/skills/sk:skill-creator/scripts/package_skill.py +136 -0
- package/skills/sk:skill-creator/scripts/quick_validate.py +103 -0
- package/skills/sk:skill-creator/scripts/run_eval.py +310 -0
- package/skills/sk:skill-creator/scripts/run_loop.py +332 -0
- package/skills/sk:skill-creator/scripts/utils.py +47 -0
- package/skills/sk:smart-commit/SKILL.md +175 -0
- package/skills/sk:test/SKILL.md +171 -0
- package/skills/sk:write-tests/SKILL.md +195 -0
- package/skills/sk:write-tests/references/patterns.md +209 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Safe writer for tasks/findings.md - appends findings without overwriting."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FindingsWriter:
|
|
9
|
+
"""Append findings to tasks/findings.md without overwriting."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, root: Path = None):
|
|
12
|
+
self.root = root or Path.cwd()
|
|
13
|
+
self.findings_path = self.root / 'tasks' / 'findings.md'
|
|
14
|
+
|
|
15
|
+
def write_findings(self, state: Dict) -> bool:
|
|
16
|
+
"""Write findings entry from debug state."""
|
|
17
|
+
bug_info = state.get('bug_info', {})
|
|
18
|
+
hypotheses = state.get('hypotheses', [])
|
|
19
|
+
root_cause = state.get('root_cause_confirmed', '')
|
|
20
|
+
|
|
21
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
22
|
+
bug_summary = bug_info.get('description', 'Unknown')
|
|
23
|
+
|
|
24
|
+
# Build the entry
|
|
25
|
+
entry = f"\n## {now} — Bug: {bug_summary}\n\n"
|
|
26
|
+
entry += f"**Symptom:** {bug_info.get('actual_behavior', 'Unknown')}\n\n"
|
|
27
|
+
entry += f"**Root cause:** {root_cause}\n\n"
|
|
28
|
+
|
|
29
|
+
# Add hypotheses tested
|
|
30
|
+
if hypotheses:
|
|
31
|
+
entry += "**Hypotheses tested:**\n"
|
|
32
|
+
for h in hypotheses:
|
|
33
|
+
status = h.get('status', 'UNKNOWN')
|
|
34
|
+
entry += f"- H{h['rank']}: {h['description']} → **{status}**\n"
|
|
35
|
+
if h.get('test_results'):
|
|
36
|
+
entry += f" Result: {h['test_results']}\n"
|
|
37
|
+
entry += "\n"
|
|
38
|
+
|
|
39
|
+
entry += f"**Status:** Investigation complete\n"
|
|
40
|
+
|
|
41
|
+
# Write to file
|
|
42
|
+
try:
|
|
43
|
+
self._ensure_file_exists()
|
|
44
|
+
self._append_entry(entry)
|
|
45
|
+
print(f"\n✅ Findings written to {self.findings_path}")
|
|
46
|
+
return True
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"\n❌ Error writing findings: {e}")
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def _ensure_file_exists(self):
|
|
52
|
+
"""Create tasks/findings.md if missing with template."""
|
|
53
|
+
if not self.findings_path.exists():
|
|
54
|
+
self.findings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
template = """# Bug Findings
|
|
56
|
+
|
|
57
|
+
Investigations of bugs found during development.
|
|
58
|
+
|
|
59
|
+
## Entry Format
|
|
60
|
+
|
|
61
|
+
Each entry includes:
|
|
62
|
+
- **Date and summary** — When and what
|
|
63
|
+
- **Symptom** — What the user reported
|
|
64
|
+
- **Root cause** — What was actually wrong
|
|
65
|
+
- **Hypotheses tested** — What we checked
|
|
66
|
+
- **Status** — Investigation stage
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
self.findings_path.write_text(template)
|
|
72
|
+
|
|
73
|
+
def _append_entry(self, entry: str):
|
|
74
|
+
"""Append entry to findings.md without overwriting."""
|
|
75
|
+
current = self.findings_path.read_text()
|
|
76
|
+
self.findings_path.write_text(current + entry + "\n")
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Safe writer for tasks/lessons.md - appends lessons only when pattern detected."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LessonsWriter:
|
|
9
|
+
"""Append lessons to tasks/lessons.md only when recurrence risk detected."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, root: Path = None):
|
|
12
|
+
self.root = root or Path.cwd()
|
|
13
|
+
self.lessons_path = self.root / 'tasks' / 'lessons.md'
|
|
14
|
+
|
|
15
|
+
def should_create_lesson(self, state: Dict) -> bool:
|
|
16
|
+
"""Determine if this bug warrants a lesson (recurrence risk)."""
|
|
17
|
+
root_cause = state.get('root_cause_confirmed', '')
|
|
18
|
+
|
|
19
|
+
if not root_cause:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
# Skip lessons for one-off issues:
|
|
23
|
+
skip_patterns = [
|
|
24
|
+
'typo', 'spelling', 'copy-paste',
|
|
25
|
+
'environment variable not set',
|
|
26
|
+
'hardware failure', 'network timeout',
|
|
27
|
+
'race condition (timing)',
|
|
28
|
+
'one-off',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
for pattern in skip_patterns:
|
|
32
|
+
if pattern.lower() in root_cause.lower():
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
# Create lesson for systematic issues that could recur:
|
|
36
|
+
# - Logic errors
|
|
37
|
+
# - Missing validation
|
|
38
|
+
# - Architectural oversights
|
|
39
|
+
# - Performance issues
|
|
40
|
+
# - Missing error handling
|
|
41
|
+
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
def write_lesson(self, state: Dict) -> bool:
|
|
45
|
+
"""Write lesson entry from debug state."""
|
|
46
|
+
if not self.should_create_lesson(state):
|
|
47
|
+
print("\n⏭️ Skipping lesson (one-off issue, pattern unlikely to recur)")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
bug_info = state.get('bug_info', {})
|
|
51
|
+
root_cause = state.get('root_cause_confirmed', '')
|
|
52
|
+
|
|
53
|
+
date = datetime.now().strftime("%Y-%m-%d")
|
|
54
|
+
bug_symptom = bug_info.get('description', 'Unknown')
|
|
55
|
+
prevention = self._derive_prevention(state)
|
|
56
|
+
|
|
57
|
+
# Build the lesson entry
|
|
58
|
+
entry = f"\n### [{date}] {self._generate_title(root_cause)}\n\n"
|
|
59
|
+
entry += f"**Bug:** {bug_symptom}\n\n"
|
|
60
|
+
entry += f"**Root cause:** {root_cause}\n\n"
|
|
61
|
+
entry += f"**Prevention:** {prevention}\n"
|
|
62
|
+
|
|
63
|
+
# Write to file
|
|
64
|
+
try:
|
|
65
|
+
self._ensure_file_exists()
|
|
66
|
+
self._append_lesson(entry)
|
|
67
|
+
print(f"\n✅ New lesson added to {self.lessons_path}")
|
|
68
|
+
return True
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"\n❌ Error writing lesson: {e}")
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def _ensure_file_exists(self):
|
|
74
|
+
"""Create tasks/lessons.md if missing with template."""
|
|
75
|
+
if not self.lessons_path.exists():
|
|
76
|
+
self.lessons_path.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
template = """# Lessons Learned
|
|
78
|
+
|
|
79
|
+
Patterns that caused bugs and prevention rules to avoid them in the future.
|
|
80
|
+
|
|
81
|
+
## Entry Format
|
|
82
|
+
|
|
83
|
+
Each lesson includes:
|
|
84
|
+
- **[Date] Title** — When and what we learned
|
|
85
|
+
- **Bug** — What went wrong
|
|
86
|
+
- **Root cause** — Why it happened
|
|
87
|
+
- **Prevention** — How to avoid it next time
|
|
88
|
+
|
|
89
|
+
Lessons are applied as standing constraints by:
|
|
90
|
+
- `/brainstorm` — Applies to requirement exploration
|
|
91
|
+
- `/write-plan` — Applies to plan creation
|
|
92
|
+
- `/execute-plan` — Applies as implementation constraints
|
|
93
|
+
- `/write-tests` — Applies to test patterns
|
|
94
|
+
- `/review` — Applies to code review checks
|
|
95
|
+
- `/finish-feature` — Applies as merge gate checks
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Active Lessons
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
self.lessons_path.write_text(template)
|
|
103
|
+
|
|
104
|
+
def _append_lesson(self, entry: str):
|
|
105
|
+
"""Append lesson to lessons.md in the Active Lessons section."""
|
|
106
|
+
content = self.lessons_path.read_text()
|
|
107
|
+
|
|
108
|
+
# Find "## Active Lessons" marker
|
|
109
|
+
marker = "## Active Lessons\n"
|
|
110
|
+
if marker in content:
|
|
111
|
+
# Insert after the marker
|
|
112
|
+
parts = content.split(marker)
|
|
113
|
+
self.lessons_path.write_text(
|
|
114
|
+
parts[0] + marker + "\n" + entry + "\n" + parts[1]
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
# Fallback: append at end
|
|
118
|
+
self.lessons_path.write_text(content + entry + "\n")
|
|
119
|
+
|
|
120
|
+
def _derive_prevention(self, state: Dict) -> str:
|
|
121
|
+
"""Suggest prevention rule from root cause."""
|
|
122
|
+
root_cause = state.get('root_cause_confirmed', '').lower()
|
|
123
|
+
|
|
124
|
+
# Simple heuristics to suggest prevention
|
|
125
|
+
if 'validation' in root_cause:
|
|
126
|
+
return "Always validate input at system boundaries (user input, API calls, file uploads) before processing. Check type, range, format, and size."
|
|
127
|
+
|
|
128
|
+
elif 'race condition' in root_cause:
|
|
129
|
+
return "Use locks or atomic operations for concurrent access to shared state. Add tests for concurrent scenarios."
|
|
130
|
+
|
|
131
|
+
elif 'type' in root_cause or 'coercion' in root_cause:
|
|
132
|
+
return "Enable and enforce strict type checking. Avoid implicit type coercion. Use type hints/annotations."
|
|
133
|
+
|
|
134
|
+
elif 'error handling' in root_cause or 'exception' in root_cause:
|
|
135
|
+
return "Always handle exceptions explicitly. Don't silently catch and ignore errors. Log unexpected conditions."
|
|
136
|
+
|
|
137
|
+
elif 'null' in root_cause or 'undefined' in root_cause:
|
|
138
|
+
return "Check for null/undefined values before accessing properties. Use optional chaining or default values."
|
|
139
|
+
|
|
140
|
+
elif 'memory' in root_cause or 'resource' in root_cause:
|
|
141
|
+
return "Always clean up resources (close files, connections, listeners). Use try-finally or context managers."
|
|
142
|
+
|
|
143
|
+
elif 'performance' in root_cause or 'slow' in root_cause:
|
|
144
|
+
return "Profile before optimizing. Add monitoring and alerts for performance regressions. Include performance tests."
|
|
145
|
+
|
|
146
|
+
elif 'security' in root_cause or 'injection' in root_cause:
|
|
147
|
+
return "Apply input validation and output encoding. Use parameterized queries. Never trust user input. Review OWASP top 10."
|
|
148
|
+
|
|
149
|
+
else:
|
|
150
|
+
# Generic prevention
|
|
151
|
+
return f"Pattern identified: {root_cause[:60]}... Review root cause and implement safeguard in code review checklist."
|
|
152
|
+
|
|
153
|
+
def _generate_title(self, root_cause: str) -> str:
|
|
154
|
+
"""Generate lesson title from root cause."""
|
|
155
|
+
# Try to create a concise title
|
|
156
|
+
title = root_cause
|
|
157
|
+
|
|
158
|
+
# Limit length
|
|
159
|
+
if len(title) > 60:
|
|
160
|
+
title = title[:57] + "..."
|
|
161
|
+
|
|
162
|
+
# Capitalize first letter
|
|
163
|
+
title = title[0].upper() + title[1:] if title else "Bug Lesson"
|
|
164
|
+
|
|
165
|
+
return title
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Execute individual debug steps (3-11)."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StepRunner:
|
|
10
|
+
"""Execute each debugging step."""
|
|
11
|
+
|
|
12
|
+
def execute(self, step, state: Dict) -> Dict:
|
|
13
|
+
"""Execute a specific step and return results."""
|
|
14
|
+
step_num = step.value
|
|
15
|
+
|
|
16
|
+
if step_num == 3:
|
|
17
|
+
return self._step_3_check_changes()
|
|
18
|
+
elif step_num == 4:
|
|
19
|
+
return self._step_4_reproduce(state)
|
|
20
|
+
elif step_num == 5:
|
|
21
|
+
return self._step_5_isolate(state)
|
|
22
|
+
elif step_num == 6:
|
|
23
|
+
return self._step_6_form_hypotheses(state)
|
|
24
|
+
elif step_num == 7:
|
|
25
|
+
return self._step_7_test_hypotheses(state)
|
|
26
|
+
elif step_num == 8:
|
|
27
|
+
return self._step_8_update_findings(state)
|
|
28
|
+
elif step_num == 9:
|
|
29
|
+
return self._step_9_propose_fix(state)
|
|
30
|
+
elif step_num == 10:
|
|
31
|
+
return self._step_10_verify_fix(state)
|
|
32
|
+
elif step_num == 11:
|
|
33
|
+
return self._step_11_document(state)
|
|
34
|
+
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
def _step_3_check_changes(self) -> Dict:
|
|
38
|
+
"""Check recent git changes."""
|
|
39
|
+
print("Checking recent git commits and diffs...\n")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Recent commits
|
|
43
|
+
print("📋 Recent commits:")
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
"git log --oneline -10",
|
|
46
|
+
shell=True,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True
|
|
49
|
+
)
|
|
50
|
+
print(result.stdout)
|
|
51
|
+
|
|
52
|
+
# Recent diffs
|
|
53
|
+
print("\n📋 Recent file changes:")
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
"git diff HEAD~3 --stat",
|
|
56
|
+
shell=True,
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True
|
|
59
|
+
)
|
|
60
|
+
print(result.stdout)
|
|
61
|
+
|
|
62
|
+
return {'changes_reviewed': True}
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"⚠️ Could not check git history: {e}")
|
|
66
|
+
return {'changes_reviewed': False}
|
|
67
|
+
|
|
68
|
+
def _step_4_reproduce(self, state: Dict) -> Dict:
|
|
69
|
+
"""Reproduce the bug (CLI or browser)."""
|
|
70
|
+
bug_info = state['bug_info']
|
|
71
|
+
|
|
72
|
+
print("Now let's reproduce the bug to confirm it exists.\n")
|
|
73
|
+
|
|
74
|
+
# Detect if likely browser bug
|
|
75
|
+
is_browser_bug = self._detect_browser_bug(bug_info)
|
|
76
|
+
|
|
77
|
+
if is_browser_bug:
|
|
78
|
+
return self._reproduce_browser_bug(state)
|
|
79
|
+
else:
|
|
80
|
+
return self._reproduce_cli_bug(state)
|
|
81
|
+
|
|
82
|
+
def _detect_browser_bug(self, bug_info: Dict) -> bool:
|
|
83
|
+
"""Heuristic: is this a browser/UI bug?"""
|
|
84
|
+
keywords = [
|
|
85
|
+
'visual', 'browser', 'javascript', 'console', 'render',
|
|
86
|
+
'DOM', 'click', 'button', 'form', 'UI', 'page', 'screen'
|
|
87
|
+
]
|
|
88
|
+
text = (
|
|
89
|
+
bug_info.get('description', '') + ' ' +
|
|
90
|
+
bug_info.get('error_message', '')
|
|
91
|
+
).lower()
|
|
92
|
+
return any(kw in text for kw in keywords)
|
|
93
|
+
|
|
94
|
+
def _reproduce_cli_bug(self, state: Dict) -> Dict:
|
|
95
|
+
"""Reproduce CLI/server bug via bash command."""
|
|
96
|
+
print("📝 Provide the bash command that triggers the bug:")
|
|
97
|
+
print(" (Leave blank if you'd rather describe manually)\n")
|
|
98
|
+
|
|
99
|
+
command = input("> ").strip()
|
|
100
|
+
|
|
101
|
+
if not command:
|
|
102
|
+
print("\n⚠️ Cannot reproduce without a command.")
|
|
103
|
+
print(" Please provide the exact command/steps to trigger the bug.")
|
|
104
|
+
return {'reproduced': False}
|
|
105
|
+
|
|
106
|
+
print(f"\n🔧 Running: {command}\n")
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
command,
|
|
111
|
+
shell=True,
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
timeout=30
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
output = result.stdout + result.stderr
|
|
118
|
+
print(output)
|
|
119
|
+
|
|
120
|
+
print("\n✅ Bug reproduced successfully!")
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
'reproduced': True,
|
|
124
|
+
'bug_type': 'cli',
|
|
125
|
+
'reproduction_command': command,
|
|
126
|
+
'reproduction_output': output[:500], # First 500 chars
|
|
127
|
+
'evidence_collected': [f"Reproduced with: {command}"]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
except subprocess.TimeoutExpired:
|
|
131
|
+
print("\n⚠️ Command timed out (30s)")
|
|
132
|
+
return {'reproduced': False}
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"\n❌ Error: {e}")
|
|
135
|
+
return {'reproduced': False}
|
|
136
|
+
|
|
137
|
+
def _reproduce_browser_bug(self, state: Dict) -> Dict:
|
|
138
|
+
"""Reproduce browser/UI bug using Playwright (if available)."""
|
|
139
|
+
print("🌐 Browser bug detected. Provide URL where bug occurs:")
|
|
140
|
+
print(" (e.g., http://localhost:3000/page)\n")
|
|
141
|
+
|
|
142
|
+
url = input("> ").strip()
|
|
143
|
+
|
|
144
|
+
if not url:
|
|
145
|
+
print("\n⚠️ Cannot reproduce without URL.")
|
|
146
|
+
return {'reproduced': False}
|
|
147
|
+
|
|
148
|
+
print(f"\n🔧 Navigating to {url}...\n")
|
|
149
|
+
|
|
150
|
+
# TODO: Integrate Playwright MCP if available
|
|
151
|
+
# For now, mark as reproduced with manual confirmation
|
|
152
|
+
|
|
153
|
+
confirmed = input("Is the bug visible? (y/n): ").strip().lower()
|
|
154
|
+
|
|
155
|
+
if confirmed in ['y', 'yes']:
|
|
156
|
+
return {
|
|
157
|
+
'reproduced': True,
|
|
158
|
+
'bug_type': 'browser',
|
|
159
|
+
'url': url,
|
|
160
|
+
'evidence_collected': [f"Reproduced at {url}"]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {'reproduced': False}
|
|
164
|
+
|
|
165
|
+
def _step_5_isolate(self, state: Dict) -> Dict:
|
|
166
|
+
"""Isolate the problem by analyzing code."""
|
|
167
|
+
print("Now let's isolate the problem.\n")
|
|
168
|
+
|
|
169
|
+
error = state['bug_info'].get('error_message', '')
|
|
170
|
+
stack_trace = state['bug_info'].get('stack_trace', '')
|
|
171
|
+
|
|
172
|
+
print("📊 Code path to investigate:")
|
|
173
|
+
if error:
|
|
174
|
+
print(f" Error: {error}")
|
|
175
|
+
if stack_trace:
|
|
176
|
+
print(f" Stack trace: {stack_trace}")
|
|
177
|
+
|
|
178
|
+
isolation_notes = input("\n> Describe what code you think is involved: ").strip()
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
'isolated_problem': isolation_notes,
|
|
182
|
+
'evidence_collected': [f"Isolated problem: {isolation_notes}"]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def _step_6_form_hypotheses(self, state: Dict) -> Dict:
|
|
186
|
+
"""Form 2-3 hypotheses about root cause."""
|
|
187
|
+
print("\nForm hypotheses about what's causing the bug.\n")
|
|
188
|
+
print("For each hypothesis, provide:")
|
|
189
|
+
print(" 1. Your theory")
|
|
190
|
+
print(" 2. Evidence that supports it")
|
|
191
|
+
print(" 3. How you'd test it\n")
|
|
192
|
+
|
|
193
|
+
hypotheses = []
|
|
194
|
+
for rank in range(1, 4):
|
|
195
|
+
print(f"Hypothesis {rank}:")
|
|
196
|
+
description = input(" > Theory: ").strip()
|
|
197
|
+
if not description:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
evidence = input(" > Evidence supporting this: ").strip()
|
|
201
|
+
test_plan = input(" > How to test it: ").strip()
|
|
202
|
+
|
|
203
|
+
hypotheses.append({
|
|
204
|
+
'rank': rank,
|
|
205
|
+
'description': description,
|
|
206
|
+
'evidence': evidence,
|
|
207
|
+
'test_plan': test_plan,
|
|
208
|
+
'status': 'UNKNOWN',
|
|
209
|
+
'test_results': '',
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
return {'hypotheses': hypotheses}
|
|
213
|
+
|
|
214
|
+
def _step_7_test_hypotheses(self, state: Dict) -> Dict:
|
|
215
|
+
"""Test each hypothesis systematically."""
|
|
216
|
+
hypotheses = state.get('hypotheses', [])
|
|
217
|
+
|
|
218
|
+
print(f"\nTesting {len(hypotheses)} hypothesis/hypotheses...\n")
|
|
219
|
+
|
|
220
|
+
for h in hypotheses:
|
|
221
|
+
print(f"Testing H{h['rank']}: {h['description']}")
|
|
222
|
+
print(f" Plan: {h['test_plan']}\n")
|
|
223
|
+
|
|
224
|
+
result = input(" > Test result (CONFIRMED/REJECTED/PARTIAL): ").strip().upper()
|
|
225
|
+
details = input(" > Details: ").strip()
|
|
226
|
+
|
|
227
|
+
h['status'] = result if result in ['CONFIRMED', 'REJECTED', 'PARTIAL'] else 'UNKNOWN'
|
|
228
|
+
h['test_results'] = details
|
|
229
|
+
|
|
230
|
+
if h['status'] == 'CONFIRMED':
|
|
231
|
+
print(f" ✅ H{h['rank']} CONFIRMED!\n")
|
|
232
|
+
elif h['status'] == 'REJECTED':
|
|
233
|
+
print(f" ❌ H{h['rank']} rejected.\n")
|
|
234
|
+
else:
|
|
235
|
+
print(f" ⚠️ H{h['rank']} inconclusive.\n")
|
|
236
|
+
|
|
237
|
+
return {'hypotheses': hypotheses}
|
|
238
|
+
|
|
239
|
+
def _step_8_update_findings(self, state: Dict) -> Dict:
|
|
240
|
+
"""Update findings.md with investigation results."""
|
|
241
|
+
print("\n📝 Findings will be written to tasks/findings.md")
|
|
242
|
+
print(" (This happens automatically in the next step)")
|
|
243
|
+
return {}
|
|
244
|
+
|
|
245
|
+
def _step_9_propose_fix(self, state: Dict) -> Dict:
|
|
246
|
+
"""Propose minimal fix."""
|
|
247
|
+
hypotheses = state.get('hypotheses', [])
|
|
248
|
+
confirmed = next((h for h in hypotheses if h['status'] == 'CONFIRMED'), None)
|
|
249
|
+
|
|
250
|
+
if not confirmed:
|
|
251
|
+
print("\n❌ No confirmed hypothesis—cannot propose fix.")
|
|
252
|
+
return {}
|
|
253
|
+
|
|
254
|
+
print(f"\nConfirmed root cause: {confirmed['description']}\n")
|
|
255
|
+
print("Describe the minimal fix:")
|
|
256
|
+
print(" - What file(s) to change")
|
|
257
|
+
print(" - What to change")
|
|
258
|
+
print(" - Why it fixes the problem\n")
|
|
259
|
+
|
|
260
|
+
fix_description = input("> Fix proposal: ").strip()
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
'confirmed_hypothesis': confirmed['description'],
|
|
264
|
+
'proposed_fix': fix_description,
|
|
265
|
+
'root_cause_confirmed': confirmed['description']
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
def _step_10_verify_fix(self, state: Dict) -> Dict:
|
|
269
|
+
"""Verify the fix works."""
|
|
270
|
+
print("\n✅ Apply the fix to your code, then return here.\n")
|
|
271
|
+
input("Press Enter when fix is applied: ")
|
|
272
|
+
|
|
273
|
+
print("\nVerifying fix works...\n")
|
|
274
|
+
|
|
275
|
+
command = input("Run your reproduction command again: ").strip()
|
|
276
|
+
|
|
277
|
+
if command:
|
|
278
|
+
try:
|
|
279
|
+
result = subprocess.run(
|
|
280
|
+
command,
|
|
281
|
+
shell=True,
|
|
282
|
+
capture_output=True,
|
|
283
|
+
text=True,
|
|
284
|
+
timeout=30
|
|
285
|
+
)
|
|
286
|
+
output = result.stdout + result.stderr
|
|
287
|
+
print(output)
|
|
288
|
+
|
|
289
|
+
fixed = input("\nIs the bug fixed? (y/n): ").strip().lower()
|
|
290
|
+
if fixed in ['y', 'yes']:
|
|
291
|
+
print("✅ Fix verified!")
|
|
292
|
+
return {'fix_verified': True}
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
print(f"Error: {e}")
|
|
296
|
+
|
|
297
|
+
return {'fix_verified': False}
|
|
298
|
+
|
|
299
|
+
def _step_11_document(self, state: Dict) -> Dict:
|
|
300
|
+
"""Document the findings and create lesson if needed."""
|
|
301
|
+
print("\n📚 Documentation will be written:")
|
|
302
|
+
print(" ✅ tasks/findings.md (bug details)")
|
|
303
|
+
|
|
304
|
+
# Determine if a lesson should be created
|
|
305
|
+
root_cause = state.get('root_cause_confirmed', '')
|
|
306
|
+
|
|
307
|
+
skip_patterns = [
|
|
308
|
+
'typo', 'spelling', 'copy-paste',
|
|
309
|
+
'environment variable',
|
|
310
|
+
'one-off', 'race condition'
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
skip_lesson = any(p.lower() in root_cause.lower() for p in skip_patterns)
|
|
314
|
+
|
|
315
|
+
if not skip_lesson and root_cause:
|
|
316
|
+
print(" ✅ tasks/lessons.md (prevention rule)")
|
|
317
|
+
return {
|
|
318
|
+
'lesson_created': True,
|
|
319
|
+
'root_cause_confirmed': root_cause
|
|
320
|
+
}
|
|
321
|
+
else:
|
|
322
|
+
print(" ⏭️ Skipping lesson (one-off issue)")
|
|
323
|
+
return {
|
|
324
|
+
'lesson_created': False,
|
|
325
|
+
'root_cause_confirmed': root_cause
|
|
326
|
+
}
|