Conversation
- 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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughRebrands 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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 | 🟡 MinorPrompt 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.
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.🛠️ 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
🤖 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_infovariable 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 intest_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 == snapshotAs 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 unusedproject_dirparameter.The
project_dirparameter is immediately silenced with_ = project_dirand 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, parametersproject_dir,local_mode, andlocal_repo_dirare 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().
| - 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 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .github/workflows/release-dev.ymlRepository: 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.
| changelog: | ||
| description: "Custom changelog content (optional - overrides generated changelog)" | ||
| required: false | ||
| type: string |
There was a problem hiding this comment.
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]") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Get the content of installer/cli.py to see the context of the URL changes
wc -l installer/cli.pyRepository: 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.pyRepository: 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.
| 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: |
There was a problem hiding this comment.
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}")🤖 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.
| 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 |
There was a problem hiding this comment.
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)🤖 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
There was a problem hiding this comment.
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_*returnsNonedue 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 inctx.config) so users can remediate. As per coding guidelines: installer/**/*.py: Review installer code for proper error handling and user feedback.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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 | ||
| ) |
There was a problem hiding this comment.
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.
| 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.
| assert "function claude" not in content | ||
| assert "end" not in content or "# after" in content |
There was a problem hiding this comment.
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.
| 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
There was a problem hiding this comment.
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_argto leverage shell word splitting, which works but is considered fragile and would trigger ShellCheck warnings (SC2086). IfVERSIONever 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 fiinstaller/steps/dependencies.py (2)
429-436: Update failure is silently ignored.When
sxalready exists, the function callssx updatebut ignores its return value, always returningTrue. 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")returnsFalse.
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
| _original_info = ui.info # noqa: F841 - stored for potential restoration | ||
| ui.info = lambda message: info_calls.append(message) |
There was a problem hiding this comment.
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.
| _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
There was a problem hiding this comment.
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 | 🟡 MinorCross-platform: Windows binary may need
.exeextension.The hardcoded binary name
pilotat line 16 won't resolve on Windows where executables typically require the.exeextension. 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/**/*.pyshould be reviewed for cross-platform compatibility.
🧹 Nitpick comments (2)
docs/site/index.html (1)
30-31: Consider using standard favicon format.Using
.jpgfor favicons works but.icoor.pngare more conventional and provide better compatibility (especially.icofor older browsers). This is a minor point if.jpgis 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):
|
🎉 This PR is included in version 6.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Major release introducing multi-pass verification at both planning and implementation phases, plus installer improvements for auto-version fetching.
New Features
plan-verifieragents verify the plan matches user requirements before approvalspec-verifieragents verify implementation matches the planVERSION=X.Y.Zto install specific versionsFiles Changed
pilot/agents/plan-verifier.md- New agent for plan verificationpilot/agents/spec-verifier.md- Updated code verification agentpilot/commands/spec.md- Added Step 7 for plan verificationpilot/rules/workflow-enforcement.md- Updated workflow documentationinstall.sh- Auto-version fetching and repo fallback.github/workflows/release.yml- Enhanced approval stepCHANGELOG.md+cliff.toml- Changelog generationBreaking Change
This is a BREAKING CHANGE release for Claude Pilot v6.0 with significant workflow improvements.
Test Plan
Summary by CodeRabbit
New Features
Documentation
Chores
Style
Tests