From patchwork Wed Dec 17 02:25:56 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 934 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765938428; bh=pA3MVt2ekv8UPQV7wYEpa2yHnwGoPyN6ytgr78Fas7w=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=Orfg5I9bOvj+KgWu7Dn8AIc1w3trczyx5yIPQ9Kh4hM89qCZJ9av5lflTjRm12T5V 4CnCdfLV2+LUfthh8eKHDgxZuofPj6F7c5hZqaMaDpmRdmfVz59HaE4UaURegxRcew ONHXXy2fHhzX1hPAYbsnP2Q/rG7q7bxHluspGC6pEYu2BS43wxQ0hkmwgvZzBE6iAV FUlgSa5ZM/fql7YeLZiJgx54N0ZzB7CTBGMhdGllZd5KURE52LTvU5JWpE4SmMEXWa moLOYDetO5wlx1XWZpS2Wh4foiZDxE5nyIKssWMl6sWzt0drO/IMyafP0o2lP/86Sl 1BoyGnZjdSy2g== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 7F5B668BAB for ; Tue, 16 Dec 2025 19:27:08 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id YowGFDTysutn for ; Tue, 16 Dec 2025 19:27:08 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765938428; bh=pA3MVt2ekv8UPQV7wYEpa2yHnwGoPyN6ytgr78Fas7w=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=Orfg5I9bOvj+KgWu7Dn8AIc1w3trczyx5yIPQ9Kh4hM89qCZJ9av5lflTjRm12T5V 4CnCdfLV2+LUfthh8eKHDgxZuofPj6F7c5hZqaMaDpmRdmfVz59HaE4UaURegxRcew ONHXXy2fHhzX1hPAYbsnP2Q/rG7q7bxHluspGC6pEYu2BS43wxQ0hkmwgvZzBE6iAV FUlgSa5ZM/fql7YeLZiJgx54N0ZzB7CTBGMhdGllZd5KURE52LTvU5JWpE4SmMEXWa moLOYDetO5wlx1XWZpS2Wh4foiZDxE5nyIKssWMl6sWzt0drO/IMyafP0o2lP/86Sl 1BoyGnZjdSy2g== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 6DAB268BA3 for ; Tue, 16 Dec 2025 19:27:08 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765938426; bh=p6l8ZbkJdZAl9Nux1EDlG1lFR9rY+Prs6QUgq507xHQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=wBNgkz90oJowyBOzAki4mEH+vgBuKPUczo/UPM47mU2tOSdjKPB6hL9079jxYWaPO E+c/X/+I6XNj52LW4FvSmplNOCnU4C/YcfGnP2US5idJkTIA+ryRRMDpfk9RFp9Tb+ 9AeFy03MEPbaXOZ0zYkjWs5TP+aWdVNyz8E6qPk8PB2j60UhPDJjLBz8FS16XwXuXs feCwyzJvH3P6KLGSKHm5II+Htq/RYmVhqx/1TZriJAGBmo+6u7DoTStGg3My0DgLUv GbRVUcwyIZYmfQgSv/FYjvNembVvtjw+5fDQHkRN1q1ahXNZ9szs4pnpZSlX0rH6eM rjQwsQ34b8xjA== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id C98F968AFB; Tue, 16 Dec 2025 19:27:06 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id 5vIIjQHIMSZ1; Tue, 16 Dec 2025 19:27:06 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765938422; bh=WH7CdHzUKDmWwvANL2APUhZdQOWbE//FCyPYeC4hB5M=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=B4uxOSDDLTKM57jawb/V0HrSwefJrnr0zUXZfJvGdPy61u+njFAvlbxUqDG7mLV2H +6THc5I3V6f6TDj5LuchEPY1XTUXycNpknwZ4dF+r9alHd0nEFMPnLq4uh99cZhwGN 45khPnXE4+67+ohMQl5Ngfxu9SjeLK2qaa6SfbKX1MFRIpdrFEjuKVgWMtm92yrp2t 4TWCGsM4Vtzm/rO7Jm8R/bNsOa7bUItibYsTe0fAfPz4dePZ0ni22zRiD8YDGnY5co r2a8T7EIZ8AqqNlPJLnOqJkvecbalQ2eY4ex6GM84SJfsAdt3TRmQ/uF/xZBibZTWW /mzLhAbFxdyBw== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 4999F6884F; Tue, 16 Dec 2025 19:27:02 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Tue, 16 Dec 2025 19:25:56 -0700 Message-ID: <20251217022611.389379-9-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251217022611.389379-1-sjg@u-boot.org> References: <20251217022611.389379-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: ELE6CGMEHKXHD2G67BWE7Y7CQJKQGJ5F X-Message-ID-Hash: ELE6CGMEHKXHD2G67BWE7Y7CQJKQGJ5F X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Heinrich Schuchardt , Simon Glass , "Claude Opus 4 . 5" X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 08/17] pickman: Add a Claude agent List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass 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 Signed-off-by: Simon Glass --- 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 +# +"""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 + - For merge commits (identified by "Merge" in subject): git cherry-pick -x -m 1 --allow-empty + 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))