ai-agent-rules 0.11.0__py3-none-any.whl → 0.15.8__py3-none-any.whl

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.

Potentially problematic release.


This version of ai-agent-rules might be problematic. Click here for more details.

ai_rules/cli.py CHANGED
@@ -22,6 +22,7 @@ from rich.table import Table
22
22
 
23
23
  from ai_rules.agents.base import Agent
24
24
  from ai_rules.agents.claude import ClaudeAgent
25
+ from ai_rules.agents.cursor import CursorAgent
25
26
  from ai_rules.agents.goose import GooseAgent
26
27
  from ai_rules.agents.shared import SharedAgent
27
28
  from ai_rules.config import (
@@ -115,6 +116,7 @@ def get_agents(config_dir: Path, config: Config) -> list[Agent]:
115
116
  """
116
117
  return [
117
118
  ClaudeAgent(config_dir, config),
119
+ CursorAgent(config_dir, config),
118
120
  GooseAgent(config_dir, config),
119
121
  SharedAgent(config_dir, config),
120
122
  ]
@@ -132,6 +134,18 @@ def complete_agents(
132
134
  return [CompletionItem(aid) for aid in agent_ids if aid.startswith(incomplete)]
133
135
 
134
136
 
137
+ def complete_profiles(
138
+ ctx: click.Context, param: click.Parameter, incomplete: str
139
+ ) -> list[CompletionItem]:
140
+ """Dynamically complete profile names for --profile option."""
141
+ from ai_rules.profiles import ProfileLoader
142
+
143
+ loader = ProfileLoader()
144
+ profiles = loader.list_profiles()
145
+
146
+ return [CompletionItem(p) for p in profiles if p.startswith(incomplete)]
147
+
148
+
135
149
  def detect_old_config_symlinks() -> list[tuple[Path, Path]]:
136
150
  """Detect symlinks pointing to old config/ location.
137
151
 
@@ -438,6 +452,20 @@ def version_callback(ctx: click.Context, param: click.Parameter, value: bool) ->
438
452
 
439
453
  console.print(f"ai-rules, version {__version__}")
440
454
 
455
+ try:
456
+ from ai_rules.bootstrap import get_tool_version, is_command_available
457
+
458
+ if is_command_available("claude-statusline"):
459
+ statusline_version = get_tool_version("claude-code-statusline")
460
+ if statusline_version:
461
+ console.print(f"statusline, version {statusline_version}")
462
+ else:
463
+ console.print(
464
+ "statusline, version [dim](installed, version unknown)[/dim]"
465
+ )
466
+ except Exception as e:
467
+ logger.debug(f"Failed to get statusline version: {e}")
468
+
441
469
  try:
442
470
  from ai_rules.bootstrap import check_tool_updates, get_tool_by_id
443
471
 
@@ -592,17 +620,26 @@ def install_user_symlinks(
592
620
 
593
621
 
594
622
  @main.command()
623
+ @click.option("--github", is_flag=True, help="Install from GitHub instead of PyPI")
595
624
  @click.option("--force", is_flag=True, help="Skip confirmation prompts")
596
625
  @click.option("--dry-run", is_flag=True, help="Show what would be done")
597
626
  @click.option("--skip-symlinks", is_flag=True, help="Skip symlink installation step")
598
627
  @click.option("--skip-completions", is_flag=True, help="Skip shell completion setup")
628
+ @click.option(
629
+ "--profile",
630
+ default=None,
631
+ shell_complete=complete_profiles,
632
+ help="Profile to use (default: 'default')",
633
+ )
599
634
  @click.pass_context
600
635
  def setup(
601
636
  ctx: click.Context,
637
+ github: bool,
602
638
  force: bool,
603
639
  dry_run: bool,
604
640
  skip_symlinks: bool,
605
641
  skip_completions: bool,
642
+ profile: str | None,
606
643
  ) -> None:
607
644
  """One-time setup: install symlinks and make ai-rules available system-wide.
608
645
 
@@ -616,43 +653,96 @@ def setup(
616
653
  get_tool_config_dir,
617
654
  install_tool,
618
655
  )
656
+ from ai_rules.bootstrap.updater import (
657
+ check_tool_updates,
658
+ get_tool_by_id,
659
+ perform_tool_upgrade,
660
+ )
619
661
 
620
- statusline_result = ensure_statusline_installed(dry_run=dry_run)
662
+ console.print("[bold cyan]Step 1/3: Install ai-rules system-wide[/bold cyan]")
663
+ console.print("This allows you to run 'ai-rules' from any directory.\n")
664
+
665
+ statusline_result, statusline_message = ensure_statusline_installed(
666
+ dry_run=dry_run, from_github=github
667
+ )
621
668
  if statusline_result == "installed":
622
- console.print("[green]✓[/green] Installed claude-statusline\n")
669
+ if dry_run and statusline_message:
670
+ console.print(f"[dim]{statusline_message}[/dim]")
671
+ else:
672
+ console.print("[green]✓[/green] Installed claude-statusline")
673
+ elif statusline_result == "upgraded":
674
+ console.print(
675
+ f"[green]✓[/green] Upgraded claude-statusline ({statusline_message})"
676
+ )
677
+ elif statusline_result == "upgrade_available":
678
+ console.print(f"[dim]{statusline_message}[/dim]")
623
679
  elif statusline_result == "failed":
624
680
  console.print(
625
- "[yellow]⚠[/yellow] Failed to install claude-statusline (continuing anyway)\n"
681
+ "[yellow]⚠[/yellow] Failed to install claude-statusline (continuing anyway)"
626
682
  )
627
683
 
628
- console.print("[bold cyan]Step 1/3: Install ai-rules system-wide[/bold cyan]")
629
- console.print("This allows you to run 'ai-rules' from any directory.\n")
684
+ ai_rules_tool = get_tool_by_id("ai-rules")
685
+ tool_install_success = False
630
686
 
631
- if not force:
632
- if not Confirm.ask("Install ai-rules permanently?", default=True):
633
- console.print(
634
- "\n[yellow]Skipped.[/yellow] You can still run via: uvx ai-rules <command>"
687
+ if ai_rules_tool and ai_rules_tool.is_installed():
688
+ try:
689
+ update_info = check_tool_updates(ai_rules_tool, timeout=10)
690
+ if update_info and update_info.has_update:
691
+ if dry_run:
692
+ console.print(
693
+ f"[dim]Would upgrade ai-rules {update_info.current_version} → {update_info.latest_version}[/dim]"
694
+ )
695
+ tool_install_success = True
696
+ else:
697
+ if not force and not Confirm.ask(
698
+ f"Upgrade ai-rules {update_info.current_version} → {update_info.latest_version}?",
699
+ default=True,
700
+ ):
701
+ console.print("[yellow]Skipped ai-rules upgrade[/yellow]")
702
+ tool_install_success = True
703
+ else:
704
+ success, msg, _ = perform_tool_upgrade(ai_rules_tool)
705
+ if success:
706
+ console.print(
707
+ f"[green]✓[/green] Upgraded ai-rules ({update_info.current_version} → {update_info.latest_version})"
708
+ )
709
+ tool_install_success = True
710
+ else:
711
+ console.print(
712
+ "[red]Error:[/red] Failed to upgrade ai-rules"
713
+ )
714
+ else:
715
+ tool_install_success = True
716
+ except Exception:
717
+ pass
718
+
719
+ if not tool_install_success:
720
+ if not force and not dry_run:
721
+ if not Confirm.ask("Install ai-rules permanently?", default=True):
722
+ console.print(
723
+ "\n[yellow]Skipped.[/yellow] You can still run via: uvx ai-rules <command>"
724
+ )
725
+ return
726
+
727
+ try:
728
+ success, message = install_tool(
729
+ "ai-agent-rules", from_github=github, force=force, dry_run=dry_run
635
730
  )
636
- return
637
731
 
638
- tool_install_success = False
639
- try:
640
- success, message = install_tool("ai-agent-rules", force=force, dry_run=dry_run)
641
-
642
- if dry_run:
643
- console.print(f"\n[dim]{message}[/dim]")
644
- tool_install_success = True
645
- elif success:
646
- console.print("[green] Tool installed successfully[/green]\n")
647
- tool_install_success = True
648
- else:
649
- console.print(f"\n[red]Error:[/red] {message}")
650
- console.print("\n[yellow]Manual installation:[/yellow]")
651
- console.print(" uv tool install ai-agent-rules")
732
+ if dry_run:
733
+ console.print(f"[dim]{message}[/dim]")
734
+ tool_install_success = True
735
+ elif success:
736
+ console.print("[green]✓[/green] Tool installed successfully")
737
+ tool_install_success = True
738
+ else:
739
+ console.print(f"\n[red]Error:[/red] {message}")
740
+ console.print("\n[yellow]Manual installation:[/yellow]")
741
+ console.print(" uv tool install ai-agent-rules")
742
+ return
743
+ except Exception as e:
744
+ console.print(f"\n[red]Error:[/red] {e}")
652
745
  return
653
- except Exception as e:
654
- console.print(f"\n[red]Error:[/red] {e}")
655
- return
656
746
 
657
747
  if not skip_symlinks:
658
748
  console.print(
@@ -685,21 +775,31 @@ def setup(
685
775
  rebuild_cache=False,
686
776
  agents=None,
687
777
  skip_completions=True,
778
+ profile=profile,
688
779
  config_dir_override=config_dir_override,
689
780
  )
690
781
 
691
782
  if not skip_completions:
692
783
  from ai_rules.completions import (
693
784
  detect_shell,
785
+ find_config_file,
694
786
  get_supported_shells,
695
787
  install_completion,
788
+ is_completion_installed,
696
789
  )
697
790
 
698
791
  console.print("\n[bold cyan]Step 3/3: Shell completion setup[/bold cyan]\n")
699
792
 
700
793
  shell = detect_shell()
701
794
  if shell:
702
- if force or Confirm.ask(f"Install {shell} tab completion?", default=True):
795
+ config_path = find_config_file(shell)
796
+ if config_path and is_completion_installed(config_path):
797
+ console.print(f"[green]✓[/green] {shell} completion already installed")
798
+ elif (
799
+ force
800
+ or dry_run
801
+ or Confirm.ask(f"Install {shell} tab completion?", default=True)
802
+ ):
703
803
  success, msg = install_completion(shell, dry_run=dry_run)
704
804
  if success:
705
805
  console.print(f"[green]✓[/green] {msg}")
@@ -711,8 +811,11 @@ def setup(
711
811
  f"[dim]Shell completion not available for your shell (only {supported} supported)[/dim]"
712
812
  )
713
813
 
714
- console.print("\n[green]✓ Setup complete![/green]")
715
- console.print("You can now run [bold]ai-rules[/bold] from anywhere.")
814
+ if dry_run:
815
+ console.print("\n[dim]Dry run complete - no changes were made.[/dim]")
816
+ else:
817
+ console.print("\n[green]✓ Setup complete![/green]")
818
+ console.print("You can now run [bold]ai-rules[/bold] from anywhere.")
716
819
 
717
820
 
718
821
  @main.command()
@@ -733,6 +836,12 @@ def setup(
733
836
  is_flag=True,
734
837
  help="Skip shell completion installation",
735
838
  )
839
+ @click.option(
840
+ "--profile",
841
+ default=None,
842
+ shell_complete=complete_profiles,
843
+ help="Profile to use (default: 'default' for backward compatibility)",
844
+ )
736
845
  @click.option(
737
846
  "--config-dir",
738
847
  "config_dir_override",
@@ -745,14 +854,18 @@ def install(
745
854
  rebuild_cache: bool,
746
855
  agents: str | None,
747
856
  skip_completions: bool,
857
+ profile: str | None,
748
858
  config_dir_override: str | None = None,
749
859
  ) -> None:
750
860
  """Install AI agent configs via symlinks."""
751
861
  from ai_rules.bootstrap import ensure_statusline_installed
752
862
 
753
- statusline_result = ensure_statusline_installed(dry_run=dry_run)
863
+ statusline_result, statusline_message = ensure_statusline_installed(dry_run=dry_run)
754
864
  if statusline_result == "installed":
755
- console.print("[green]✓[/green] Installed claude-statusline\n")
865
+ if dry_run and statusline_message:
866
+ console.print(f"[dim]{statusline_message}[/dim]\n")
867
+ else:
868
+ console.print("[green]✓[/green] Installed claude-statusline\n")
756
869
  elif statusline_result == "failed":
757
870
  console.print(
758
871
  "[yellow]⚠[/yellow] Failed to install claude-statusline (continuing anyway)\n"
@@ -766,7 +879,20 @@ def install(
766
879
  else:
767
880
  config_dir = get_config_dir()
768
881
 
769
- config = Config.load()
882
+ from ai_rules.profiles import ProfileNotFoundError
883
+ from ai_rules.state import set_active_profile
884
+
885
+ try:
886
+ config = Config.load(profile=profile)
887
+ except ProfileNotFoundError as e:
888
+ console.print(f"[red]Error:[/red] {e}")
889
+ sys.exit(1)
890
+
891
+ if not dry_run:
892
+ set_active_profile(profile or "default")
893
+
894
+ if profile and profile != "default":
895
+ console.print(f"[dim]Using profile: {profile}[/dim]\n")
770
896
 
771
897
  if rebuild_cache and not dry_run:
772
898
  import shutil
@@ -783,6 +909,12 @@ def install(
783
909
  "claude", claude_settings, force_rebuild=rebuild_cache
784
910
  )
785
911
 
912
+ cursor_settings = config_dir / "cursor" / "settings.json"
913
+ if cursor_settings.exists():
914
+ config.build_merged_settings(
915
+ "cursor", cursor_settings, force_rebuild=rebuild_cache
916
+ )
917
+
786
918
  goose_settings = config_dir / "goose" / "config.yaml"
787
919
  if goose_settings.exists():
788
920
  config.build_merged_settings(
@@ -985,6 +1117,8 @@ def _display_symlink_status(
985
1117
  )
986
1118
  def status(agents: str | None) -> None:
987
1119
  """Check status of AI agent symlinks."""
1120
+ from ai_rules.state import get_active_profile
1121
+
988
1122
  config_dir = get_config_dir()
989
1123
  config = Config.load()
990
1124
  all_agents = get_agents(config_dir, config)
@@ -992,6 +1126,10 @@ def status(agents: str | None) -> None:
992
1126
 
993
1127
  console.print("[bold]AI Rules Status[/bold]\n")
994
1128
 
1129
+ active_profile = get_active_profile()
1130
+ if active_profile:
1131
+ console.print(f"[dim]Profile: {active_profile}[/dim]\n")
1132
+
995
1133
  all_correct = True
996
1134
 
997
1135
  console.print("[bold cyan]User-Level Configuration[/bold cyan]\n")
@@ -1299,7 +1437,7 @@ def upgrade(check: bool, force: bool, skip_install: bool, only: str | None) -> N
1299
1437
  from ai_rules.bootstrap import (
1300
1438
  UPDATABLE_TOOLS,
1301
1439
  check_tool_updates,
1302
- perform_pypi_update,
1440
+ perform_tool_upgrade,
1303
1441
  )
1304
1442
 
1305
1443
  tools = [t for t in UPDATABLE_TOOLS if only is None or t.tool_id == only]
@@ -1348,13 +1486,12 @@ def upgrade(check: bool, force: bool, skip_install: bool, only: str | None) -> N
1348
1486
  console.print("[green]✓[/green] All tools are up to date!")
1349
1487
  return
1350
1488
 
1351
- if not check:
1352
- for tool, update_info in tool_updates:
1353
- if update_info.has_update:
1354
- console.print(
1355
- f"[cyan]Update available for {tool.display_name}:[/cyan] "
1356
- f"{update_info.current_version} → {update_info.latest_version}"
1357
- )
1489
+ for tool, update_info in tool_updates:
1490
+ if update_info.has_update:
1491
+ console.print(
1492
+ f"[cyan]Update available for {tool.display_name}:[/cyan] "
1493
+ f"{update_info.current_version} {update_info.latest_version}"
1494
+ )
1358
1495
 
1359
1496
  if check:
1360
1497
  if tool_updates:
@@ -1374,7 +1511,7 @@ def upgrade(check: bool, force: bool, skip_install: bool, only: str | None) -> N
1374
1511
  for tool, update_info in tool_updates:
1375
1512
  with console.status(f"Upgrading {tool.display_name}..."):
1376
1513
  try:
1377
- success, msg, was_upgraded = perform_pypi_update(tool.package_name)
1514
+ success, msg, was_upgraded = perform_tool_upgrade(tool)
1378
1515
  except Exception as e:
1379
1516
  console.print(
1380
1517
  f"\n[red]Error:[/red] {tool.display_name} upgrade failed: {e}"
@@ -1453,6 +1590,59 @@ def upgrade(check: bool, force: bool, skip_install: bool, only: str | None) -> N
1453
1590
  console.print("[dim]Restart your terminal if the command doesn't work[/dim]")
1454
1591
 
1455
1592
 
1593
+ @main.command()
1594
+ def info() -> None:
1595
+ """Show installation method and version info for ai-rules tools.
1596
+
1597
+ Displays how each tool was installed (PyPI, GitHub, or local development)
1598
+ along with current versions and update availability.
1599
+ """
1600
+ from rich.table import Table
1601
+
1602
+ from ai_rules.bootstrap import (
1603
+ UPDATABLE_TOOLS,
1604
+ check_tool_updates,
1605
+ get_tool_source,
1606
+ )
1607
+
1608
+ table = Table(title="AI Rules Installation Info", show_header=True)
1609
+ table.add_column("Tool", style="cyan")
1610
+ table.add_column("Source", style="bold")
1611
+ table.add_column("Version")
1612
+ table.add_column("Update")
1613
+
1614
+ has_updates = False
1615
+
1616
+ for tool in UPDATABLE_TOOLS:
1617
+ tool_name = tool.display_name
1618
+
1619
+ if not tool.is_installed():
1620
+ table.add_row(tool_name, "-", "-", "[dim](not installed)[/dim]")
1621
+ continue
1622
+
1623
+ source = get_tool_source(tool.package_name)
1624
+ source_display = source.name.lower() if source else "[dim]unknown[/dim]"
1625
+
1626
+ version = tool.get_version()
1627
+ version_display = version if version else "[dim]unknown[/dim]"
1628
+
1629
+ update_display = "-"
1630
+ try:
1631
+ update_info = check_tool_updates(tool, timeout=5)
1632
+ if update_info and update_info.has_update:
1633
+ update_display = f"[cyan]{update_info.latest_version} available[/cyan]"
1634
+ has_updates = True
1635
+ except Exception:
1636
+ update_display = "[dim](check failed)[/dim]"
1637
+
1638
+ table.add_row(tool_name, source_display, version_display, update_display)
1639
+
1640
+ console.print(table)
1641
+
1642
+ if has_updates:
1643
+ console.print("\n[dim]Run 'ai-rules upgrade' to install updates.[/dim]")
1644
+
1645
+
1456
1646
  @main.command()
1457
1647
  @click.option(
1458
1648
  "--agents",
@@ -2181,6 +2371,134 @@ def config_init() -> None:
2181
2371
  console.print("[dim]Configuration not saved[/dim]")
2182
2372
 
2183
2373
 
2374
+ @main.group()
2375
+ def profile() -> None:
2376
+ """Manage configuration profiles."""
2377
+ pass
2378
+
2379
+
2380
+ @profile.command("list")
2381
+ def profile_list() -> None:
2382
+ """List available profiles."""
2383
+ from rich.table import Table
2384
+
2385
+ from ai_rules.profiles import ProfileLoader
2386
+
2387
+ loader = ProfileLoader()
2388
+ profiles = loader.list_profiles()
2389
+
2390
+ table = Table(title="Available Profiles", show_header=True)
2391
+ table.add_column("Name", style="cyan")
2392
+ table.add_column("Description")
2393
+ table.add_column("Extends")
2394
+
2395
+ for name in profiles:
2396
+ try:
2397
+ info = loader.get_profile_info(name)
2398
+ desc = info.get("description", "")
2399
+ extends = info.get("extends") or "-"
2400
+ table.add_row(name, desc, extends)
2401
+ except Exception:
2402
+ table.add_row(name, "[dim]Error loading[/dim]", "-")
2403
+
2404
+ console.print(table)
2405
+
2406
+
2407
+ @profile.command("show")
2408
+ @click.argument("name")
2409
+ @click.option(
2410
+ "--resolved", is_flag=True, help="Show resolved profile with inheritance applied"
2411
+ )
2412
+ def profile_show(name: str, resolved: bool) -> None:
2413
+ """Show profile details."""
2414
+ from ai_rules.profiles import (
2415
+ CircularInheritanceError,
2416
+ ProfileLoader,
2417
+ ProfileNotFoundError,
2418
+ )
2419
+
2420
+ loader = ProfileLoader()
2421
+
2422
+ try:
2423
+ if resolved:
2424
+ profile = loader.load_profile(name)
2425
+ console.print(f"[bold]Profile: {profile.name}[/bold] (resolved)")
2426
+ console.print(f"[dim]Description:[/dim] {profile.description}")
2427
+ if profile.extends:
2428
+ console.print(f"[dim]Extends:[/dim] {profile.extends}")
2429
+
2430
+ if profile.settings_overrides:
2431
+ console.print("\n[bold]Settings Overrides:[/bold]")
2432
+ for agent, overrides in sorted(profile.settings_overrides.items()):
2433
+ console.print(f" [cyan]{agent}:[/cyan]")
2434
+ for key, value in sorted(overrides.items()):
2435
+ console.print(f" {key}: {value}")
2436
+
2437
+ if profile.exclude_symlinks:
2438
+ console.print("\n[bold]Exclude Symlinks:[/bold]")
2439
+ for pattern in sorted(profile.exclude_symlinks):
2440
+ console.print(f" - {pattern}")
2441
+
2442
+ if profile.mcp_overrides:
2443
+ console.print("\n[bold]MCP Overrides:[/bold]")
2444
+ for mcp, overrides in sorted(profile.mcp_overrides.items()):
2445
+ console.print(f" [cyan]{mcp}:[/cyan]")
2446
+ for key, value in sorted(overrides.items()):
2447
+ console.print(f" {key}: {value}")
2448
+ else:
2449
+ import yaml
2450
+
2451
+ info = loader.get_profile_info(name)
2452
+ console.print(f"[bold]Profile: {info.get('name', name)}[/bold]")
2453
+ console.print(yaml.dump(info, default_flow_style=False, sort_keys=False))
2454
+
2455
+ except ProfileNotFoundError as e:
2456
+ console.print(f"[red]Error:[/red] {e}")
2457
+ sys.exit(1)
2458
+ except CircularInheritanceError as e:
2459
+ console.print(f"[red]Error:[/red] {e}")
2460
+ sys.exit(1)
2461
+
2462
+
2463
+ @profile.command("current")
2464
+ def profile_current() -> None:
2465
+ """Show currently active profile."""
2466
+ from ai_rules.state import get_active_profile
2467
+
2468
+ active = get_active_profile()
2469
+ if active:
2470
+ console.print(f"Active profile: [cyan]{active}[/cyan]")
2471
+ else:
2472
+ console.print("[dim]No profile set (using default)[/dim]")
2473
+
2474
+
2475
+ @profile.command("switch")
2476
+ @click.argument("name", shell_complete=complete_profiles)
2477
+ @click.pass_context
2478
+ def profile_switch(ctx: click.Context, name: str) -> None:
2479
+ """Switch to a different profile."""
2480
+ from ai_rules.profiles import ProfileLoader, ProfileNotFoundError
2481
+
2482
+ loader = ProfileLoader()
2483
+ try:
2484
+ loader.load_profile(name)
2485
+ except ProfileNotFoundError as e:
2486
+ console.print(f"[red]Error:[/red] {e}")
2487
+ sys.exit(1)
2488
+
2489
+ console.print(f"Switching to profile: [cyan]{name}[/cyan]")
2490
+ ctx.invoke(
2491
+ install,
2492
+ profile=name,
2493
+ rebuild_cache=True,
2494
+ force=True,
2495
+ skip_completions=True,
2496
+ agents=None,
2497
+ dry_run=False,
2498
+ config_dir_override=None,
2499
+ )
2500
+
2501
+
2184
2502
  @main.group()
2185
2503
  def completions() -> None:
2186
2504
  """Manage shell tab completion."""
ai_rules/config/AGENTS.md CHANGED
@@ -187,15 +187,16 @@ def test_register_user_calls_hash_password():
187
187
  ## Priority 3: Style & Formatting Guidelines
188
188
 
189
189
  ### Code Comment Standards
190
- **Rule:** ONLY add comments explaining WHY, NOT WHAT.
190
+ **Rule:** ONLY add comments explaining WHY, NOT WHAT. Never add code comments that simply restate what the code does.
191
191
 
192
- **Prohibited (WHAT):**
192
+ **Prohibited - Comments that restate code behavior (WHAT):**
193
193
  ```python
194
194
  counter += 1 # Increment counter by 1
195
195
  for user in users: # Loop through users
196
+ result = calculate_total(items) # Calculate the total from items
196
197
  ```
197
198
 
198
- **Required (WHY):**
199
+ **Required - Comments that explain reasoning or context (WHY):**
199
200
  ```python
200
201
  # Use exponential backoff to avoid Stripe API rate limiting
201
202
  delay = 2 ** retry_count
@@ -207,7 +208,7 @@ results.sort(key=lambda x: x.timestamp, reverse=True)
207
208
  sanitized = validate_and_sanitize(user_input)
208
209
  ```
209
210
 
210
- **Why:** Code should be self-explanatory via naming. Comments explaining WHAT become outdated. Comments explaining WHY preserve critical context not visible in code.
211
+ **Why:** Code should be self-explanatory via naming. Comments that restate what code does become outdated and add noise. Comments explaining WHY preserve critical context (business rules, security requirements, performance considerations) not visible in the code itself.
211
212
 
212
213
  ### Whitespace Standards
213
214
  **Rule:** Remove ALL trailing whitespace | Ensure blank lines have NO whitespace | Preserve existing newlines | All files end with single newline
@@ -0,0 +1 @@
1
+ @~/AGENTS.md