From: Simon Glass <simon.glass@canonical.com> Add an agent which uses the Claude Agent SDK to automate cherry-picking commits. The agent module provides an async interface to the Claude Agent SDK with a synchronous wrapper for easy use from the CLI. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/pickman/agent.py | 134 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tools/pickman/agent.py diff --git a/tools/pickman/agent.py b/tools/pickman/agent.py new file mode 100644 index 00000000000..3b4b96838b7 --- /dev/null +++ b/tools/pickman/agent.py @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright 2025 Canonical Ltd. +# Written by Simon Glass <simon.glass@canonical.com> +# +"""Agent module for pickman - uses Claude to automate cherry-picking.""" + +import asyncio +import os +import sys + +# Allow 'from pickman import xxx' to work via symlink +our_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.join(our_path, '..')) + +# pylint: disable=wrong-import-position,import-error +from u_boot_pylib import tout + +# Check if claude_agent_sdk is available +try: + from claude_agent_sdk import query, ClaudeAgentOptions + AGENT_AVAILABLE = True +except ImportError: + AGENT_AVAILABLE = False + + +def check_available(): + """Check if the Claude Agent SDK is available + + Returns: + bool: True if available, False otherwise + """ + if not AGENT_AVAILABLE: + tout.error('Claude Agent SDK not available') + tout.error('Install with: pip install claude-agent-sdk') + return False + return True + + +async def run(commits, source, branch_name, repo_path=None): + """Run the Claude agent to cherry-pick commits + + Args: + commits (list): list of (hash, short_hash, subject) tuples + source (str): source branch name + branch_name (str): name for the new branch to create + repo_path (str): path to repository (defaults to current directory) + + Returns: + bool: True on success, False on failure + """ + if not check_available(): + return False + + if repo_path is None: + repo_path = os.getcwd() + + # Build commit list for the prompt + commit_list = '\n'.join( + f' - {short_hash}: {subject}' + for _, short_hash, subject in commits + ) + commit_hashes = ' '.join(hash for hash, _, _ in commits) + + prompt = f"""Cherry-pick the following commits from {source} branch: + +{commit_list} + +Steps to follow: +1. First run 'git status' to check the repository state is clean +2. Create and checkout a new branch based on ci/master: git checkout -b {branch_name} ci/master +3. Cherry-pick each commit in order: + - For regular commits: git cherry-pick -x <hash> + - For merge commits (identified by "Merge" in subject): git cherry-pick -x -m 1 --allow-empty <hash> + Cherry-pick one commit at a time to handle each appropriately. +4. If there are conflicts: + - Show the conflicting files + - Try to resolve simple conflicts automatically + - For complex conflicts, describe what needs manual resolution and abort + - When fix-ups are needed, amend the commit to add a one-line note at the end + of the commit message describing the changes made +5. After ALL cherry-picks complete, verify with 'git log --oneline -n {len(commits) + 2}' + Ensure all {len(commits)} commits are present. +6. Run 'buildman -L --board sandbox -w -o /tmp/pickman' to verify the build +7. Report the final status including: + - Build result (ok or list of warnings/errors) + - Any fix-ups that were made + +The cherry-pick branch will be left ready for pushing. Do NOT merge it back to any other branch. + +Important: +- Stop immediately if there's a conflict that cannot be auto-resolved +- Do not force push or modify history +- If cherry-pick fails, run 'git cherry-pick --abort' +""" + + options = ClaudeAgentOptions( + allowed_tools=['Bash', 'Read', 'Grep'], + cwd=repo_path, + ) + + tout.info(f'Starting Claude agent to cherry-pick {len(commits)} commits...') + tout.info('') + + conversation_log = [] + try: + async for message in query(prompt=prompt, options=options): + # Print agent output and capture it + if hasattr(message, 'content'): + for block in message.content: + if hasattr(block, 'text'): + print(block.text) + conversation_log.append(block.text) + return True, '\n\n'.join(conversation_log) + except (RuntimeError, ValueError, OSError) as exc: + tout.error(f'Agent failed: {exc}') + return False, '\n\n'.join(conversation_log) + + +def cherry_pick_commits(commits, source, branch_name, repo_path=None): + """Synchronous wrapper for running the cherry-pick agent + + Args: + commits (list): list of (hash, short_hash, subject) tuples + source (str): source branch name + branch_name (str): name for the new branch to create + repo_path (str): path to repository (defaults to current directory) + + Returns: + tuple: (success, conversation_log) where success is bool and + conversation_log is the agent's output text + """ + return asyncio.run(run(commits, source, branch_name, + repo_path)) -- 2.43.0