init.sh now archives and resets an existing task/<uuid> branch to the current base when AGENT_BRANCH_RESET_ON_START=true. Default off: all nodes of a workflow share the branch, so only the workflow's first node may set it. Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
240 lines
12 KiB
Bash
Executable File
240 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
# Agent repo init script — clones reference repos and working repo
|
|
# AR-12 through AR-15, AR-32
|
|
set -euo pipefail
|
|
|
|
echo "=== agent-repo/v1 init.sh ==="
|
|
|
|
# Ensure workspace directories exist
|
|
mkdir -p /workspace/reference
|
|
mkdir -p /workspace/project
|
|
mkdir -p /workspace/.agent-output
|
|
|
|
# Clone reference branches (AR-12, AR-13)
|
|
if [ -n "${REFERENCE_BRANCHES:-}" ] && [ "${REFERENCE_BRANCHES:-}" != "[]" ]; then
|
|
echo "Cloning reference branches..."
|
|
echo "$REFERENCE_BRANCHES" | python3 -u -c "
|
|
import json, sys, subprocess, os
|
|
refs = json.load(sys.stdin)
|
|
print(f'reference_branches loop: {len(refs)} entries')
|
|
for ref in refs:
|
|
name = ref.get('name', '')
|
|
repo_url = ref.get('repo_url', '')
|
|
branch = ref.get('branch', 'main')
|
|
dest = f'/workspace/reference/{name}'
|
|
print(f'Cloning {repo_url} ({branch}) -> {dest}')
|
|
# Full clone (no --depth) so AR-14a-seeded agent branches have visible
|
|
# ancestry when pushed back to the agent-repo. Gitea rejects shallow
|
|
# pushes with shallow-update-not-allowed. Real incident: 2026-05-08
|
|
# probe 7.
|
|
result = subprocess.run(
|
|
['git', 'clone', '--branch', branch,
|
|
'-c', 'core.symlinks=false', repo_url, dest],
|
|
capture_output=True, text=True
|
|
)
|
|
print(f'clone returncode={result.returncode}')
|
|
if result.stdout:
|
|
print(f'clone stdout (last 1KB): {result.stdout[-1000:]}')
|
|
if result.stderr:
|
|
print(f'clone stderr (last 1KB): {result.stderr[-1000:]}')
|
|
if result.returncode != 0:
|
|
print(f'ERROR: Failed to clone {repo_url}', file=sys.stderr)
|
|
sys.exit(1)
|
|
# Strip any symlinks (security: prevent /proc/1/environ exfiltration)
|
|
subprocess.run(['find', dest, '-type', 'l', '-exec', 'rm', '{}', ';'])
|
|
# Make reference read-only
|
|
chmod_r = subprocess.run(['chmod', '-R', 'a-w', dest], capture_output=True, text=True)
|
|
if chmod_r.returncode != 0:
|
|
print(f'WARNING: chmod returned {chmod_r.returncode}: {chmod_r.stderr[:300]}')
|
|
if not os.path.isdir(os.path.join(dest, '.git')):
|
|
print(f'ERROR: clone exit 0 but {dest}/.git missing', file=sys.stderr)
|
|
sys.exit(1)
|
|
print(f'Cloned {name} successfully')
|
|
"
|
|
fi
|
|
|
|
# Clone agent repo working branch (AR-14).
|
|
#
|
|
# AR-14a (2026-05-08): seed fresh task branches from /workspace/reference/main/
|
|
# rather than the agent-repo fork's main, so a stale fork (e.g. periodic
|
|
# "Fork cleanup" PRs that reset main) doesn't poison every fresh task with
|
|
# old project state. Existing AGENT_BRANCH cherry-picks remain unchanged
|
|
# (continuing prior work). Pre-existing operator workaround in
|
|
# memory/gotchas-airouter.md item 27.
|
|
if [ -n "${AGENT_REPO_URL:-}" ] && [ -n "${AGENT_BRANCH:-}" ]; then
|
|
echo "Cloning agent working repo: $AGENT_REPO_URL (branch: $AGENT_BRANCH)"
|
|
if git clone --depth 1 --branch "$AGENT_BRANCH" "$AGENT_REPO_URL" /workspace/project 2>/dev/null; then
|
|
echo "Cloned existing branch $AGENT_BRANCH (continuing prior work)"
|
|
# AR-42: archive the stale task branch and reset it to the current base.
|
|
# Opt-in per node (AGENT_BRANCH_RESET_ON_START=true, set by the workflow
|
|
# template on its FIRST node only): every node of a workflow shares this
|
|
# one branch, so an unconditional reset would wipe earlier nodes' commits.
|
|
if [ "${AGENT_BRANCH_RESET_ON_START:-}" = "true" ]; then
|
|
mkdir -p /workspace/.agent-output
|
|
AR42_TOOL=/opt/agent/agent_repo_base.py
|
|
AR42_FAIL() {
|
|
echo "ERROR: AR-42 $1" >&2
|
|
printf '{"error": "ar42_archive_and_reset_failed", "detail": %s}\n' \
|
|
"$(printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()[:500]))')" \
|
|
> /workspace/.agent-output/init-error.json
|
|
exit 1
|
|
}
|
|
AR42_SEL=$(python3 "$AR42_TOOL" select-base \
|
|
--workdir /workspace/project \
|
|
--branch "$AGENT_BRANCH" \
|
|
--upstream-url "${AGENT_UPSTREAM_REPO_URL:-}" \
|
|
--upstream-branch "${AGENT_UPSTREAM_BRANCH:-main}" \
|
|
--reference-dir /workspace/reference/main \
|
|
--out /workspace/.agent-output/branch_base.json 2>&1) \
|
|
|| AR42_FAIL "select-base failed: $AR42_SEL"
|
|
AR42_SHA=$(printf '%s\n' "$AR42_SEL" | sed -n 's/^sha=//p' | head -1)
|
|
[ -n "$AR42_SHA" ] || AR42_FAIL "select-base returned no sha: $AR42_SEL"
|
|
AR42_OUT=$(python3 "$AR42_TOOL" archive-and-reset \
|
|
--workdir /workspace/project \
|
|
--branch "$AGENT_BRANCH" \
|
|
--base-sha "$AR42_SHA" \
|
|
--fork-url "$AGENT_REPO_URL" \
|
|
--archive-prefix archive \
|
|
--base-metadata /workspace/.agent-output/branch_base.json \
|
|
--out /workspace/.agent-output/branch_base.json 2>&1) \
|
|
|| AR42_FAIL "archive-and-reset failed: $AR42_OUT"
|
|
echo "AR-42: $AGENT_BRANCH reset to base $AR42_SHA"
|
|
fi
|
|
else
|
|
echo "Branch $AGENT_BRANCH does not exist — seeding fresh branch from upstream reference"
|
|
# Full clone (not --depth 1) so we get a working remote for finalize.sh push.
|
|
if git clone "$AGENT_REPO_URL" /workspace/project; then
|
|
cd /workspace/project
|
|
# AR-39: resolve the seed point via entrypoint.agent_repo_base
|
|
# (upstream > reference > fork). Writes branch_base.json for
|
|
# finalize.sh to merge into ci_metadata.json (AR-25). Falls back
|
|
# to the legacy inline AR-14a logic if the CLI is unavailable.
|
|
mkdir -p /workspace/.agent-output
|
|
AR39_OUT=$(python3 /opt/agent/agent_repo_base.py select-base \
|
|
--workdir /workspace/project \
|
|
--branch "$AGENT_BRANCH" \
|
|
--upstream-url "${AGENT_UPSTREAM_REPO_URL:-}" \
|
|
--upstream-branch "${AGENT_UPSTREAM_BRANCH:-main}" \
|
|
--reference-dir /workspace/reference/main \
|
|
--out /workspace/.agent-output/branch_base.json \
|
|
--fork-url "$AGENT_REPO_URL" 2>&1) && AR39_RC=0 || AR39_RC=$?
|
|
if [ "$AR39_RC" = "0" ]; then
|
|
AR39_SHA=$(printf '%s\n' "$AR39_OUT" | sed -n 's/^sha=//p' | head -1)
|
|
AR39_SRC=$(printf '%s\n' "$AR39_OUT" | sed -n 's/^source=//p' | head -1)
|
|
git checkout -b "$AGENT_BRANCH" "$AR39_SHA"
|
|
echo "Seeded $AGENT_BRANCH from $AR39_SRC @ $AR39_SHA (AR-39)"
|
|
else
|
|
echo "WARNING: AR-39 select-base failed (rc=$AR39_RC): $AR39_OUT — falling back to inline AR-14a seeding" >&2
|
|
fi
|
|
REF_REPO=/workspace/reference/main
|
|
if [ "$AR39_RC" != "0" ] && [ -d "$REF_REPO/.git" ]; then
|
|
# Seed working tree from the upstream reference clone — AR-14a.
|
|
# Reference is read-only (chmod a-w), but git can still read it
|
|
# as a local-path remote for fetch + reset.
|
|
#
|
|
# Use a temporary remote name so we don't collide with 'origin'.
|
|
git remote add upstream-ref "$REF_REPO"
|
|
# Full fetch (no --depth) — the agent's branch will be pushed
|
|
# back to the agent-repo, and gitea rejects shallow pushes
|
|
# with "shallow update not allowed". Even though the
|
|
# reference clone itself may be shallow, fetch as much as
|
|
# the source has so the agent's HEAD has visible ancestry.
|
|
# Real incident: 2026-05-08 probe 7 (shallow update reject).
|
|
git fetch upstream-ref 2>&1 | head -3 || {
|
|
echo "WARNING: failed to fetch from upstream reference; falling back to fork main" >&2
|
|
git remote remove upstream-ref 2>/dev/null
|
|
git checkout -b "$AGENT_BRANCH"
|
|
}
|
|
if git rev-parse upstream-ref/HEAD >/dev/null 2>&1; then
|
|
UPSTREAM_REF="upstream-ref/HEAD"
|
|
elif git rev-parse upstream-ref/main >/dev/null 2>&1; then
|
|
UPSTREAM_REF="upstream-ref/main"
|
|
else
|
|
UPSTREAM_REF=""
|
|
fi
|
|
if [ -n "$UPSTREAM_REF" ]; then
|
|
git checkout -b "$AGENT_BRANCH" "$UPSTREAM_REF"
|
|
git remote remove upstream-ref
|
|
echo "Seeded $AGENT_BRANCH from $REF_REPO ($(git log --oneline -1)) — AR-14a"
|
|
else
|
|
git remote remove upstream-ref 2>/dev/null
|
|
git checkout -b "$AGENT_BRANCH"
|
|
echo "WARNING: upstream-ref had no resolvable HEAD; using fork main (may be stale)" >&2
|
|
fi
|
|
elif [ "$AR39_RC" != "0" ]; then
|
|
# No reference clone available — fall back to fork main.
|
|
git checkout -b "$AGENT_BRANCH"
|
|
echo "WARNING: $REF_REPO/.git not found; using fork main (may be stale)" >&2
|
|
fi
|
|
else
|
|
echo "ERROR: Failed to clone agent repo $AGENT_REPO_URL" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Clean up results/ from any previous run (AR-15)
|
|
if [ -d /workspace/project/results ]; then
|
|
rm -rf /workspace/project/results
|
|
fi
|
|
mkdir -p /workspace/project/results
|
|
|
|
# Ensure scratchpad/ exists at the top level (cross-node artifact passing).
|
|
# The git branch is this project's shared data channel between manifest
|
|
# nodes — each node runs in its own ephemeral container/pod with no shared
|
|
# filesystem, so an agent node that needs to hand data to a downstream
|
|
# kind: script node (or another agent node) does it by writing here and
|
|
# committing/pushing, for the next node to clone the same branch and read
|
|
# back. NEVER merged into main — the no-scratchpad-in-main CI gate
|
|
# (agent-runtimes .gitea/workflows/build-images.yaml) rejects any commit to
|
|
# main that still contains this directory.
|
|
mkdir -p /workspace/project/scratchpad
|
|
if [ ! -f /workspace/project/scratchpad/README.md ]; then
|
|
cat > /workspace/project/scratchpad/README.md << 'SCRATCHPAD_EOF'
|
|
# scratchpad/
|
|
|
|
Ephemeral, per-task working data — NOT deliverable content.
|
|
|
|
- Use this directory to pass data between nodes in the same workflow
|
|
manifest (e.g. an agent node writes a verdict/artifact here for a
|
|
downstream `kind: script` node to read back off the SAME branch — the
|
|
git branch is the only thing nodes in a manifest share, since each runs
|
|
in its own ephemeral container).
|
|
- Anything under `scratchpad/` is scratch, not review/deliverable content.
|
|
**Do not** reference or copy `scratchpad/` content into the actual
|
|
deliverable during integration/merge.
|
|
- A CI gate rejects any commit to `main` that still contains a
|
|
`scratchpad/` directory — it must never survive merge.
|
|
SCRATCHPAD_EOF
|
|
fi
|
|
|
|
# Populate .gitignore (AR-32)
|
|
GITIGNORE=/workspace/project/.gitignore
|
|
cat >> "$GITIGNORE" << 'GITIGNORE_EOF'
|
|
# Agent-repo auto-generated gitignore entries
|
|
*.key
|
|
*.pem
|
|
*.p12
|
|
*.pfx
|
|
.env
|
|
*.env
|
|
*.secret
|
|
GITIGNORE_EOF
|
|
|
|
# Handle retry: clone previous attempt branch as read-only reference (AR-16)
|
|
if [ -n "${AGENT_PREVIOUS_BRANCH:-}" ] && [ "${AGENT_RETRY_COUNT:-0}" -gt 0 ]; then
|
|
echo "Cloning previous attempt branch: $AGENT_PREVIOUS_BRANCH"
|
|
git clone --depth 1 --branch "$AGENT_PREVIOUS_BRANCH" \
|
|
-c core.symlinks=false \
|
|
"$AGENT_REPO_URL" /workspace/reference/previous-attempt 2>/dev/null || \
|
|
echo "Warning: Could not clone previous attempt branch (non-fatal)"
|
|
if [ -d /workspace/reference/previous-attempt ]; then
|
|
find /workspace/reference/previous-attempt -type l -exec rm {} \;
|
|
chmod -R a-w /workspace/reference/previous-attempt
|
|
fi
|
|
fi
|
|
|
|
export AGENT_WORKING_DIR="/workspace/project"
|
|
echo "AGENT_WORKING_DIR=$AGENT_WORKING_DIR"
|
|
echo "=== agent-repo/v1 init.sh complete ==="
|