diff --git a/bin/disinto b/bin/disinto index 09757c8..ed4c2b5 100755 --- a/bin/disinto +++ b/bin/disinto @@ -26,6 +26,7 @@ set -euo pipefail FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)" source "${FACTORY_ROOT}/lib/env.sh" source "${FACTORY_ROOT}/lib/ops-setup.sh" +source "${FACTORY_ROOT}/lib/hire-agent.sh" # ── Helpers ────────────────────────────────────────────────────────────────── @@ -2571,453 +2572,7 @@ disinto_shell() { # Creates a Forgejo user and .profile repo for an agent. # Usage: disinto hire-an-agent [--formula ] -disinto_hire_an_agent() { - local agent_name="${1:-}" - local role="${2:-}" - local formula_path="" - local local_model="" - local poll_interval="" - - if [ -z "$agent_name" ] || [ -z "$role" ]; then - echo "Error: agent-name and role required" >&2 - echo "Usage: disinto hire-an-agent [--formula ] [--local-model ] [--poll-interval ]" >&2 - exit 1 - fi - shift 2 - - # Parse flags - while [ $# -gt 0 ]; do - case "$1" in - --formula) - formula_path="$2" - shift 2 - ;; - --local-model) - local_model="$2" - shift 2 - ;; - --poll-interval) - poll_interval="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" >&2 - exit 1 - ;; - esac - done - - # Default formula path — try both naming conventions - if [ -z "$formula_path" ]; then - formula_path="${FACTORY_ROOT}/formulas/${role}.toml" - if [ ! -f "$formula_path" ]; then - formula_path="${FACTORY_ROOT}/formulas/run-${role}.toml" - fi - fi - - # Validate formula exists - if [ ! -f "$formula_path" ]; then - echo "Error: formula not found at ${formula_path}" >&2 - exit 1 - fi - - echo "── Hiring agent: ${agent_name} (${role}) ───────────────────────" - echo "Formula: ${formula_path}" - if [ -n "$local_model" ]; then - echo "Local model: ${local_model}" - echo "Poll interval: ${poll_interval:-300}s" - fi - - # Ensure FORGE_TOKEN is set - if [ -z "${FORGE_TOKEN:-}" ]; then - echo "Error: FORGE_TOKEN not set" >&2 - exit 1 - fi - - # Get Forge URL - local forge_url="${FORGE_URL:-http://localhost:3000}" - echo "Forge: ${forge_url}" - - # Step 1: Create user via API (skip if exists) - echo "" - echo "Step 1: Creating user '${agent_name}' (if not exists)..." - - local user_pass="" - local admin_pass="" - - # Read admin password from .env for standalone runs (#184) - local env_file="${FACTORY_ROOT}/.env" - if [ -f "$env_file" ] && grep -q '^FORGE_ADMIN_PASS=' "$env_file" 2>/dev/null; then - admin_pass=$(grep '^FORGE_ADMIN_PASS=' "$env_file" | head -1 | cut -d= -f2-) - fi - - # Get admin token early (needed for both user creation and password reset) - local admin_user="disinto-admin" - admin_pass="${admin_pass:-admin}" - local admin_token="" - local admin_token_name - admin_token_name="temp-token-$(date +%s)" - admin_token=$(curl -sf -X POST \ - -u "${admin_user}:${admin_pass}" \ - -H "Content-Type: application/json" \ - "${forge_url}/api/v1/users/${admin_user}/tokens" \ - -d "{\"name\":\"${admin_token_name}\",\"scopes\":[\"all\"]}" 2>/dev/null \ - | jq -r '.sha1 // empty') || admin_token="" - if [ -z "$admin_token" ]; then - # Token might already exist — try listing - admin_token=$(curl -sf \ - -u "${admin_user}:${admin_pass}" \ - "${forge_url}/api/v1/users/${admin_user}/tokens" 2>/dev/null \ - | jq -r '.[0].sha1 // empty') || admin_token="" - fi - if [ -z "$admin_token" ]; then - echo "Error: failed to obtain admin API token" >&2 - echo " Cannot proceed without admin privileges" >&2 - exit 1 - fi - - if curl -sf --max-time 5 "${forge_url}/api/v1/users/${agent_name}" >/dev/null 2>&1; then - echo " User '${agent_name}' already exists" - # Reset user password so we can get a token (#184) - user_pass="agent-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" - # Use Forgejo CLI to reset password (API PATCH ignores must_change_password in Forgejo 11.x) - if _forgejo_exec forgejo admin user change-password \ - --username "${agent_name}" \ - --password "${user_pass}" \ - --must-change-password=false >/dev/null 2>&1; then - echo " Reset password for existing user '${agent_name}'" - else - echo " Warning: could not reset password for existing user" >&2 - fi - else - # Create user using basic auth (admin token fallback would poison subsequent calls) - # Create the user - user_pass="agent-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" - if curl -sf -X POST \ - -u "${admin_user}:${admin_pass}" \ - -H "Content-Type: application/json" \ - "${forge_url}/api/v1/admin/users" \ - -d "{\"username\":\"${agent_name}\",\"password\":\"${user_pass}\",\"email\":\"${agent_name}@${PROJECT_NAME:-disinto}.local\",\"full_name\":\"${agent_name}\",\"active\":true,\"admin\":false,\"must_change_password\":false}" >/dev/null 2>&1; then - echo " Created user '${agent_name}'" - else - echo " Warning: failed to create user via admin API" >&2 - # Try alternative: user might already exist - if curl -sf --max-time 5 "${forge_url}/api/v1/users/${agent_name}" >/dev/null 2>&1; then - echo " User '${agent_name}' exists (confirmed)" - else - echo " Error: failed to create user '${agent_name}'" >&2 - exit 1 - fi - fi - fi - - # Step 1.5: Generate Forge token for the new/existing user - echo "" - echo "Step 1.5: Generating Forge token for '${agent_name}'..." - - # Convert role to uppercase token variable name (e.g., architect -> FORGE_ARCHITECT_TOKEN) - local role_upper - role_upper=$(echo "$role" | tr '[:lower:]' '[:upper:]') - local token_var="FORGE_${role_upper}_TOKEN" - - # Generate token using the user's password (basic auth) - local agent_token="" - agent_token=$(curl -sf -X POST \ - -u "${agent_name}:${user_pass}" \ - -H "Content-Type: application/json" \ - "${forge_url}/api/v1/users/${agent_name}/tokens" \ - -d "{\"name\":\"disinto-${agent_name}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \ - | jq -r '.sha1 // empty') || agent_token="" - - if [ -z "$agent_token" ]; then - # Token name collision — create with timestamp suffix - agent_token=$(curl -sf -X POST \ - -u "${agent_name}:${user_pass}" \ - -H "Content-Type: application/json" \ - "${forge_url}/api/v1/users/${agent_name}/tokens" \ - -d "{\"name\":\"disinto-${agent_name}-$(date +%s)\",\"scopes\":[\"all\"]}" 2>/dev/null \ - | jq -r '.sha1 // empty') || agent_token="" - fi - - if [ -z "$agent_token" ]; then - echo " Warning: failed to create API token for '${agent_name}'" >&2 - else - # Store token in .env under the role-specific variable name - if grep -q "^${token_var}=" "$env_file" 2>/dev/null; then - # Use sed with alternative delimiter and proper escaping for special chars in token - local escaped_token - escaped_token=$(printf '%s\n' "$agent_token" | sed 's/[&/\]/\\&/g') - sed -i "s|^${token_var}=.*|${token_var}=${escaped_token}|" "$env_file" - echo " ${agent_name} token updated (${token_var})" - else - printf '%s=%s\n' "$token_var" "$agent_token" >> "$env_file" - echo " ${agent_name} token saved (${token_var})" - fi - export "${token_var}=${agent_token}" - fi - - # Step 2: Create .profile repo on Forgejo - echo "" - echo "Step 2: Creating '${agent_name}/.profile' repo (if not exists)..." - - if curl -sf --max-time 5 "${forge_url}/api/v1/repos/${agent_name}/.profile" >/dev/null 2>&1; then - echo " Repo '${agent_name}/.profile' already exists" - else - # Create the repo using the admin API to ensure it's created in the agent's namespace. - # Using POST /api/v1/user/repos with a user token would create the repo under the - # authenticated user, which could be wrong if the token belongs to a different user. - # The admin API POST /api/v1/admin/users/{username}/repos explicitly creates in the - # specified user's namespace. - local create_output - create_output=$(curl -sf -X POST \ - -u "${admin_user}:${admin_pass}" \ - -H "Content-Type: application/json" \ - "${forge_url}/api/v1/admin/users/${agent_name}/repos" \ - -d "{\"name\":\".profile\",\"description\":\"${agent_name}'s .profile repo\",\"private\":true,\"auto_init\":false}" 2>&1) || true - - if echo "$create_output" | grep -q '"id":\|[0-9]'; then - echo " Created repo '${agent_name}/.profile' (via admin API)" - else - echo " Error: failed to create repo '${agent_name}/.profile'" >&2 - echo " Response: ${create_output}" >&2 - exit 1 - fi - fi - - # Step 3: Clone repo and create initial commit - echo "" - echo "Step 3: Cloning repo and creating initial commit..." - - local clone_dir="/tmp/.profile-clone-${agent_name}" - rm -rf "$clone_dir" - mkdir -p "$clone_dir" - - # Build authenticated clone URL using basic auth (user_pass is always set in Step 1) - if [ -z "${user_pass:-}" ]; then - echo " Error: no user password available for cloning" >&2 - exit 1 - fi - - local clone_url="${forge_url}/${agent_name}/.profile.git" - local auth_url - auth_url=$(printf '%s' "$forge_url" | sed "s|://|://${agent_name}:${user_pass}@|") - auth_url="${auth_url}/${agent_name}/.profile.git" - - # Display unauthenticated URL (auth token only in actual git clone command) - echo " Cloning: ${forge_url}/${agent_name}/.profile.git" - - # Try authenticated clone first (required for private repos) - if ! git clone --quiet "$auth_url" "$clone_dir" 2>/dev/null; then - echo " Error: failed to clone repo with authentication" >&2 - echo " Note: Ensure the user has a valid API token with repository access" >&2 - rm -rf "$clone_dir" - exit 1 - fi - - # Configure git - git -C "$clone_dir" config user.name "disinto-admin" - git -C "$clone_dir" config user.email "disinto-admin@localhost" - - # Create directory structure - echo " Creating directory structure..." - mkdir -p "${clone_dir}/journal" - mkdir -p "${clone_dir}/knowledge" - touch "${clone_dir}/journal/.gitkeep" - touch "${clone_dir}/knowledge/.gitkeep" - - # Copy formula - echo " Copying formula..." - cp "$formula_path" "${clone_dir}/formula.toml" - - # Create README - if [ ! -f "${clone_dir}/README.md" ]; then - cat > "${clone_dir}/README.md" </dev/null; then - git -C "$clone_dir" commit -m "chore: initial .profile setup" -q - git -C "$clone_dir" push origin main >/dev/null 2>&1 || \ - git -C "$clone_dir" push origin master >/dev/null 2>&1 || true - echo " Committed: initial .profile setup" - else - echo " No changes to commit" - fi - - rm -rf "$clone_dir" - - # Step 4: Set up branch protection - echo "" - echo "Step 4: Setting up branch protection..." - - # Source branch-protection.sh helper - local bp_script="${FACTORY_ROOT}/lib/branch-protection.sh" - if [ -f "$bp_script" ]; then - # Source required environment - if [ -f "${FACTORY_ROOT}/lib/env.sh" ]; then - source "${FACTORY_ROOT}/lib/env.sh" - fi - - # Set up branch protection for .profile repo - if source "$bp_script" 2>/dev/null && setup_profile_branch_protection "${agent_name}/.profile" "main"; then - echo " Branch protection configured for main branch" - echo " - Requires 1 approval before merge" - echo " - Admin-only merge enforcement" - echo " - Journal branch created for direct agent pushes" - else - echo " Warning: could not configure branch protection (Forgejo API may not be available)" - echo " Note: Branch protection can be set up manually later" - fi - else - echo " Warning: branch-protection.sh not found at ${bp_script}" - fi - - # Step 5: Create state marker - echo "" - echo "Step 5: Creating state marker..." - - local state_dir="${FACTORY_ROOT}/state" - mkdir -p "$state_dir" - local state_file="${state_dir}/.${role}-active" - - if [ ! -f "$state_file" ]; then - touch "$state_file" - echo " Created: ${state_file}" - else - echo " State marker already exists: ${state_file}" - fi - - # Step 6: Set up local model agent (if --local-model specified) - if [ -n "$local_model" ]; then - echo "" - echo "Step 6: Configuring local model agent..." - - local compose_file="${FACTORY_ROOT}/docker-compose.yml" - local override_file="${FACTORY_ROOT}/docker-compose.override.yml" - local override_dir - override_dir=$(dirname "$override_file") - mkdir -p "$override_dir" - - # Validate model endpoint is reachable - echo " Validating model endpoint: ${local_model}" - if ! curl -sf --max-time 10 "${local_model}/health" >/dev/null 2>&1; then - # Try /v1/chat/completions as fallback endpoint check - if ! curl -sf --max-time 10 "${local_model}/v1/chat/completions" >/dev/null 2>&1; then - echo " Warning: model endpoint may not be reachable at ${local_model}" - echo " Continuing with configuration..." - fi - else - echo " Model endpoint is reachable" - fi - - # Generate service name from agent name (lowercase) - local service_name="agents-${agent_name}" - service_name=$(echo "$service_name" | tr '[:upper:]' '[:lower:]') - - # Set default poll interval - local interval="${poll_interval:-300}" - - # Generate the override compose file - # Bash expands ${service_name}, ${local_model}, ${interval}, ${PROJECT_NAME} at generation time - # \$HOME, \$FORGE_TOKEN become ${HOME}, ${FORGE_TOKEN} in the file for docker-compose runtime expansion - cat > "$override_file" < [--formula ] [--local-model ] [--poll-interval ] +# ============================================================================= +set -euo pipefail + +disinto_hire_an_agent() { + local agent_name="${1:-}" + local role="${2:-}" + local formula_path="" + local local_model="" + local poll_interval="" + + if [ -z "$agent_name" ] || [ -z "$role" ]; then + echo "Error: agent-name and role required" >&2 + echo "Usage: disinto hire-an-agent [--formula ] [--local-model ] [--poll-interval ]" >&2 + exit 1 + fi + shift 2 + + # Parse flags + while [ $# -gt 0 ]; do + case "$1" in + --formula) + formula_path="$2" + shift 2 + ;; + --local-model) + local_model="$2" + shift 2 + ;; + --poll-interval) + poll_interval="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac + done + + # Default formula path — try both naming conventions + if [ -z "$formula_path" ]; then + formula_path="${FACTORY_ROOT}/formulas/${role}.toml" + if [ ! -f "$formula_path" ]; then + formula_path="${FACTORY_ROOT}/formulas/run-${role}.toml" + fi + fi + + # Validate formula exists + if [ ! -f "$formula_path" ]; then + echo "Error: formula not found at ${formula_path}" >&2 + exit 1 + fi + + echo "── Hiring agent: ${agent_name} (${role}) ───────────────────────" + echo "Formula: ${formula_path}" + if [ -n "$local_model" ]; then + echo "Local model: ${local_model}" + echo "Poll interval: ${poll_interval:-300}s" + fi + + # Ensure FORGE_TOKEN is set + if [ -z "${FORGE_TOKEN:-}" ]; then + echo "Error: FORGE_TOKEN not set" >&2 + exit 1 + fi + + # Get Forge URL + local forge_url="${FORGE_URL:-http://localhost:3000}" + echo "Forge: ${forge_url}" + + # Step 1: Create user via API (skip if exists) + echo "" + echo "Step 1: Creating user '${agent_name}' (if not exists)..." + + local user_pass="" + local admin_pass="" + + # Read admin password from .env for standalone runs (#184) + local env_file="${FACTORY_ROOT}/.env" + if [ -f "$env_file" ] && grep -q '^FORGE_ADMIN_PASS=' "$env_file" 2>/dev/null; then + admin_pass=$(grep '^FORGE_ADMIN_PASS=' "$env_file" | head -1 | cut -d= -f2-) + fi + + # Get admin token early (needed for both user creation and password reset) + local admin_user="disinto-admin" + admin_pass="${admin_pass:-admin}" + local admin_token="" + local admin_token_name + admin_token_name="temp-token-$(date +%s)" + admin_token=$(curl -sf -X POST \ + -u "${admin_user}:${admin_pass}" \ + -H "Content-Type: application/json" \ + "${forge_url}/api/v1/users/${admin_user}/tokens" \ + -d "{\"name\":\"${admin_token_name}\",\"scopes\":[\"all\"]}" 2>/dev/null \ + | jq -r '.sha1 // empty') || admin_token="" + if [ -z "$admin_token" ]; then + # Token might already exist — try listing + admin_token=$(curl -sf \ + -u "${admin_user}:${admin_pass}" \ + "${forge_url}/api/v1/users/${admin_user}/tokens" 2>/dev/null \ + | jq -r '.[0].sha1 // empty') || admin_token="" + fi + if [ -z "$admin_token" ]; then + echo "Error: failed to obtain admin API token" >&2 + echo " Cannot proceed without admin privileges" >&2 + exit 1 + fi + + if curl -sf --max-time 5 "${forge_url}/api/v1/users/${agent_name}" >/dev/null 2>&1; then + echo " User '${agent_name}' already exists" + # Reset user password so we can get a token (#184) + user_pass="agent-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" + # Use Forgejo CLI to reset password (API PATCH ignores must_change_password in Forgejo 11.x) + if _forgejo_exec forgejo admin user change-password \ + --username "${agent_name}" \ + --password "${user_pass}" \ + --must-change-password=false >/dev/null 2>&1; then + echo " Reset password for existing user '${agent_name}'" + else + echo " Warning: could not reset password for existing user" >&2 + fi + else + # Create user using basic auth (admin token fallback would poison subsequent calls) + # Create the user + user_pass="agent-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" + if curl -sf -X POST \ + -u "${admin_user}:${admin_pass}" \ + -H "Content-Type: application/json" \ + "${forge_url}/api/v1/admin/users" \ + -d "{\"username\":\"${agent_name}\",\"password\":\"${user_pass}\",\"email\":\"${agent_name}@${PROJECT_NAME:-disinto}.local\",\"full_name\":\"${agent_name}\",\"active\":true,\"admin\":false,\"must_change_password\":false}" >/dev/null 2>&1; then + echo " Created user '${agent_name}'" + else + echo " Warning: failed to create user via admin API" >&2 + # Try alternative: user might already exist + if curl -sf --max-time 5 "${forge_url}/api/v1/users/${agent_name}" >/dev/null 2>&1; then + echo " User '${agent_name}' exists (confirmed)" + else + echo " Error: failed to create user '${agent_name}'" >&2 + exit 1 + fi + fi + fi + + # Step 1.5: Generate Forge token for the new/existing user + echo "" + echo "Step 1.5: Generating Forge token for '${agent_name}'..." + + # Convert role to uppercase token variable name (e.g., architect -> FORGE_ARCHITECT_TOKEN) + local role_upper + role_upper=$(echo "$role" | tr '[:lower:]' '[:upper:]') + local token_var="FORGE_${role_upper}_TOKEN" + + # Generate token using the user's password (basic auth) + local agent_token="" + agent_token=$(curl -sf -X POST \ + -u "${agent_name}:${user_pass}" \ + -H "Content-Type: application/json" \ + "${forge_url}/api/v1/users/${agent_name}/tokens" \ + -d "{\"name\":\"disinto-${agent_name}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \ + | jq -r '.sha1 // empty') || agent_token="" + + if [ -z "$agent_token" ]; then + # Token name collision — create with timestamp suffix + agent_token=$(curl -sf -X POST \ + -u "${agent_name}:${user_pass}" \ + -H "Content-Type: application/json" \ + "${forge_url}/api/v1/users/${agent_name}/tokens" \ + -d "{\"name\":\"disinto-${agent_name}-$(date +%s)\",\"scopes\":[\"all\"]}" 2>/dev/null \ + | jq -r '.sha1 // empty') || agent_token="" + fi + + if [ -z "$agent_token" ]; then + echo " Warning: failed to create API token for '${agent_name}'" >&2 + else + # Store token in .env under the role-specific variable name + if grep -q "^${token_var}=" "$env_file" 2>/dev/null; then + # Use sed with alternative delimiter and proper escaping for special chars in token + local escaped_token + escaped_token=$(printf '%s\n' "$agent_token" | sed 's/[&/\]/\\&/g') + sed -i "s|^${token_var}=.*|${token_var}=${escaped_token}|" "$env_file" + echo " ${agent_name} token updated (${token_var})" + else + printf '%s=%s\n' "$token_var" "$agent_token" >> "$env_file" + echo " ${agent_name} token saved (${token_var})" + fi + export "${token_var}=${agent_token}" + fi + + # Step 2: Create .profile repo on Forgejo + echo "" + echo "Step 2: Creating '${agent_name}/.profile' repo (if not exists)..." + + if curl -sf --max-time 5 "${forge_url}/api/v1/repos/${agent_name}/.profile" >/dev/null 2>&1; then + echo " Repo '${agent_name}/.profile' already exists" + else + # Create the repo using the admin API to ensure it's created in the agent's namespace. + # Using POST /api/v1/user/repos with a user token would create the repo under the + # authenticated user, which could be wrong if the token belongs to a different user. + # The admin API POST /api/v1/admin/users/{username}/repos explicitly creates in the + # specified user's namespace. + local create_output + create_output=$(curl -sf -X POST \ + -u "${admin_user}:${admin_pass}" \ + -H "Content-Type: application/json" \ + "${forge_url}/api/v1/admin/users/${agent_name}/repos" \ + -d "{\"name\":\".profile\",\"description\":\"${agent_name}'s .profile repo\",\"private\":true,\"auto_init\":false}" 2>&1) || true + + if echo "$create_output" | grep -q '"id":\|[0-9]'; then + echo " Created repo '${agent_name}/.profile' (via admin API)" + else + echo " Error: failed to create repo '${agent_name}/.profile'" >&2 + echo " Response: ${create_output}" >&2 + exit 1 + fi + fi + + # Step 3: Clone repo and create initial commit + echo "" + echo "Step 3: Cloning repo and creating initial commit..." + + local clone_dir="/tmp/.profile-clone-${agent_name}" + rm -rf "$clone_dir" + mkdir -p "$clone_dir" + + # Build authenticated clone URL using basic auth (user_pass is always set in Step 1) + if [ -z "${user_pass:-}" ]; then + echo " Error: no user password available for cloning" >&2 + exit 1 + fi + + local clone_url="${forge_url}/${agent_name}/.profile.git" + local auth_url + auth_url=$(printf '%s' "$forge_url" | sed "s|://|://${agent_name}:${user_pass}@|") + auth_url="${auth_url}/${agent_name}/.profile.git" + + # Display unauthenticated URL (auth token only in actual git clone command) + echo " Cloning: ${forge_url}/${agent_name}/.profile.git" + + # Try authenticated clone first (required for private repos) + if ! git clone --quiet "$auth_url" "$clone_dir" 2>/dev/null; then + echo " Error: failed to clone repo with authentication" >&2 + echo " Note: Ensure the user has a valid API token with repository access" >&2 + rm -rf "$clone_dir" + exit 1 + fi + + # Configure git + git -C "$clone_dir" config user.name "disinto-admin" + git -C "$clone_dir" config user.email "disinto-admin@localhost" + + # Create directory structure + echo " Creating directory structure..." + mkdir -p "${clone_dir}/journal" + mkdir -p "${clone_dir}/knowledge" + touch "${clone_dir}/journal/.gitkeep" + touch "${clone_dir}/knowledge/.gitkeep" + + # Copy formula + echo " Copying formula..." + cp "$formula_path" "${clone_dir}/formula.toml" + + # Create README + if [ ! -f "${clone_dir}/README.md" ]; then + cat > "${clone_dir}/README.md" </dev/null; then + git -C "$clone_dir" commit -m "chore: initial .profile setup" -q + git -C "$clone_dir" push origin main >/dev/null 2>&1 || \ + git -C "$clone_dir" push origin master >/dev/null 2>&1 || true + echo " Committed: initial .profile setup" + else + echo " No changes to commit" + fi + + rm -rf "$clone_dir" + + # Step 4: Set up branch protection + echo "" + echo "Step 4: Setting up branch protection..." + + # Source branch-protection.sh helper + local bp_script="${FACTORY_ROOT}/lib/branch-protection.sh" + if [ -f "$bp_script" ]; then + # Source required environment + if [ -f "${FACTORY_ROOT}/lib/env.sh" ]; then + source "${FACTORY_ROOT}/lib/env.sh" + fi + + # Set up branch protection for .profile repo + if source "$bp_script" 2>/dev/null && setup_profile_branch_protection "${agent_name}/.profile" "main"; then + echo " Branch protection configured for main branch" + echo " - Requires 1 approval before merge" + echo " - Admin-only merge enforcement" + echo " - Journal branch created for direct agent pushes" + else + echo " Warning: could not configure branch protection (Forgejo API may not be available)" + echo " Note: Branch protection can be set up manually later" + fi + else + echo " Warning: branch-protection.sh not found at ${bp_script}" + fi + + # Step 5: Create state marker + echo "" + echo "Step 5: Creating state marker..." + + local state_dir="${FACTORY_ROOT}/state" + mkdir -p "$state_dir" + local state_file="${state_dir}/.${role}-active" + + if [ ! -f "$state_file" ]; then + touch "$state_file" + echo " Created: ${state_file}" + else + echo " State marker already exists: ${state_file}" + fi + + # Step 6: Set up local model agent (if --local-model specified) + if [ -n "$local_model" ]; then + echo "" + echo "Step 6: Configuring local model agent..." + + local compose_file="${FACTORY_ROOT}/docker-compose.yml" + local override_file="${FACTORY_ROOT}/docker-compose.override.yml" + local override_dir + override_dir=$(dirname "$override_file") + mkdir -p "$override_dir" + + # Validate model endpoint is reachable + echo " Validating model endpoint: ${local_model}" + if ! curl -sf --max-time 10 "${local_model}/health" >/dev/null 2>&1; then + # Try /v1/chat/completions as fallback endpoint check + if ! curl -sf --max-time 10 "${local_model}/v1/chat/completions" >/dev/null 2>&1; then + echo " Warning: model endpoint may not be reachable at ${local_model}" + echo " Continuing with configuration..." + fi + else + echo " Model endpoint is reachable" + fi + + # Generate service name from agent name (lowercase) + local service_name="agents-${agent_name}" + service_name=$(echo "$service_name" | tr '[:upper:]' '[:lower:]') + + # Set default poll interval + local interval="${poll_interval:-300}" + + # Generate the override compose file + # Bash expands ${service_name}, ${local_model}, ${interval}, ${PROJECT_NAME} at generation time + # \$HOME, \$FORGE_TOKEN become ${HOME}, ${FORGE_TOKEN} in the file for docker-compose runtime expansion + cat > "$override_file" <