Skip to content

Commit 06f04dc

Browse files
jeremyederclaude
andauthored
feat: Implement align subcommand for automated remediation (Issue #14) (#34)
* feat: implement align subcommand for automated remediation - Add Fix data models (FileCreationFix, FileModificationFix, CommandFix, MultiStepFix) - Implement BaseFixer abstract class for fixer strategy pattern - Create concrete fixers: - CLAUDEmdFixer: Generate CLAUDE.md from template - GitignoreFixer: Add recommended patterns to .gitignore - PrecommitHooksFixer: Create .pre-commit-config.yaml and install hooks - Implement FixerService for orchestrating fixes and score projection - Add align CLI command with --dry-run, --interactive, --attributes options - Create Jinja2 templates for CLAUDE.md and gitignore patterns - Add comprehensive unit tests (10 tests, all passing) The align command: 1. Runs assessment to identify failing attributes 2. Generates fixes for fixable failures 3. Projects score improvement before applying 4. Supports dry-run mode for previewing changes 5. Supports interactive mode for confirming each fix 6. Shows certification level changes (Bronze → Silver → Gold → Platinum) Usage: agentready align . # Apply all fixes agentready align . --dry-run # Preview without applying agentready align . --interactive # Confirm each fix agentready align . --attributes claude_md_file,gitignore_completeness Implements Issue #14 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: apply black formatting to pass CI checks - Reformat backlog_to_issues.py (quotes, line breaks) - Reformat demo.py (ternary operator line break) - Reformat test_fixers.py (function signature line break) Fixes CI black --check failures * fix: add ruff to dev dependencies for CI - Add ruff>=0.1.0 to dev dependencies in pyproject.toml - Fixes 'ruff: command not found' error in CI pipeline The CI workflow runs ruff check but ruff was missing from dependencies * fix: resolve ruff linting issues across codebase - Remove unused imports (F401) from assessors and test files - Remove extraneous f-string prefixes (F541) in CLI and scripts - Remove unused exception variables (F841) Auto-fixed with: ruff check --fix . All 36 issues resolved * fix: handle detached HEAD state in repository scanner - Wrap repo.active_branch.name in try/except - Fallback to 'HEAD' when in detached HEAD state - Fixes CI failure where GitHub Actions checks out in detached HEAD Error: TypeError: HEAD is a detached symbolic reference Occurs in CI environments during git checkout * fix: reformat files after ruff import cleanup - Reformat 5 files that had imports removed by ruff - structure.py, documentation.py, testing.py, stub_assessors.py - test_assessors_structure.py Black formatting required after ruff --fix removed unused imports * fix: sort imports in test files after ruff cleanup - Fix isort errors in test_scan_workflow.py and test_security.py - These files had imports removed by ruff but weren't re-sorted * fix: fix RepomixConfigAssessor to use proper Finding constructor - Replace non-existent Finding.create_pass/create_fail methods with proper Finding() constructor - Add missing Remediation and Citation imports - Fix Repository initialization in tests with all required fields (.git, name, url, branch, commit_hash, total_files, total_lines) - Fix assertion in test_check_freshness_no_files to match actual error message ("files found" instead of "not found") - Fix assertion in test_assess_config_but_no_output with same correction All 21 repomix tests now passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5b2285b commit 06f04dc

27 files changed

Lines changed: 1340 additions & 168 deletions

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dev = [
3232
"black>=23.0.0",
3333
"isort>=5.12.0",
3434
"flake8>=6.0.0",
35+
"ruff>=0.1.0",
3536
]
3637

3738
[project.scripts]

scripts/backlog_to_issues.py

Lines changed: 92 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
5. Pauses after first item for user review
1111
"""
1212

13+
import json
1314
import re
1415
import subprocess
1516
import sys
1617
from pathlib import Path
17-
from typing import List, Dict, Optional
18-
import json
18+
from typing import Dict, List, Optional
1919

2020

2121
class BacklogItem:
@@ -34,34 +34,33 @@ def __repr__(self):
3434
def parse_backlog(backlog_path: Path) -> List[BacklogItem]:
3535
"""Parse BACKLOG.md and extract all items."""
3636

37-
with open(backlog_path, 'r') as f:
37+
with open(backlog_path, "r") as f:
3838
content = f.read()
3939

4040
items = []
4141

4242
# Split into sections by ###
43-
sections = re.split(r'\n### ', content)
43+
sections = re.split(r"\n### ", content)
4444

4545
for i, section in enumerate(sections[1:], 1): # Skip first section (header)
46-
lines = section.split('\n')
46+
lines = section.split("\n")
4747
title = lines[0].strip()
4848

4949
# Find priority in next few lines
5050
priority = "P4" # default
5151
for line in lines[1:5]:
52-
if match := re.search(r'\*\*Priority\*\*:\s*(P\d)', line):
52+
if match := re.search(r"\*\*Priority\*\*:\s*(P\d)", line):
5353
priority = match.group(1)
5454
break
5555

5656
# Get full content until next ### or end
57-
full_content = '\n'.join(lines)
57+
full_content = "\n".join(lines)
5858

59-
items.append(BacklogItem(
60-
title=title,
61-
priority=priority,
62-
content=full_content,
63-
section_start=i
64-
))
59+
items.append(
60+
BacklogItem(
61+
title=title, priority=priority, content=full_content, section_start=i
62+
)
63+
)
6564

6665
return items
6766

@@ -231,7 +230,9 @@ def generate_coldstart_prompt(item: BacklogItem, repo_context: Dict) -> str:
231230
return prompt
232231

233232

234-
def create_github_issue(item: BacklogItem, prompt: str, repo_context: Dict, dry_run: bool = False) -> Optional[str]:
233+
def create_github_issue(
234+
item: BacklogItem, prompt: str, repo_context: Dict, dry_run: bool = False
235+
) -> Optional[str]:
235236
"""Create GitHub issue via gh CLI and attach coldstart prompt as comment."""
236237

237238
# Prepare issue title
@@ -243,62 +244,64 @@ def create_github_issue(item: BacklogItem, prompt: str, repo_context: Dict, dry_
243244
body_parts.append(f"**Priority**: {item.priority}\n")
244245

245246
# Extract description (first paragraph after Priority)
246-
lines = item.content.split('\n')
247+
lines = item.content.split("\n")
247248
in_description = False
248249
description_lines = []
249250

250251
for line in lines:
251-
if '**Description**:' in line:
252+
if "**Description**:" in line:
252253
in_description = True
253254
continue
254255
if in_description:
255-
if line.startswith('**') and ':' in line:
256+
if line.startswith("**") and ":" in line:
256257
break
257258
description_lines.append(line)
258259

259260
if description_lines:
260261
body_parts.append("## Description\n")
261-
body_parts.append('\n'.join(description_lines))
262+
body_parts.append("\n".join(description_lines))
262263

263264
# Add link to full context
264265
body_parts.append("\n\n## Full Context\n")
265-
body_parts.append(f"See [BACKLOG.md](https://github.com/{repo_context['owner']}/{repo_context['repo']}/blob/main/BACKLOG.md) for complete requirements.\n")
266+
body_parts.append(
267+
f"See [BACKLOG.md](https://github.com/{repo_context['owner']}/{repo_context['repo']}/blob/main/BACKLOG.md) for complete requirements.\n"
268+
)
266269

267270
# Add acceptance criteria if present
268-
if '**Acceptance Criteria**:' in item.content:
269-
criteria_start = item.content.find('**Acceptance Criteria**:')
270-
criteria_section = item.content[criteria_start:criteria_start+1000]
271+
if "**Acceptance Criteria**:" in item.content:
272+
criteria_start = item.content.find("**Acceptance Criteria**:")
273+
criteria_section = item.content[criteria_start : criteria_start + 1000]
271274
body_parts.append("\n## Acceptance Criteria\n")
272-
body_parts.append(criteria_section.split('\n\n')[0])
275+
body_parts.append(criteria_section.split("\n\n")[0])
273276

274-
issue_body = '\n'.join(body_parts)
277+
issue_body = "\n".join(body_parts)
275278

276279
# Determine labels
277280
labels = [f"priority:{item.priority.lower()}"]
278281

279282
# Add category labels based on title/content
280-
if 'security' in item.title.lower() or 'xss' in item.content.lower():
281-
labels.append('security')
282-
if 'bug' in item.title.lower() or 'fix' in item.title.lower():
283-
labels.append('bug')
283+
if "security" in item.title.lower() or "xss" in item.content.lower():
284+
labels.append("security")
285+
if "bug" in item.title.lower() or "fix" in item.title.lower():
286+
labels.append("bug")
284287
else:
285-
labels.append('enhancement')
286-
if 'test' in item.title.lower():
287-
labels.append('testing')
288-
if 'github' in item.title.lower():
289-
labels.append('github-integration')
290-
if 'report' in item.title.lower():
291-
labels.append('reporting')
288+
labels.append("enhancement")
289+
if "test" in item.title.lower():
290+
labels.append("testing")
291+
if "github" in item.title.lower():
292+
labels.append("github-integration")
293+
if "report" in item.title.lower():
294+
labels.append("reporting")
292295

293-
labels_str = ','.join(labels)
296+
labels_str = ",".join(labels)
294297

295298
if dry_run:
296299
print(f"\n{'='*80}")
297-
print(f"DRY RUN: Would create issue:")
300+
print("DRY RUN: Would create issue:")
298301
print(f"Title: {issue_title}")
299302
print(f"Labels: {labels_str}")
300303
print(f"Body preview:\n{issue_body[:500]}...")
301-
print(f"\nColdstart prompt would be added as first comment")
304+
print("\nColdstart prompt would be added as first comment")
302305
print(f"{'='*80}\n")
303306
return None
304307

@@ -307,34 +310,43 @@ def create_github_issue(item: BacklogItem, prompt: str, repo_context: Dict, dry_
307310
# Create the issue
308311
result = subprocess.run(
309312
[
310-
'gh', 'issue', 'create',
311-
'--title', issue_title,
312-
'--body', issue_body,
313-
'--label', labels_str
313+
"gh",
314+
"issue",
315+
"create",
316+
"--title",
317+
issue_title,
318+
"--body",
319+
issue_body,
320+
"--label",
321+
labels_str,
314322
],
315323
capture_output=True,
316324
text=True,
317-
check=True
325+
check=True,
318326
)
319327

320328
issue_url = result.stdout.strip()
321329
print(f"✅ Created issue: {issue_url}")
322330

323331
# Extract issue number from URL
324-
issue_number = issue_url.split('/')[-1]
332+
issue_number = issue_url.split("/")[-1]
325333

326334
# Add coldstart prompt as first comment
327335
subprocess.run(
328336
[
329-
'gh', 'issue', 'comment', issue_number,
330-
'--body', f"## 🤖 Coldstart Implementation Prompt\n\n{prompt}"
337+
"gh",
338+
"issue",
339+
"comment",
340+
issue_number,
341+
"--body",
342+
f"## 🤖 Coldstart Implementation Prompt\n\n{prompt}",
331343
],
332344
capture_output=True,
333345
text=True,
334-
check=True
346+
check=True,
335347
)
336348

337-
print(f"✅ Added coldstart prompt as comment")
349+
print("✅ Added coldstart prompt as comment")
338350

339351
return issue_url
340352

@@ -347,40 +359,39 @@ def get_repo_context() -> Dict:
347359
"""Get repository context (owner, repo name) from git remote."""
348360
try:
349361
result = subprocess.run(
350-
['gh', 'repo', 'view', '--json', 'owner,name'],
362+
["gh", "repo", "view", "--json", "owner,name"],
351363
capture_output=True,
352364
text=True,
353-
check=True
365+
check=True,
354366
)
355367
data = json.loads(result.stdout)
356-
return {
357-
'owner': data['owner']['login'],
358-
'repo': data['name']
359-
}
360-
except Exception as e:
368+
return {"owner": data["owner"]["login"], "repo": data["name"]}
369+
except Exception:
361370
# No git remote - ask user or use default
362-
print(f"⚠️ Warning: Could not get repo context from git remote")
363-
print(f" This is expected if repository not yet on GitHub")
364-
print(f" Using default values for now\n")
371+
print("⚠️ Warning: Could not get repo context from git remote")
372+
print(" This is expected if repository not yet on GitHub")
373+
print(" Using default values for now\n")
365374
# For agentready, we know the intended location
366-
return {'owner': 'redhat', 'repo': 'agentready'}
375+
return {"owner": "redhat", "repo": "agentready"}
367376

368377

369-
def save_prompt_to_file(item: BacklogItem, prompt: str, output_dir: Path, item_number: int) -> Path:
378+
def save_prompt_to_file(
379+
item: BacklogItem, prompt: str, output_dir: Path, item_number: int
380+
) -> Path:
370381
"""Save coldstart prompt to markdown file."""
371382

372383
# Create output directory if it doesn't exist
373384
output_dir.mkdir(parents=True, exist_ok=True)
374385

375386
# Generate filename from item number and title
376-
safe_title = re.sub(r'[^\w\s-]', '', item.title.lower())
377-
safe_title = re.sub(r'[-\s]+', '-', safe_title)[:50]
387+
safe_title = re.sub(r"[^\w\s-]", "", item.title.lower())
388+
safe_title = re.sub(r"[-\s]+", "-", safe_title)[:50]
378389
filename = f"{item_number:02d}-{safe_title}.md"
379390

380391
filepath = output_dir / filename
381392

382393
# Write prompt to file
383-
with open(filepath, 'w') as f:
394+
with open(filepath, "w") as f:
384395
f.write(prompt)
385396

386397
return filepath
@@ -390,19 +401,19 @@ def main():
390401
"""Main script execution."""
391402

392403
# Parse command line args
393-
create_issues = '--create-issues' in sys.argv
394-
process_all = '--all' in sys.argv
404+
create_issues = "--create-issues" in sys.argv
405+
process_all = "--all" in sys.argv
395406

396407
# Get repository root
397408
repo_root = Path(__file__).parent.parent
398-
backlog_path = repo_root / 'BACKLOG.md'
409+
backlog_path = repo_root / "BACKLOG.md"
399410

400411
if not backlog_path.exists():
401412
print(f"❌ BACKLOG.md not found at {backlog_path}")
402413
sys.exit(1)
403414

404415
# Create output directory
405-
output_dir = repo_root / '.github' / 'coldstart-prompts'
416+
output_dir = repo_root / ".github" / "coldstart-prompts"
406417

407418
# Get repo context
408419
repo_context = get_repo_context()
@@ -444,20 +455,22 @@ def main():
444455
if issue_url:
445456
print(f"✅ Created issue: {issue_url}\n")
446457
else:
447-
print(f"❌ Failed to create issue\n")
458+
print("❌ Failed to create issue\n")
448459

449460
# Pause after first item unless --all specified
450461
if not process_all and idx == 1:
451462
print(f"\n{'='*80}")
452-
print(f"✅ FIRST PROMPT GENERATED")
463+
print("✅ FIRST PROMPT GENERATED")
453464
print(f"{'='*80}\n")
454465
print(f"Saved to: {filepath}")
455-
print(f"\nPlease review the prompt file.")
456-
print(f"Once approved, run with --all to process remaining {len(items) - 1} items:")
457-
print(f" python scripts/backlog_to_issues.py --all")
466+
print("\nPlease review the prompt file.")
467+
print(
468+
f"Once approved, run with --all to process remaining {len(items) - 1} items:"
469+
)
470+
print(" python scripts/backlog_to_issues.py --all")
458471
if not create_issues:
459-
print(f"\nTo also create GitHub issues, add --create-issues flag:")
460-
print(f" python scripts/backlog_to_issues.py --all --create-issues")
472+
print("\nTo also create GitHub issues, add --create-issues flag:")
473+
print(" python scripts/backlog_to_issues.py --all --create-issues")
461474
return
462475

463476
# All items processed
@@ -466,12 +479,12 @@ def main():
466479
print(f"{'='*80}\n")
467480
print(f"Coldstart prompts saved to: {output_dir}/")
468481
if create_issues:
469-
print(f"GitHub issues created (check repository)")
470-
print(f"\nNext steps:")
482+
print("GitHub issues created (check repository)")
483+
print("\nNext steps:")
471484
print(f" 1. Review generated prompts in {output_dir}/")
472-
print(f" 2. Create GitHub issues manually, or run with --create-issues")
473-
print(f" 3. Start implementing features using the coldstart prompts!")
485+
print(" 2. Create GitHub issues manually, or run with --create-issues")
486+
print(" 3. Start implementing features using the coldstart prompts!")
474487

475488

476-
if __name__ == '__main__':
489+
if __name__ == "__main__":
477490
main()

src/agentready/assessors/code_quality.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Code quality assessors for complexity, file length, type annotations, and code smells."""
22

33
import subprocess
4-
from pathlib import Path
54

65
from ..models.attribute import Attribute
76
from ..models.finding import Citation, Finding, Remediation
@@ -326,7 +325,7 @@ def _assess_python_complexity(self, repository: Repository) -> Finding:
326325
self.attribute, reason="No Python code to analyze"
327326
)
328327

329-
except MissingToolError as e:
328+
except MissingToolError:
330329
raise # Re-raise to be caught by Scanner
331330
except Exception as e:
332331
return Finding.error(
@@ -352,7 +351,7 @@ def _assess_with_lizard(self, repository: Repository) -> Finding:
352351
self.attribute, reason="Lizard analysis not fully implemented"
353352
)
354353

355-
except MissingToolError as e:
354+
except MissingToolError:
356355
raise
357356
except Exception as e:
358357
return Finding.error(

src/agentready/assessors/documentation.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Documentation assessor for CLAUDE.md, README, docstrings, and ADRs."""
22

3-
from pathlib import Path
4-
53
from ..models.attribute import Attribute
64
from ..models.finding import Citation, Finding, Remediation
75
from ..models.repository import Repository

0 commit comments

Comments
 (0)