From 471d24fa23ec0304e50e52a507108aae8aac5c55 Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 1 Apr 2026 08:42:09 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20feat(20e):=20formula=20evolution=20?= =?UTF-8?q?=E2=80=94=20agent=20proposes=20changes=20via=20PR=20to=20.profi?= =?UTF-8?q?le=20(#88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- lib/profile.sh | 210 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 lib/profile.sh diff --git a/AGENTS.md b/AGENTS.md index a12b61f..a6ac1fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ disinto/ (code repo) │ preflight.sh — pre-flight data collection for supervisor formula │ supervisor-poll.sh — legacy bash orchestrator (superseded) ├── vault/ vault-env.sh — shared env setup (vault redesign in progress, see #73-#77) -├── lib/ env.sh, agent-session.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, guard.sh, mirrors.sh, pr-lifecycle.sh, issue-lifecycle.sh, worktree.sh, build-graph.py +├── lib/ env.sh, agent-session.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, guard.sh, mirrors.sh, pr-lifecycle.sh, issue-lifecycle.sh, worktree.sh, formula-session.sh, profile.sh, build-graph.py ├── projects/ *.toml.example — templates; *.toml — local per-box config (gitignored) ├── formulas/ Issue templates (TOML specs for multi-step agent tasks) └── docs/ Protocol docs (PHASE-PROTOCOL.md, EVIDENCE-ARCHITECTURE.md) diff --git a/lib/profile.sh b/lib/profile.sh new file mode 100644 index 0000000..79f8514 --- /dev/null +++ b/lib/profile.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# profile.sh — Helpers for agent .profile repo management +# +# Source after lib/env.sh and lib/formula-session.sh: +# source "$(dirname "$0")/../lib/env.sh" +# source "$(dirname "$0")/lib/formula-session.sh" +# source "$(dirname "$0")/lib/profile.sh" +# +# Required globals: FORGE_TOKEN, FORGE_URL, AGENT_IDENTITY, PROFILE_REPO_PATH +# +# Functions: +# profile_propose_formula NEW_FORMULA CONTENT REASON — create PR to update formula.toml + +set -euo pipefail + +# Internal log helper +_profile_log() { + if declare -f log >/dev/null 2>&1; then + log "profile: $*" + else + printf '[%s] profile: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >&2 + fi +} + +# ----------------------------------------------------------------------------- +# profile_propose_formula — Propose a formula change via PR +# +# Creates a branch, writes updated formula.toml, opens a PR, and returns PR number. +# Branch is protected (requires admin approval per #87). +# +# Args: +# $1 - NEW_FORMULA_CONTENT: The complete new formula.toml content +# $2 - REASON: Human-readable explanation of what changed and why +# +# Returns: +# 0 on success, prints PR number to stdout +# 1 on failure +# +# Example: +# source "$(dirname "$0")/../lib/env.sh" +# source "$(dirname "$0")/lib/formula-session.sh" +# source "$(dirname "$0")/lib/profile.sh" +# AGENT_IDENTITY="dev-bot" +# ensure_profile_repo "$AGENT_IDENTITY" +# profile_propose_formula "$new_formula" "Added new prompt pattern for code review" +# ----------------------------------------------------------------------------- +profile_propose_formula() { + local new_formula="$1" + local reason="$2" + + if [ -z "${AGENT_IDENTITY:-}" ]; then + _profile_log "ERROR: AGENT_IDENTITY not set" + return 1 + fi + + if [ -z "${PROFILE_REPO_PATH:-}" ]; then + _profile_log "ERROR: PROFILE_REPO_PATH not set — ensure_profile_repo not called" + return 1 + fi + + if [ -z "${FORGE_TOKEN:-}" ]; then + _profile_log "ERROR: FORGE_TOKEN not set" + return 1 + fi + + if [ -z "${FORGE_URL:-}" ]; then + _profile_log "ERROR: FORGE_URL not set" + return 1 + fi + + # Generate short description from reason for branch name + local short_desc + short_desc=$(printf '%s' "$reason" | \ + tr '[:upper:]' '[:lower:]' | \ + sed 's/[^a-z0-9 ]//g' | \ + sed 's/ */ /g' | \ + sed 's/^ *//;s/ *$//' | \ + cut -c1-40 | \ + tr ' ' '-') + + if [ -z "$short_desc" ]; then + short_desc="formula-update" + fi + + local branch_name="formula/${short_desc}" + local formula_path="${PROFILE_REPO_PATH}/formula.toml" + + _profile_log "Proposing formula change: ${branch_name}" + _profile_log "Reason: ${reason}" + + # Ensure we're on main branch and up-to-date + _profile_log "Fetching .profile repo" + ( + cd "$PROFILE_REPO_PATH" || return 1 + + git fetch origin main --quiet 2>/dev/null || \ + git fetch origin master --quiet 2>/dev/null || true + + # Reset to main/master + if git checkout main --quiet 2>/dev/null; then + git pull --ff-only origin main --quiet 2>/dev/null || true + elif git checkout master --quiet 2>/dev/null; then + git pull --ff-only origin master --quiet 2>/dev/null || true + else + _profile_log "ERROR: Failed to checkout main/master branch" + return 1 + fi + + # Create and checkout new branch + git checkout -b "$branch_name" 2>/dev/null || { + _profile_log "Branch ${branch_name} may already exist" + git checkout "$branch_name" 2>/dev/null || return 1 + } + + # Write formula.toml + printf '%s' "$new_formula" > "$formula_path" + + # Commit the change + git config user.name "${AGENT_IDENTITY}" || true + git config user.email "${AGENT_IDENTITY}@users.noreply.codeberg.org" || true + + git add "$formula_path" + git commit -m "formula: ${reason}" --no-verify || { + _profile_log "No changes to commit (formula unchanged)" + # Check if branch has any commits + if git rev-parse HEAD >/dev/null 2>&1; then + : # branch has commits, continue + else + _profile_log "ERROR: Failed to create commit" + return 1 + fi + } + + # Push branch + local remote="${FORGE_REMOTE:-origin}" + git push --set-upstream "$remote" "$branch_name" --quiet 2>/dev/null || { + _profile_log "ERROR: Failed to push branch" + return 1 + } + + _profile_log "Branch pushed: ${branch_name}" + + # Create PR + local forge_url="${FORGE_URL%/}" + local api_url="${forge_url}/api/v1/repos/${AGENT_IDENTITY}/.profile" + local primary_branch="main" + + # Check if main or master is the primary branch + if ! curl -sf -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${FORGE_TOKEN}" \ + "${api_url}/git/branches/main" 2>/dev/null | grep -q "200"; then + primary_branch="master" + fi + + local pr_title="formula: ${reason}" + local pr_body="# Formula Update + +**Reason:** ${reason} + +--- +*This PR was auto-generated by ${AGENT_IDENTITY}.* +" + + local pr_response http_code + local pr_json + pr_json=$(jq -n \ + --arg t "$pr_title" \ + --arg b "$pr_body" \ + --arg h "$branch_name" \ + --arg base "$primary_branch" \ + '{title:$t, body:$b, head:$h, base:$base}') || { + _profile_log "ERROR: Failed to build PR JSON" + return 1 + } + + pr_response=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${api_url}/pulls" \ + -d "$pr_json" || true) + + http_code=$(printf '%s\n' "$pr_response" | tail -1) + pr_response=$(printf '%s\n' "$pr_response" | sed '$d') + + if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then + local pr_num + pr_num=$(printf '%s' "$pr_response" | jq -r '.number') + _profile_log "PR created: #${pr_num}" + printf '%s' "$pr_num" + return 0 + else + # Check if PR already exists (409 conflict) + if [ "$http_code" = "409" ]; then + local existing_pr + existing_pr=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${api_url}/pulls?state=open&head=${AGENT_IDENTITY}:formula/${short_desc}" 2>/dev/null | \ + jq -r '.[0].number // empty') || true + if [ -n "$existing_pr" ]; then + _profile_log "PR already exists: #${existing_pr}" + printf '%s' "$existing_pr" + return 0 + fi + fi + _profile_log "ERROR: Failed to create PR (HTTP ${http_code})" + return 1 + fi + ) + + return $? +}