fix: refactor: lib/env.sh — split into a defined-surface shared lib; entrypoints own context-specific paths (#674)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/smoke-init Pipeline was successful

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-11 13:21:30 +00:00
parent 3d7c27f6c6
commit 6589c761ba
6 changed files with 86 additions and 25 deletions

View file

@ -24,6 +24,13 @@
set -euo pipefail
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# Ensure USER and HOME are set — preconditions for lib/env.sh (#674).
# On the host these are normally provided by the shell; defensive defaults
# handle edge cases (cron, minimal containers).
export USER="${USER:-$(id -un)}"
export HOME="${HOME:-$(eval echo "~${USER}")}"
source "${FACTORY_ROOT}/lib/env.sh"
source "${FACTORY_ROOT}/lib/ops-setup.sh" # setup_ops_repo, migrate_ops_repo
source "${FACTORY_ROOT}/lib/hire-agent.sh"

View file

@ -101,8 +101,11 @@ configure_tea_login() {
log "Agent container starting"
# Set USER for scripts that source lib/env.sh (e.g., OPS_REPO_ROOT default)
# Set USER and HOME for scripts that source lib/env.sh.
# These are preconditions required by lib/env.sh's surface contract.
# gosu agent inherits the parent's env, so exports here propagate to all children.
export USER=agent
export HOME=/home/agent
# Verify Claude CLI is available (expected via volume mount from host).
if ! command -v claude &>/dev/null; then
@ -343,6 +346,25 @@ while true; do
# The flock on session.lock already serializes claude -p calls.
for toml in "${DISINTO_DIR}"/projects/*.toml; do
[ -f "$toml" ] || continue
# Parse project name and primary branch from TOML so env.sh preconditions
# are satisfied when agent scripts source it (#674).
_toml_vals=$(python3 -c "
import tomllib, sys
with open(sys.argv[1], 'rb') as f:
cfg = tomllib.load(f)
print(cfg.get('name', ''))
print(cfg.get('primary_branch', 'main'))
" "$toml" 2>/dev/null || true)
_pname=$(sed -n '1p' <<< "$_toml_vals")
_pbranch=$(sed -n '2p' <<< "$_toml_vals")
[ -n "$_pname" ] || { log "WARNING: could not parse project name from ${toml} — skipping"; continue; }
export PROJECT_NAME="$_pname"
export PROJECT_REPO_ROOT="/home/agent/repos/${_pname}"
export OPS_REPO_ROOT="/home/agent/repos/${_pname}-ops"
export PRIMARY_BRANCH="${_pbranch:-main}"
log "Processing project TOML: ${toml}"
# --- Fast agents: run in background, wait before slow agents ---

View file

@ -1,8 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
# Set USER before sourcing env.sh (Alpine doesn't set USER)
export USER="${USER:-root}"
# Set USER and HOME before sourcing env.sh — preconditions for lib/env.sh (#674).
export USER="${USER:-agent}"
export HOME="${HOME:-/home/agent}"
FORGE_URL="${FORGE_URL:-http://forgejo:3000}"
@ -138,6 +139,12 @@ if [ -n "${EDGE_TUNNEL_HOST:-}" ]; then
fi
fi
# Set project context vars for scripts that source lib/env.sh (#674).
# These satisfy env.sh's preconditions for edge-container scripts.
export PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-/opt/disinto}"
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-main}"
export OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/agent/repos/${PROJECT_NAME:-disinto}-ops}"
# Start dispatcher in background
bash /opt/disinto/docker/edge/dispatcher.sh &

View file

@ -84,6 +84,10 @@ export DISINTO_CONTAINER=1
export HOME="${HOME:-/home/agent}"
export USER="${USER:-agent}"
# Set project context vars for lib/env.sh surface contract (#674).
# PROJECT_NAME and PROJECT_REPO_ROOT are set below after TOML parsing.
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-main}"
# Configure git credential helper so reproduce/triage agents can clone/push
# without needing tokens embedded in remote URLs (#604).
if [ -f "${DISINTO_DIR}/lib/git-creds.sh" ]; then
@ -107,6 +111,8 @@ with open(sys.argv[1], 'rb') as f:
export PROJECT_NAME
PROJECT_REPO_ROOT="/home/agent/repos/${PROJECT_NAME}"
export PROJECT_REPO_ROOT
export OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/agent/repos/${PROJECT_NAME}-ops}"
if [ "$AGENT_TYPE" = "triage" ]; then
log "Starting triage-agent for issue #${ISSUE_NUMBER} (project: ${PROJECT_NAME})"

View file

@ -1,12 +1,41 @@
#!/usr/bin/env bash
# =============================================================================
# env.sh — Load environment and shared utilities
# Source this at the top of every script: source "$(dirname "$0")/lib/env.sh"
#
# SURFACE CONTRACT
#
# Required preconditions — the entrypoint (or caller) MUST set these before
# sourcing this file:
# USER — OS user name (e.g. "agent", "johba")
# HOME — home directory (e.g. "/home/agent")
#
# Required when PROJECT_TOML is set (i.e. agent scripts loading a project):
# PROJECT_REPO_ROOT — absolute path to the project git clone
# PRIMARY_BRANCH — default branch name (e.g. "main")
# OPS_REPO_ROOT — absolute path to the ops repo clone
# (these are normally populated by load-project.sh from the TOML)
#
# What this file sets / exports:
# FACTORY_ROOT, DISINTO_LOG_DIR
# .env / .env.enc secrets (FORGE_TOKEN, etc.)
# FORGE_API, FORGE_WEB, TEA_LOGIN, FORGE_OPS_REPO (derived from FORGE_URL/FORGE_REPO)
# Per-agent tokens (FORGE_REVIEW_TOKEN, FORGE_GARDENER_TOKEN, …)
# CLAUDE_SHARED_DIR, CLAUDE_CONFIG_DIR
# Helper functions: log(), validate_url(), forge_api(), forge_api_all(),
# woodpecker_api(), wpdb(), memory_guard()
# =============================================================================
set -euo pipefail
# Resolve script root (parent of lib/)
FACTORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# ── Precondition assertions ──────────────────────────────────────────────────
# These must be set by the entrypoint before sourcing this file.
: "${USER:?must be set by entrypoint before sourcing lib/env.sh}"
: "${HOME:?must be set by entrypoint before sourcing lib/env.sh}"
# Container detection: when running inside the agent container, DISINTO_CONTAINER
# is set by docker-compose.yml. Adjust paths so phase files, logs, and thread
# maps land on the persistent volume instead of /tmp (which is ephemeral).
@ -72,7 +101,6 @@ fi
# PATH: foundry, node, system
export PATH="${HOME}/.local/bin:${HOME}/.foundry/bin:${HOME}/.nvm/versions/node/v22.20.0/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
export HOME="${HOME:-/home/debian}"
# Load project TOML if PROJECT_TOML is set (by poll scripts that accept project arg)
if [ -n "${PROJECT_TOML:-}" ] && [ -f "$PROJECT_TOML" ]; then
@ -112,12 +140,14 @@ fi
export TEA_LOGIN
export PROJECT_NAME="${PROJECT_NAME:-${FORGE_REPO##*/}}"
export PROJECT_REPO_ROOT="${PROJECT_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}}"
export PRIMARY_BRANCH="${PRIMARY_BRANCH:-master}"
# Ops repo: operational data (vault items, journals, evidence, prerequisites).
# Default convention: sibling directory named {project}-ops.
export OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/${USER}/${PROJECT_NAME}-ops}"
# Project-specific paths: no guessing from USER/HOME — must be set by
# the entrypoint or loaded from PROJECT_TOML (via load-project.sh above).
if [ -n "${PROJECT_TOML:-}" ]; then
: "${PROJECT_REPO_ROOT:?must be set by entrypoint or PROJECT_TOML before sourcing lib/env.sh}"
: "${PRIMARY_BRANCH:?must be set by entrypoint or PROJECT_TOML before sourcing lib/env.sh}"
: "${OPS_REPO_ROOT:?must be set by entrypoint or PROJECT_TOML before sourcing lib/env.sh}"
fi
# Forge repo slug for the ops repo (used by agents that commit to ops).
export FORGE_OPS_REPO="${FORGE_OPS_REPO:-${FORGE_REPO:+${FORGE_REPO}-ops}}"

View file

@ -103,22 +103,11 @@ if [ -n "$FORGE_REPO" ]; then
export FORGE_REPO_OWNER="${FORGE_REPO%%/*}"
fi
# Derive PROJECT_REPO_ROOT if not explicitly set
if [ -z "${PROJECT_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then
export PROJECT_REPO_ROOT="/home/${USER}/${PROJECT_NAME}"
fi
# Derive OPS_REPO_ROOT if not explicitly set
if [ -z "${OPS_REPO_ROOT:-}" ] && [ -n "${PROJECT_NAME:-}" ]; then
export OPS_REPO_ROOT="/home/${USER}/${PROJECT_NAME}-ops"
fi
# Inside the container, always derive repo paths from PROJECT_NAME — the TOML
# carries host-perspective paths that do not exist in the container filesystem.
if [ "${DISINTO_CONTAINER:-}" = "1" ] && [ -n "${PROJECT_NAME:-}" ]; then
export PROJECT_REPO_ROOT="/home/agent/repos/${PROJECT_NAME}"
export OPS_REPO_ROOT="/home/agent/repos/${PROJECT_NAME}-ops"
fi
# PROJECT_REPO_ROOT and OPS_REPO_ROOT: no fallback derivation from USER/HOME.
# These must be set by the entrypoint (container) or the TOML (host CLI).
# Inside the container, the entrypoint exports the correct paths before agent
# scripts source env.sh; the TOML's host-perspective paths are skipped by the
# DISINTO_CONTAINER guard above.
# Derive FORGE_OPS_REPO if not explicitly set
if [ -z "${FORGE_OPS_REPO:-}" ] && [ -n "${FORGE_REPO:-}" ]; then