Skip to content

feat: Claude Pilot v6.0 - Multi-pass verification and installer improvements#42

Merged
maxritter merged 13 commits intomainfrom
dev
Feb 2, 2026
Merged

feat: Claude Pilot v6.0 - Multi-pass verification and installer improvements#42
maxritter merged 13 commits intomainfrom
dev

Conversation

@maxritter
Copy link
Copy Markdown
Owner

@maxritter maxritter commented Feb 2, 2026

Summary

Major release introducing multi-pass verification at both planning and implementation phases, plus installer improvements for auto-version fetching.

New Features

  • Multi-pass plan verification: 3 parallel plan-verifier agents verify the plan matches user requirements before approval
  • Multi-pass code verification: 3 parallel spec-verifier agents verify implementation matches the plan
  • Installer auto-version: Auto-fetches latest version from GitHub API when no version specified
  • Repo fallback: Tries claude-pilot first, falls back to claude-codepro during transition
  • VERSION env var: Support for VERSION=X.Y.Z to install specific versions
  • Changelog generation: git-cliff integration for automatic CHANGELOG.md updates
  • Release workflow improvements: Version display and changelog override in approval step

Files Changed

  • pilot/agents/plan-verifier.md - New agent for plan verification
  • pilot/agents/spec-verifier.md - Updated code verification agent
  • pilot/commands/spec.md - Added Step 7 for plan verification
  • pilot/rules/workflow-enforcement.md - Updated workflow documentation
  • install.sh - Auto-version fetching and repo fallback
  • .github/workflows/release.yml - Enhanced approval step
  • CHANGELOG.md + cliff.toml - Changelog generation

Breaking Change

This is a BREAKING CHANGE release for Claude Pilot v6.0 with significant workflow improvements.

Test Plan

  • All 543+ tests pass
  • Type checking passes (basedpyright)
  • Linting passes (ruff)
  • install.sh syntax validated (bash -n, shellcheck)

Summary by CodeRabbit

  • New Features

    • Full rebrand to "Claude Pilot": new installer, migration from prior installs, Team Vault sharing, unified shell alias, repo fallback for installs.
  • Documentation

    • README, website, install docs, license and manifests updated; new CHANGELOG added.
  • Chores

    • Release/CI and packaging updated for Pilot; installer now stores config at user-level (~/.pilot).
  • Style

    • Refreshed site colors, gradients, and UI accents.
  • Tests

    • Unit tests updated to reflect Pilot flows and migrations.

- Simplify claude-mem cleanup: don't kill worker on exit, clean up on
  next launch instead (more reliable with daemon processes)
- Add port-based killing as fallback in kill_processes()
- Add ~/.vexor/models to local model check paths
- Add claude-mem settings check to reset non-haiku model configs
- Refactor claude_files.py: extract 14 helper methods from 350-line
  run() method for better maintainability
- Add sx (sleuth.io skills exchange) installation to installer
- Update sync.md: add Phase 0 for team vault setup, Phase 11 for vault sync
- Update spec.md: standard rules now in ~/.claude/rules/, custom in .claude/rules/
- Update path references throughout sync.md and spec.md
- Add spec-verifier agent for parallel code review passes
- Update workflow rules to allow sub-agents for verification
- Update /spec Step 8 with multi-pass verification flow
- Add Task(spec-verifier:*) permission to settings
- Update tool_redirect hook to allow spec-verifier agent
- Add plan-verifier agent for 3-pass plan verification before approval
- Add spec-verifier improvements for code verification
- Installer now auto-fetches latest version from GitHub API
- Add repo fallback (claude-pilot -> claude-codepro)
- Support VERSION env var for specific version installs
- Add CHANGELOG.md with git-cliff for changelog generation
- Update release workflow with version display and changelog override
- Update workflow-enforcement rules for dual verification points

BREAKING CHANGE: Major workflow changes for Claude Pilot v6.0
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
claude-codepro Ready Ready Preview, Comment Feb 2, 2026 10:36am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 2, 2026

Walkthrough

Rebrands project from Claude CodePro to Claude Pilot, migrates installer state to ~/.pilot with primary/fallback repo resolution, removes BootstrapStep and adds MigrationStep, refactors many installer steps/tests, updates site/CI/release workflows (git-cliff changelog), and adjusts packaging/devcontainer/install scripts. (50 words)

Changes

Cohort / File(s) Summary
Branding & top-level docs
README.md, LICENSE, CHANGELOG.md, pyproject.toml
Global rename ccp/CodePro → pilot/Pilot; metadata, badges, URLs, packaging adjusted; new CHANGELOG.md added.
Site / UI
docs/site/index.html, docs/site/public/manifest.json, docs/site/src/..., docs/site/src/index.css, docs/site/tailwind.config.ts
SEO/JSON‑LD, component copy, icons, install links and color tokens updated to Pilot branding; palette and styles adjusted.
Devcontainer & repo metadata
.devcontainer/devcontainer.json, .coderabbit.yaml, .gitattributes, .gitignore, .claude/ccp/plugin.json (deleted)
Devcontainer name/workspace/postCreate updated; path filters and git-crypt targets changed; plugin manifest deleted; .gitignore entries updated.
CI / Release workflows
.github/workflows/claude.yml, .github/workflows/release-dev.yml, .github/workflows/release.yml, .releaserc.json
Renamed jobs/artifacts ccp→pilot/launcher; release flow reworked (git-cliff changelog, current_version wiring, manual approval, artifact names/paths).
Changelog tooling
cliff.toml
Add git-cliff config for changelog generation and conventional-commit parsing.
Top-level installer script
install.sh
Major refactor: primary/fallback repo strategy, get_latest_release(repo), check_repo_exists, pilot binary naming/paths, RESTART_PILOT, config moved to ~/.pilot/config.json, added validation and repo fallback behavior.
Installer core API
installer/__init__.py, installer/cli.py, installer/config.py, installer/ui.py
Branding updates; config API moved project→home-based (signatures changed), CLI/UI renamed to pilot equivalents, theme constant renamed.
Installer steps — removal & migration
installer/steps/bootstrap.py (deleted), installer/steps/migration.py (added)
Removed BootstrapStep; added MigrationStep to detect/migrate legacy CodePro installs to ~/.pilot and cleanup old artifacts.
Installer steps — large refactors
installer/steps/claude_files.py, installer/steps/dependencies.py, installer/steps/finalize.py, installer/steps/shell_config.py
ClaudeFilesStep reworked for home-based installs, categorization, repo fallback and many helpers; dependencies adds sx installer and claude-mem patching; finalize reads pilot version; shell_config consolidates alias logic/markers.
Installer tests
installer/tests/unit/...
Removed bootstrap tests; added extensive migration tests; many tests updated to use ~/.pilot paths, pilot binary naming, Path.home mocking, and revised APIs.
Workflows/templates & misc
.github/ISSUE_TEMPLATE/bug_report.md, .gitattributes, .gitignore
Bug template install URL updated; other templates and git attributes adjusted for Pilot/launcher paths.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant InstallScript as "install.sh"
    participant RepoResolver as "Repo Resolver"
    participant Installer as "Python Installer"
    participant Migration as "MigrationStep"
    participant FilesStep as "ClaudeFilesStep"
    participant Config as "Config (~/.pilot/config.json)"

    User->>InstallScript: run installer
    InstallScript->>RepoResolver: get_latest_release(primary)
    alt primary missing
        RepoResolver->>InstallScript: return fallback
    end
    InstallScript->>Installer: invoke installer (with version)
    Installer->>Migration: check & migrate legacy CodePro
    Migration->>Config: merge/move old config -> ~/.pilot/config.json
    Migration->>Installer: report migration result
    Installer->>FilesStep: resolve repo URLs and download files
    FilesStep->>RepoResolver: resolve primary/fallback URLs
    RepoResolver->>FilesStep: return download URL
    FilesStep->>Config: update/save config
    Installer->>User: installation complete / success message
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

released

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main change: Claude Pilot v6.0 with multi-pass verification and installer improvements. It accurately reflects the core objectives and scope of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 92.66% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
installer/steps/shell_config.py (1)

112-147: ⚠️ Potential issue | 🟡 Minor

Prompt a reload when an alias is updated, not just added.

If an existing alias is replaced, the current shell still needs a reload to pick up the new definition.

🛠️ Suggested fix
                 modified_files.append(str(config_file))
+                needs_reload = True
                 if ui:
                     if alias_existed:
                         ui.success(f"Updated alias in {config_file.name}")
                     else:
                         ui.success(f"Added alias to {config_file.name}")
-                        needs_reload = True
As per coding guidelines: `installer/**/*.py`: Review installer code for idempotency (can be run multiple times safely), proper error handling and user feedback, cross-platform compatibility.
🤖 Fix all issues with AI agents
In @.github/workflows/release-dev.yml:
- Around line 90-96: The Darwin upload step in the release workflow omits the
pilot binary; update the Darwin "Upload artifact" action so its with:path
includes both the existing pilot-darwin-arm64.so entry and the pilot executable
(i.e., mirror the Linux upload behavior), ensuring the Darwin artifact list
contains the .so plus the pilot binary.

In @.github/workflows/release.yml:
- Around line 14-17: The approval step currently always uses
needs.prepare-release.outputs.changelog for the summary and ignores the optional
inputs.changelog input; update the approval summary logic in the approval step
to prefer the provided inputs.changelog when non-empty (fall back to
needs.prepare-release.outputs.changelog only if inputs.changelog is empty or
undefined). Locate the approval step where the summary or body is built
(references to approvals/summary and needs.prepare-release.outputs.changelog)
and change it to use a conditional selection (e.g., use inputs.changelog ?
inputs.changelog : needs.prepare-release.outputs.changelog) so manual runs
reflect the custom changelog; apply the same fix to the other occurrence noted
(the secondary summary usage).

In `@install.sh`:
- Line 9: Normalize the VERSION variable after its initialization by stripping
any leading "v" so that downstream URL constructions using VERSION (e.g., in the
code that builds release URLs) don't produce "vv..." sequences; specifically,
after the existing VERSION="${VERSION:-}" assignment, add a normalization step
that removes a leading 'v' from VERSION (e.g., using shell parameter expansion
like VERSION="${VERSION#v}" or equivalent) so all usages of VERSION (the VERSION
variable) are safe.
- Around line 446-454: The issue is that version_arg="--target-version $VERSION"
combines flag and value into one argv element causing argparse to reject it;
update the calls that use "$version_arg" (the two python -m installer install
invocations inside the is_in_container / saved_mode branch and the else branch)
to pass the flag and value as separate arguments (e.g., --target-version
"$VERSION") or build an array of arguments so the flag and VERSION are separate
elements, ensuring argparse receives "--target-version" and the VERSION as
distinct argv entries.

In `@installer/cli.py`:
- Line 104: The installer references an unreachable subscription URL ("Subscribe
at: https://license.claude-pilot.com") in console.print calls; update all
occurrences of that literal string to a verified, reachable license URL (or to a
configurable constant) so users aren't sent to a 502 page—search for the string
"Subscribe at: https://license.claude-pilot.com" in installer/cli.py and replace
it with the correct URL (or a LICENSE_URL constant used by the CLI output), and
ensure any tests or docs that reference the old URL are updated accordingly.

In `@installer/steps/claude_files.py`:
- Around line 246-276: The cleanup currently erases entire global dirs in
_cleanup_old_directories via _clear_directory_safe which can delete user
content; instead iterate only the managed files listed in the categories maps
(the FileInfo entries under categories["commands"] and categories["rules"]) and
remove those specific paths (or move them to a timestamped backup under
home_claude_dir/"backup") while preserving any other files; update the logic in
_cleanup_old_directories to compute target paths from each FileInfo, check
existence and ownership/marker (if available) before deletion, and call
_clear_directory_safe only on empty dirs or after backing up to ensure
idempotency and cross-platform safe behavior.
- Around line 339-378: The UI success message in _install_category_files
currently reports len(file_infos) which can overstate successes; update the
post-install reporting to use the actual installed list (len(installed)) and, if
failed is non-empty, show a different message or include failure details (e.g.,
count or list) so users see partial failures; modify the logic around
ui.success/ui.spinner in _install_category_files to compute installed/failed and
call ui.success or ui.error accordingly, referencing the installed and failed
variables and the function name _install_category_files to locate the change.

In `@installer/steps/migration.py`:
- Around line 132-152: The _cleanup_global_old_folders function currently always
uses shutil.rmtree on both Path.home() / ".claude" / "bin" and Path.home() /
".claude" / "config"; change it so that you still remove the "bin" folder with
shutil.rmtree (and append its path on success) but for the "config" folder first
check if it exists and is empty (e.g., using any(folder.iterdir()) or similar)
and only remove it with folder.rmdir() if empty (append its path on success);
preserve the existing try/except behavior to ignore OSError/IOError and ensure
the function returns the list of removed paths.
- Around line 155-175: The _migrate_custom_rules function currently uses
shutil.move which will overwrite existing files; change it to skip moving a rule
if dest (new_rules_dir / rule_file.name) already exists to preserve existing
rules and keep the function idempotent, leaving migrated counting unchanged
(only increment when a file is actually moved); update the try/except to catch
and log failures but avoid swallowing all errors silently, and add unit tests
for _migrate_custom_rules that cover: migrating new files, skipping when
destination exists, and running twice to verify idempotency (use the
old_custom_dir and new_rules_dir variables and shutil.move reference to locate
the code to modify).

In `@installer/steps/shell_config.py`:
- Around line 22-27: alias_exists_in_file currently misses unmarked "alias
claude" entries, causing duplicate aliases; update the function to also perform
a regex search for any shell alias definition for "claude" (e.g., match lines
like "alias claude='...'" with optional leading whitespace) in addition to
checking CLAUDE_ALIAS_MARKER and OLD_CCP_MARKER. Concretely, in
alias_exists_in_file use the file content and a multiline regex (e.g.,
(?m)^\s*alias\s+claude\b) to detect unmarked aliases and return True if that
regex matches or either marker is present.
- Around line 30-79: remove_old_alias fails to fully remove fish-style "function
... end" blocks because brace_count logic can clear inside_function prematurely;
update the function to separately track brace-delimited shells vs fish-style
functions (e.g., use two flags like inside_brace_function and
inside_fish_function or branch based on how the function was detected).
Specifically: when you detect "function ccp" or "function claude" set a
fish-specific flag and do not touch brace_count for that case, and only clear
that flag when a line with stripped == "end" is seen; keep the existing
brace_count handling for ccp()/ccp () { style functions but ensure it does not
affect the fish flag. Adjust the loop so lines inside either kind of function
are skipped and removed, and ensure OLD_CCP_MARKER / CLAUDE_ALIAS_MARKER and
alias lines are still skipped as before.
🧹 Nitpick comments (8)
installer/ui.py (1)

188-196: Consider centralizing the license URL constant.
It appears in multiple branches; a single constant would reduce drift.

README.md (1)

235-235: Consider using descriptive link text for accessibility.

The link text "here" is less accessible for screen reader users and doesn't provide context when links are listed out of context.

Suggested improvement
-After your trial, choose the tier that fits your needs [here](https://license.claude-pilot.com):
+After your trial, choose the tier that fits your needs at the [Claude Pilot license page](https://license.claude-pilot.com):
installer/tests/unit/steps/test_dependencies.py (1)

222-223: Remove unused variable assignment or the noqa directive.

The _original_info variable is assigned but never used for restoration. Either remove the assignment entirely or use it in a teardown/cleanup.

Suggested fix - remove unused assignment
-        _original_info = ui.info  # noqa: F841 - stored for potential restoration
         ui.info = lambda message: info_calls.append(message)
installer/tests/unit/steps/test_migration.py (1)

300-318: Strengthen the idempotency assertion in test_run_is_idempotent.
Right now it only verifies that two runs don’t throw; asserting unchanged state makes the test more meaningful.

✅ Suggested assertion
                 step.run(ctx)
-                step.run(ctx)
+                snapshot = ctx.config.copy()
+                step.run(ctx)
+                assert ctx.config == snapshot

As per coding guidelines: /tests/: Review test code briefly. Focus on: Test coverage for the feature being tested.

installer/cli.py (4)

43-45: Consider removing unused project_dir parameter.

The project_dir parameter is immediately silenced with _ = project_dir and never used. If this is kept for API compatibility with callers, consider documenting this in the docstring. Otherwise, removing it would simplify the function signature.


75-82: Multiple unused parameters in function signature.

Similar to _validate_license_key, parameters project_dir, local_mode, and local_repo_dir are immediately silenced. This pattern appears across multiple functions (_check_trial_used, _get_license_info). Consider a single refactor pass to either remove these parameters or document why they're retained for API stability.


454-458: Use list unpacking for cleaner concatenation.

Static analysis suggests using unpacking syntax which is more idiomatic and slightly more efficient.

♻️ Proposed fix
     pilot_path = find_pilot_binary()
     if pilot_path:
-        cmd = [str(pilot_path)] + claude_args
+        cmd = [str(pilot_path), *claude_args]
     else:
-        cmd = ["claude"] + claude_args
+        cmd = ["claude", *claude_args]

46-46: Consider extracting the pilot binary path to a constant.

The path Path.home() / ".pilot" / "bin" / "pilot" is repeated in multiple functions (_validate_license_key, _start_trial, _check_trial_used, _get_license_info, find_pilot_binary). Extracting this to a module-level constant or helper function would improve maintainability.

♻️ Suggested refactor
# At module level, after imports
def _get_pilot_binary_path() -> Path:
    """Get the path to the pilot binary."""
    return Path.home() / ".pilot" / "bin" / "pilot"

Then replace all instances of Path.home() / ".pilot" / "bin" / "pilot" with _get_pilot_binary_path().

Comment on lines 90 to +96
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ccp-linux-arm64
name: pilot-linux-arm64
path: |
ccp/dist/ccp-linux-arm64.so
ccp/dist/ccp
launcher/dist/pilot-linux-arm64.so
launcher/dist/pilot
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/release-dev.yml

Repository: maxritter/claude-codepro

Length of output: 8749


Darwin build is missing the pilot binary in artifact uploads.

Both Linux (lines 90-96) and Darwin (lines 141-145) jobs run the same build command: python -m launcher.build --release --version .... Since Linux uploads both pilot-linux-arm64.so and pilot, the Darwin build should produce the same artifacts. However, the Darwin upload step (lines 141-145) only specifies pilot-darwin-arm64.so, omitting the pilot binary. Update the Darwin artifact path to include:

path: |
  launcher/dist/pilot-darwin-arm64.so
  launcher/dist/pilot

This matches the Linux configuration and ensures both platforms contribute consistent artifacts to the release.

🤖 Prompt for AI Agents
In @.github/workflows/release-dev.yml around lines 90 - 96, The Darwin upload
step in the release workflow omits the pilot binary; update the Darwin "Upload
artifact" action so its with:path includes both the existing
pilot-darwin-arm64.so entry and the pilot executable (i.e., mirror the Linux
upload behavior), ensuring the Darwin artifact list contains the .so plus the
pilot binary.

Comment on lines +14 to +17
changelog:
description: "Custom changelog content (optional - overrides generated changelog)"
required: false
type: string
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the custom changelog input in the approval summary.

On manual runs, the approval step always renders needs.prepare-release.outputs.changelog, so the summary ignores the provided inputs.changelog. This can mislead approvers when a custom changelog is supplied.

✅ Suggested fix
       - name: Display release details for approval
         env:
-          CHANGELOG_CONTENT: ${{ needs.prepare-release.outputs.changelog }}
+          CHANGELOG_CONTENT: ${{ github.event.inputs.changelog != '' && github.event.inputs.changelog || needs.prepare-release.outputs.changelog }}

Also applies to: 383-386

🤖 Prompt for AI Agents
In @.github/workflows/release.yml around lines 14 - 17, The approval step
currently always uses needs.prepare-release.outputs.changelog for the summary
and ignores the optional inputs.changelog input; update the approval summary
logic in the approval step to prefer the provided inputs.changelog when
non-empty (fall back to needs.prepare-release.outputs.changelog only if
inputs.changelog is empty or undefined). Locate the approval step where the
summary or body is built (references to approvals/summary and
needs.prepare-release.outputs.changelog) and change it to use a conditional
selection (e.g., use inputs.changelog ? inputs.changelog :
needs.prepare-release.outputs.changelog) so manual runs reflect the custom
changelog; apply the same fix to the other occurrence noted (the secondary
summary usage).

if data.get("error") == "trial_already_used":
console.error("Trial has already been used on this machine")
console.print(" [cyan]Subscribe at: https://license.claude-code.pro[/cyan]")
console.print(" [cyan]Subscribe at: https://license.claude-pilot.com[/cyan]")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Get the content of installer/cli.py to see the context of the URL changes
wc -l installer/cli.py

Repository: maxritter/claude-codepro

Length of output: 88


🌐 Web query:

https://license.claude-pilot.com

💡 Result:

I couldn’t retrieve the content at https://license.claude-pilot.com because it’s currently returning 502 Bad Gateway. [1]

If what you need is the license for the claude-pilot Python package, third-party package indexes report its license as MIT. [2] [3]


🏁 Script executed:

# Check the specific lines mentioned in the review
sed -n '100,110p' installer/cli.py
sed -n '235,245p' installer/cli.py
sed -n '280,285p' installer/cli.py
sed -n '305,315p' installer/cli.py

Repository: maxritter/claude-codepro

Length of output: 2112


The new license URL is currently unreachable and must be fixed before release.

The URL https://license.claude-pilot.com referenced throughout the installer (lines 104, 237, 242, 282, 308, 313) is returning a 502 Bad Gateway error. Users attempting to subscribe will encounter a broken link, creating a poor experience and blocking license purchases. Verify the URL is correctly configured and reachable before deploying this code.

🤖 Prompt for AI Agents
In `@installer/cli.py` at line 104, The installer references an unreachable
subscription URL ("Subscribe at: https://license.claude-pilot.com") in
console.print calls; update all occurrences of that literal string to a
verified, reachable license URL (or to a configurable constant) so users aren't
sent to a 502 page—search for the string "Subscribe at:
https://license.claude-pilot.com" in installer/cli.py and replace it with the
correct URL (or a LICENSE_URL constant used by the CLI output), and ensure any
tests or docs that reference the old URL are updated accordingly.

Comment on lines +339 to +378
def _install_category_files(
self,
category: str,
file_infos: list[FileInfo],
ctx: InstallContext,
config: DownloadConfig,
ui: Any,
category_display_name: str,
) -> tuple[int, list[str], list[str]]:
"""Install files for a single category."""
installed: list[str] = []
failed: list[str] = []

def install_files() -> None:
for file_info in file_infos:
file_path = file_info.path
dest_file = self._get_dest_path(category, file_path, ctx)

if category == "settings":
success = self._install_settings(
file_path,
dest_file,
config,
ctx.enable_python,
ctx.enable_typescript,
ctx.enable_golang,
)
else:
success = download_file(file_info, dest_file, config)

if success:
installed.append(str(dest_file))
else:
failed.append(file_path)

if ui:
with ui.spinner(f"Installing {category_display_name}..."):
install_files()
ui.success(f"Installed {len(file_infos)} {category_display_name}")
else:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Category success message can overstate installs when downloads fail.

ui.success uses len(file_infos) even when failed isn’t empty. That can mislead users during partial failures.

🛠️ Suggested fix
         if ui:
             with ui.spinner(f"Installing {category_display_name}..."):
                 install_files()
-            ui.success(f"Installed {len(file_infos)} {category_display_name}")
+            if failed:
+                ui.warning(
+                    f"Installed {len(installed)} of {len(file_infos)} {category_display_name}"
+                )
+            else:
+                ui.success(f"Installed {len(installed)} {category_display_name}")
As per coding guidelines: `installer/**/*.py`: Review installer code for idempotency (can be run multiple times safely), proper error handling and user feedback, cross-platform compatibility.
🤖 Prompt for AI Agents
In `@installer/steps/claude_files.py` around lines 339 - 378, The UI success
message in _install_category_files currently reports len(file_infos) which can
overstate successes; update the post-install reporting to use the actual
installed list (len(installed)) and, if failed is non-empty, show a different
message or include failure details (e.g., count or list) so users see partial
failures; modify the logic around ui.success/ui.spinner in
_install_category_files to compute installed/failed and call ui.success or
ui.error accordingly, referencing the installed and failed variables and the
function name _install_category_files to locate the change.

Comment on lines 30 to 79
def remove_old_alias(config_file: Path) -> bool:
"""Remove old ccp alias/function from config file to allow clean update."""
"""Remove old ccp/claude alias from config file to allow clean update."""
if not config_file.exists():
return False

content = config_file.read_text()
if CCP_ALIAS_MARKER not in content and "ccp()" not in content and "alias ccp" not in content:
has_old = (
OLD_CCP_MARKER in content
or CLAUDE_ALIAS_MARKER in content
or "alias ccp" in content
or "ccp()" in content
or "claude()" in content
)
if not has_old:
return False

lines = content.split("\n")
new_lines = []
inside_ccp_function = False
inside_function = False
brace_count = 0

for line in lines:
stripped = line.strip()

if CCP_ALIAS_MARKER in line:
if OLD_CCP_MARKER in line or CLAUDE_ALIAS_MARKER in line:
continue

if stripped.startswith("alias ccp="):
if stripped.startswith("alias ccp=") or stripped.startswith("alias claude="):
continue

if stripped.startswith("ccp()") or stripped == "ccp () {":
inside_ccp_function = True
inside_function = True
brace_count = 0
elif stripped.startswith("claude()") or stripped == "claude () {":
inside_function = True
brace_count = 0

if inside_ccp_function:
if inside_function:
brace_count += line.count("{") - line.count("}")
if brace_count <= 0 and "{" in content[content.find("ccp()") :]:
inside_ccp_function = False
if brace_count <= 0:
inside_function = False
continue

if stripped.startswith("function ccp"):
inside_ccp_function = True
if stripped.startswith("function ccp") or stripped.startswith("function claude"):
inside_function = True
continue

if inside_ccp_function and stripped == "end":
inside_ccp_function = False
if inside_function and stripped == "end":
inside_function = False
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fish function ... end blocks aren’t fully removed.

The brace-count logic clears inside_function immediately for fish-style functions, leaving partial bodies and potentially breaking the shell config.

🛠️ Suggested fix
-    inside_function = False
+    inside_function = False
+    inside_fish_function = False
@@
-        if stripped.startswith("ccp()") or stripped == "ccp () {":
+        if stripped.startswith("ccp()") or stripped == "ccp () {":
             inside_function = True
             brace_count = 0
         elif stripped.startswith("claude()") or stripped == "claude () {":
             inside_function = True
             brace_count = 0
@@
-        if stripped.startswith("function ccp") or stripped.startswith("function claude"):
-            inside_function = True
+        if stripped.startswith("function ccp") or stripped.startswith("function claude"):
+            inside_fish_function = True
             continue
-
-        if inside_function and stripped == "end":
-            inside_function = False
+        if inside_fish_function:
+            if stripped == "end":
+                inside_fish_function = False
             continue
@@
-        if not inside_function:
+        if not inside_function and not inside_fish_function:
             new_lines.append(line)
As per coding guidelines: `installer/**/*.py`: Review installer code for idempotency (can be run multiple times safely), proper error handling and user feedback, cross-platform compatibility.
🤖 Prompt for AI Agents
In `@installer/steps/shell_config.py` around lines 30 - 79, remove_old_alias fails
to fully remove fish-style "function ... end" blocks because brace_count logic
can clear inside_function prematurely; update the function to separately track
brace-delimited shells vs fish-style functions (e.g., use two flags like
inside_brace_function and inside_fish_function or branch based on how the
function was detected). Specifically: when you detect "function ccp" or
"function claude" set a fish-specific flag and do not touch brace_count for that
case, and only clear that flag when a line with stripped == "end" is seen; keep
the existing brace_count handling for ccp()/ccp () { style functions but ensure
it does not affect the fish flag. Adjust the loop so lines inside either kind of
function are skipped and removed, and ensure OLD_CCP_MARKER /
CLAUDE_ALIAS_MARKER and alias lines are still skipped as before.

Variables containing space-separated arguments (version_arg, INSTALLER_ARGS)
were being quoted, causing them to be passed as single strings instead of
separate arguments. This broke argparse parsing.
- install.sh: Strip leading "v" from VERSION to prevent double-v in URLs
- migration: Only remove ~/.claude/config if empty to prevent data loss
- migration: Skip existing rules during migration to prevent overwrites
- shell_config: Detect alias claude without marker to prevent duplicates
- shell_config: Properly detect fish function pattern for removal
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@install.sh`:
- Around line 32-48: The get_latest_release function currently assumes tag_name
begins with "v"; update its parsing in both the curl and wget branches to
extract tag_name and strip an optional leading "v" so tags like "6.0.0" produce
a clean version. Locate the lines that pipe API output into grep/sed (inside
get_latest_release) and replace the sed expression with one that captures the
tag value and returns only the inner version (i.e., allow an optional leading
"v" and output the remainder), keeping the same fallback behavior and return
values for the function.

In `@installer/steps/shell_config.py`:
- Around line 22-32: alias_exists_in_file currently misses legacy shell function
definitions (e.g., bash "ccp()" or "claude()" and fish "function ccp" /
"function claude") so ShellConfigStep.run doesn't remove old wrappers; update
alias_exists_in_file to also check for these patterns (e.g., look for "ccp()",
"claude()", "function ccp", "function claude" or use regex to detect
"^\s*(function\s+)?(ccp|claude)\s*\("/line-start variants) OR alternatively call
remove_old_alias unconditionally from ShellConfigStep.run and rely on
remove_old_alias's return value; reference functions: alias_exists_in_file,
remove_old_alias, and ShellConfigStep.run when making the change.

In `@installer/tests/unit/steps/test_shell_config.py`:
- Around line 248-249: The current assertion 'assert "end" not in content or "#
after" in content' can pass when "end" remains; change it to two explicit
assertions so partial removals are caught: keep 'assert "function claude" not in
content', then replace the combined check with 'assert "end" not in content' and
'assert "# after" in content' so the test ensures the fish function body was
removed while the trailing "# after" marker remains.
🧹 Nitpick comments (1)
installer/steps/migration.py (1)

229-247: Surface migration failures to the user.

If _migrate_* returns None due to JSON/IO errors, the UI still ends with “Migration complete” and no warning. Consider capturing those failures and reporting a warning (or recording details in ctx.config) so users can remediate. As per coding guidelines: installer/**/*.py: Review installer code for proper error handling and user feedback.

Comment on lines +32 to +48
get_latest_release() {
local repo="$1"
local api_url="https://api.github.com/repos/${repo}/releases/latest"
local version=""

if command -v curl >/dev/null 2>&1; then
releases=$(curl -fsSL "$api_url" 2>/dev/null) || true
version=$(curl -fsSL "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') || true
elif command -v wget >/dev/null 2>&1; then
releases=$(wget -qO- "$api_url" 2>/dev/null) || true
version=$(wget -qO- "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') || true
fi

if [ -z "$releases" ]; then
return 1
if [ -n "$version" ]; then
echo "$version"
return 0
fi
return 1
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle tags without a leading “v”.
get_latest_release assumes tag_name starts with v. If a release tag is 6.0.0, sed will return the full JSON line, producing a malformed VERSION and broken URLs. Suggest parsing tag_name and stripping an optional v.

🔧 Proposed fix
-		version=$(curl -fsSL "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') || true
+		version=$(curl -fsSL "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\(v\)\?\([^"]*\)".*/\2/p') || true
 ...
-		version=$(wget -qO- "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') || true
+		version=$(wget -qO- "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\(v\)\?\([^"]*\)".*/\2/p') || true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get_latest_release() {
local repo="$1"
local api_url="https://api.github.com/repos/${repo}/releases/latest"
local version=""
if command -v curl >/dev/null 2>&1; then
releases=$(curl -fsSL "$api_url" 2>/dev/null) || true
version=$(curl -fsSL "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') || true
elif command -v wget >/dev/null 2>&1; then
releases=$(wget -qO- "$api_url" 2>/dev/null) || true
version=$(wget -qO- "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/') || true
fi
if [ -z "$releases" ]; then
return 1
if [ -n "$version" ]; then
echo "$version"
return 0
fi
return 1
}
get_latest_release() {
local repo="$1"
local api_url="https://api.github.com/repos/${repo}/releases/latest"
local version=""
if command -v curl >/dev/null 2>&1; then
version=$(curl -fsSL "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\(v\)\?\([^"]*\)".*/\2/p') || true
elif command -v wget >/dev/null 2>&1; then
version=$(wget -qO- "$api_url" 2>/dev/null | grep -m1 '"tag_name"' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\(v\)\?\([^"]*\)".*/\2/p') || true
fi
if [ -n "$version" ]; then
echo "$version"
return 0
fi
return 1
}
🤖 Prompt for AI Agents
In `@install.sh` around lines 32 - 48, The get_latest_release function currently
assumes tag_name begins with "v"; update its parsing in both the curl and wget
branches to extract tag_name and strip an optional leading "v" so tags like
"6.0.0" produce a clean version. Locate the lines that pipe API output into
grep/sed (inside get_latest_release) and replace the sed expression with one
that captures the tag value and returns only the inner version (i.e., allow an
optional leading "v" and output the remainder), keeping the same fallback
behavior and return values for the function.

Comment on lines 22 to +32
def alias_exists_in_file(config_file: Path) -> bool:
"""Check if ccp alias already exists in config file."""
"""Check if claude alias already exists in config file."""
if not config_file.exists():
return False
content = config_file.read_text()
return "alias ccp" in content or CCP_ALIAS_MARKER in content
return (
CLAUDE_ALIAS_MARKER in content
or OLD_CCP_MARKER in content
or "alias ccp" in content
or "alias claude" in content
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Detect function-based legacy aliases to avoid conflicting definitions.

ShellConfigStep.run only calls remove_old_alias when alias_exists_in_file returns true. Legacy ccp() / claude() (bash) or function ccp/claude (fish) definitions without markers are currently missed, leaving stale wrappers and potentially overriding the new alias. Please include these patterns in the detection (or call remove_old_alias unconditionally and use its return value).

🛠️ Proposed fix (expand detection)
 def alias_exists_in_file(config_file: Path) -> bool:
     """Check if claude alias already exists in config file."""
     if not config_file.exists():
         return False
     content = config_file.read_text()
     return (
         CLAUDE_ALIAS_MARKER in content
         or OLD_CCP_MARKER in content
         or "alias ccp" in content
         or "alias claude" in content
+        or "ccp()" in content
+        or "claude()" in content
+        or "function ccp" in content
+        or "function claude" in content
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def alias_exists_in_file(config_file: Path) -> bool:
"""Check if ccp alias already exists in config file."""
"""Check if claude alias already exists in config file."""
if not config_file.exists():
return False
content = config_file.read_text()
return "alias ccp" in content or CCP_ALIAS_MARKER in content
return (
CLAUDE_ALIAS_MARKER in content
or OLD_CCP_MARKER in content
or "alias ccp" in content
or "alias claude" in content
)
def alias_exists_in_file(config_file: Path) -> bool:
"""Check if claude alias already exists in config file."""
if not config_file.exists():
return False
content = config_file.read_text()
return (
CLAUDE_ALIAS_MARKER in content
or OLD_CCP_MARKER in content
or "alias ccp" in content
or "alias claude" in content
or "ccp()" in content
or "claude()" in content
or "function ccp" in content
or "function claude" in content
)
🤖 Prompt for AI Agents
In `@installer/steps/shell_config.py` around lines 22 - 32, alias_exists_in_file
currently misses legacy shell function definitions (e.g., bash "ccp()" or
"claude()" and fish "function ccp" / "function claude") so ShellConfigStep.run
doesn't remove old wrappers; update alias_exists_in_file to also check for these
patterns (e.g., look for "ccp()", "claude()", "function ccp", "function claude"
or use regex to detect "^\s*(function\s+)?(ccp|claude)\s*\("/line-start
variants) OR alternatively call remove_old_alias unconditionally from
ShellConfigStep.run and rely on remove_old_alias's return value; reference
functions: alias_exists_in_file, remove_old_alias, and ShellConfigStep.run when
making the change.

Comment on lines +248 to +249
assert "function claude" not in content
assert "end" not in content or "# after" in content
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the fish removal assertion to actually validate removal.

assert "end" not in content or "# after" in content passes even if end remains, so the test won’t catch partial removals. Split into explicit assertions.

🧪 Proposed fix
-            assert "end" not in content or "# after" in content
+            assert "end" not in content
+            assert "# after" in content
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert "function claude" not in content
assert "end" not in content or "# after" in content
assert "function claude" not in content
assert "end" not in content
assert "# after" in content
🤖 Prompt for AI Agents
In `@installer/tests/unit/steps/test_shell_config.py` around lines 248 - 249, The
current assertion 'assert "end" not in content or "# after" in content' can pass
when "end" remains; change it to two explicit assertions so partial removals are
caught: keep 'assert "function claude" not in content', then replace the
combined check with 'assert "end" not in content' and 'assert "# after" in
content' so the test ensures the fish function body was removed while the
trailing "# after" marker remains.

- Replace "Put Claude Code on Autopilot" with "Claude Code, Done Right"
- Add problem statement: "Claude is powerful but inconsistent"
- Update tagline: "Tests enforced. Context preserved. Quality automated."
- Remove fluff like "Ready for takeoff"
- Consistent messaging across README, website, CLI, and installer
- Fix installer download location from .claude/installer to ~/.pilot/installer
- Add claude-mem config patching to always set required fields
- Add gh auth login option in /sync when HTTPS auth fails
- Improve sx vault URL input flow with clearer instructions
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@installer/tests/unit/steps/test_dependencies.py`:
- Around line 222-223: The test stores an unused reference _original_info =
ui.info solely for potential restoration but never restores it and includes an
unnecessary noqa; remove the dead variable and the noqa directive by deleting
the assignment line for _original_info and keep the replacement assignment
ui.info = lambda message: info_calls.append(message) so the test no longer
defines or references _original_info.
🧹 Nitpick comments (3)
install.sh (1)

463-476: Relying on word splitting is fragile; consider passing arguments directly.

The current approach uses unquoted $version_arg to leverage shell word splitting, which works but is considered fragile and would trigger ShellCheck warnings (SC2086). If VERSION ever contains spaces or special characters, this could break.

♻️ Proposed improvement for robustness
-	local version_arg="--target-version $VERSION"
-	local local_arg=""
-	if [ "$USE_LOCAL_INSTALLER" = true ]; then
-		local_arg="--local --local-repo-dir $(pwd)"
-	fi
-
 	if ! is_in_container && [ "$saved_mode" = "local" ]; then
-		uv run --python 3.12 --no-project --with rich \
-			python -m installer install --local-system $version_arg $local_arg "$@"
+		if [ "$USE_LOCAL_INSTALLER" = true ]; then
+			uv run --python 3.12 --no-project --with rich \
+				python -m installer install --local-system --target-version "$VERSION" --local --local-repo-dir "$(pwd)" "$@"
+		else
+			uv run --python 3.12 --no-project --with rich \
+				python -m installer install --local-system --target-version "$VERSION" "$@"
+		fi
 	else
-		uv run --python 3.12 --no-project --with rich \
-			python -m installer install $version_arg $local_arg "$@"
+		if [ "$USE_LOCAL_INSTALLER" = true ]; then
+			uv run --python 3.12 --no-project --with rich \
+				python -m installer install --target-version "$VERSION" --local --local-repo-dir "$(pwd)" "$@"
+		else
+			uv run --python 3.12 --no-project --with rich \
+				python -m installer install --target-version "$VERSION" "$@"
+		fi
 	fi
installer/steps/dependencies.py (2)

429-436: Update failure is silently ignored.

When sx already exists, the function calls sx update but ignores its return value, always returning True. This differs from the pattern in other install functions where failures are propagated. Consider whether a failed update should be reflected in the return value.

Optional: Return update result or log warning on failure
 def install_sx() -> bool:
     """Install sx (sleuth.io skills exchange) for team skill sharing."""
     if command_exists("sx"):
-        _run_bash_with_retry("sx update")
-        return True
+        # Update existing installation; failure is non-fatal
+        _run_bash_with_retry("sx update")
+        return True

     return _run_bash_with_retry("curl -fsSL https://raw.githubusercontent.com/sleuth-io/sx/main/install.sh | bash")

If update failure should be surfaced, consider logging a warning when _run_bash_with_retry("sx update") returns False.


590-592: Broad exception handling silently swallows errors.

The static analysis flagged catching bare Exception (BLE001). While this pattern is consistent with other functions in this file for robustness, consider logging the exception details for debugging purposes, especially since the caller only receives a boolean status.

Optional: Log exception for debugging
         settings.update(required_settings)
         settings_path.write_text(json.dumps(settings, indent=2) + "\n")
         return True
-    except Exception:
+    except Exception as e:
+        # Log for debugging but don't fail installation
+        import logging
+        logging.debug(f"Could not patch claude-mem config: {e}")
         return False

Comment on lines +222 to +223
_original_info = ui.info # noqa: F841 - stored for potential restoration
ui.info = lambda message: info_calls.append(message)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused variable and noqa directive.

The _original_info variable is stored but never used (no restoration occurs), and the noqa directive is flagged as unnecessary by static analysis.

Proposed fix
-        _original_info = ui.info  # noqa: F841 - stored for potential restoration
+        original_info = ui.info
         ui.info = lambda message: info_calls.append(message)

Or simply remove the stored reference if restoration isn't needed:

-        _original_info = ui.info  # noqa: F841 - stored for potential restoration
         ui.info = lambda message: info_calls.append(message)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_original_info = ui.info # noqa: F841 - stored for potential restoration
ui.info = lambda message: info_calls.append(message)
original_info = ui.info
ui.info = lambda message: info_calls.append(message)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 222-222: Unused noqa directive (unused: F841)

Remove unused noqa directive

(RUF100)

🤖 Prompt for AI Agents
In `@installer/tests/unit/steps/test_dependencies.py` around lines 222 - 223, The
test stores an unused reference _original_info = ui.info solely for potential
restoration but never restores it and includes an unnecessary noqa; remove the
dead variable and the noqa directive by deleting the assignment line for
_original_info and keep the replacement assignment ui.info = lambda message:
info_calls.append(message) so the test no longer defines or references
_original_info.

- Replace "pilot alias" references with "claude command" in tips
- Update README to show claude command in all usage examples
- Update installer post-install message to say "Run: claude"
- Remove pricing offers from SEO schema in index.html
- Update website WhatsInside component
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
installer/steps/finalize.py (1)

14-31: ⚠️ Potential issue | 🟡 Minor

Cross-platform: Windows binary may need .exe extension.

The hardcoded binary name pilot at line 16 won't resolve on Windows where executables typically require the .exe extension. Consider platform-aware path construction.

🛠️ Proposed fix for Windows compatibility
 def _get_pilot_version() -> str:
     """Get version from Pilot binary, fallback to installer version."""
-    pilot_path = Path.home() / ".pilot" / "bin" / "pilot"
+    import sys
+    binary_name = "pilot.exe" if sys.platform == "win32" else "pilot"
+    pilot_path = Path.home() / ".pilot" / "bin" / binary_name
     if pilot_path.exists():

As per coding guidelines: installer/**/*.py should be reviewed for cross-platform compatibility.

🧹 Nitpick comments (2)
docs/site/index.html (1)

30-31: Consider using standard favicon format.

Using .jpg for favicons works but .ico or .png are more conventional and provide better compatibility (especially .ico for older browsers). This is a minor point if .jpg is working correctly.

README.md (1)

243-243: Consider more descriptive link text.

Static analysis flagged "here" as non-descriptive link text. For accessibility and SEO, consider making the link text more meaningful.

Suggested improvement
-After your trial, choose the tier that fits your needs [here](https://license.claude-pilot.com):
+After your trial, choose the tier that fits your needs on the [Claude Pilot licensing page](https://license.claude-pilot.com):

@maxritter maxritter merged commit 399c0ad into main Feb 2, 2026
10 of 11 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 2, 2026

🎉 This PR is included in version 6.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant