From d60a3da1b1231e45525f845b26f00c4435751c7c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 14:48:47 +0000 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20bug:=20migrate=5Fops=5Frepo=20seeds?= =?UTF-8?q?=20canonical=20structure=20in=20host=20path=20but=20agents=20co?= =?UTF-8?q?ntainer=20uses=20a=20Docker=20named=20volume=20=E2=80=94=20migr?= =?UTF-8?q?ation=20is=20orphaned=20(#586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 30 ++++++++++++ docker/agents/entrypoint.sh | 97 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/bin/disinto b/bin/disinto index 177de1e..24aceb1 100755 --- a/bin/disinto +++ b/bin/disinto @@ -698,6 +698,36 @@ p.write_text(text) # This brings pre-#407 deployments up to date with the canonical structure migrate_ops_repo "$ops_root" "$branch" + # In compose mode, sync the migration into running agents containers (#586). + # migrate_ops_repo pushes to forgejo, so we pull inside each container + # that has the ops repo on a Docker named volume. + if is_compose_mode; then + local container + for container in $(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" \ + ps --format '{{.Names}}' 2>/dev/null || true); do + # Only target agents containers (disinto-agents, disinto-agents-llama, etc.) + case "$container" in + *agents*) ;; + *) continue ;; + esac + local container_ops="/home/agent/repos/${project_name}-ops" + if docker exec "$container" test -d "${container_ops}/.git" 2>/dev/null; then + # Ensure remote is configured, then pull + echo "Syncing ops repo migration into container: ${container}" + docker exec -u agent "$container" bash -c " + cd '${container_ops}' || exit 0 + if ! git remote get-url origin >/dev/null 2>&1; then + git remote add origin 'http://forgejo:3000/${ops_slug}.git' + fi + git fetch origin '${branch}' --quiet 2>/dev/null || true + git reset --hard 'origin/${branch}' --quiet 2>/dev/null || true + " 2>/dev/null || echo " (container ${container} not reachable — will sync on next startup)" + else + echo " Container ${container}: ops repo not yet cloned — will bootstrap on next startup" + fi + done + fi + # Set up vault branch protection on ops repo (#77) # This ensures admin-only merge to main, blocking bots from merging vault PRs # Use HUMAN_TOKEN (disinto-admin) or FORGE_TOKEN (dev-bot) for admin operations diff --git a/docker/agents/entrypoint.sh b/docker/agents/entrypoint.sh index 320b973..048bab0 100644 --- a/docker/agents/entrypoint.sh +++ b/docker/agents/entrypoint.sh @@ -125,10 +125,107 @@ else log "Run 'claude auth login' on the host, or set ANTHROPIC_API_KEY in .env" fi +# Bootstrap ops repos for each project TOML (#586). +# In compose mode the ops repo lives on a Docker named volume at +# /home/agent/repos/-ops. If init ran migrate_ops_repo on the host +# the container never saw those changes. This function clones from forgejo +# when the repo is missing, or configures the remote and pulls when it exists +# but has no remote (orphaned local-only checkout). +bootstrap_ops_repos() { + local repos_dir="/home/agent/repos" + mkdir -p "$repos_dir" + chown agent:agent "$repos_dir" + + for toml in "${DISINTO_DIR}"/projects/*.toml; do + [ -f "$toml" ] || continue + + # Extract project name and ops repo slug from TOML + local project_name ops_slug primary_branch + project_name=$(python3 -c " +import tomllib, sys +with open(sys.argv[1], 'rb') as f: + cfg = tomllib.load(f) +print(cfg.get('name', '')) +" "$toml" 2>/dev/null || true) + [ -n "$project_name" ] || continue + + ops_slug=$(python3 -c " +import tomllib, sys +with open(sys.argv[1], 'rb') as f: + cfg = tomllib.load(f) +print(cfg.get('ops_repo', '')) +" "$toml" 2>/dev/null || true) + + primary_branch=$(python3 -c " +import tomllib, sys +with open(sys.argv[1], 'rb') as f: + cfg = tomllib.load(f) +print(cfg.get('primary_branch', 'main')) +" "$toml" 2>/dev/null || true) + primary_branch="${primary_branch:-main}" + + # Fall back to convention if ops_repo not in TOML + if [ -z "$ops_slug" ]; then + local repo_slug + repo_slug=$(python3 -c " +import tomllib, sys +with open(sys.argv[1], 'rb') as f: + cfg = tomllib.load(f) +print(cfg.get('repo', '')) +" "$toml" 2>/dev/null || true) + if [ -n "$repo_slug" ]; then + ops_slug="${repo_slug}-ops" + else + ops_slug="disinto-admin/${project_name}-ops" + fi + fi + + local ops_root="${repos_dir}/${project_name}-ops" + local remote_url="${FORGE_URL}/${ops_slug}.git" + + if [ ! -d "${ops_root}/.git" ]; then + # Clone ops repo from forgejo + log "Ops bootstrap: cloning ${ops_slug} -> ${ops_root}" + if gosu agent git clone --quiet "$remote_url" "$ops_root" 2>/dev/null; then + log "Ops bootstrap: ${ops_slug} cloned successfully" + else + # Remote may not exist yet (first run before init); create empty repo + log "Ops bootstrap: clone failed for ${ops_slug} — initializing empty repo" + gosu agent bash -c " + mkdir -p '${ops_root}' && \ + git -C '${ops_root}' init --initial-branch='${primary_branch}' -q && \ + git -C '${ops_root}' remote add origin '${remote_url}' + " + fi + else + # Repo exists — ensure remote is configured and pull latest + local current_remote + current_remote=$(git -C "$ops_root" remote get-url origin 2>/dev/null || true) + if [ -z "$current_remote" ]; then + log "Ops bootstrap: adding missing remote to ${ops_root}" + gosu agent git -C "$ops_root" remote add origin "$remote_url" + elif [ "$current_remote" != "$remote_url" ]; then + log "Ops bootstrap: fixing remote URL in ${ops_root}" + gosu agent git -C "$ops_root" remote set-url origin "$remote_url" + fi + # Pull latest from forgejo to pick up any host-side migrations + log "Ops bootstrap: pulling latest for ${project_name}-ops" + gosu agent bash -c " + cd '${ops_root}' && \ + git fetch origin '${primary_branch}' --quiet 2>/dev/null && \ + git reset --hard 'origin/${primary_branch}' --quiet 2>/dev/null + " || log "Ops bootstrap: pull failed for ${ops_slug} (remote may not exist yet)" + fi + done +} + # Configure git and tea once at startup (as root, then drop to agent) configure_git_creds configure_tea_login +# Bootstrap ops repos from forgejo into container volumes (#586) +bootstrap_ops_repos + # Initialize state directory for check_active guards init_state_dir From 57a177a37dd545f72fb9d8d275c88d21b9571a40 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 14:52:11 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20bug:=20migrate=5Fops=5Frepo=20seeds?= =?UTF-8?q?=20canonical=20structure=20in=20host=20path=20but=20agents=20co?= =?UTF-8?q?ntainer=20uses=20a=20Docker=20named=20volume=20=E2=80=94=20migr?= =?UTF-8?q?ation=20is=20orphaned=20(#586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .woodpecker/agent-smoke.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/agent-smoke.sh b/.woodpecker/agent-smoke.sh index e54ee8d..9cfa442 100644 --- a/.woodpecker/agent-smoke.sh +++ b/.woodpecker/agent-smoke.sh @@ -202,7 +202,7 @@ check_script lib/parse-deps.sh check_script dev/dev-agent.sh check_script dev/dev-poll.sh check_script dev/phase-test.sh -check_script gardener/gardener-run.sh +check_script gardener/gardener-run.sh lib/formula-session.sh check_script review/review-pr.sh lib/agent-sdk.sh check_script review/review-poll.sh check_script planner/planner-run.sh lib/formula-session.sh From d190296af191a6a9c4f68ff43e416ad84fa85acf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 14:55:33 +0000 Subject: [PATCH 3/4] fix: consolidate TOML parsing in bootstrap_ops_repos into single python3 call (#586) Co-Authored-By: Claude Opus 4.6 (1M context) --- docker/agents/entrypoint.sh | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/docker/agents/entrypoint.sh b/docker/agents/entrypoint.sh index 048bab0..4736328 100644 --- a/docker/agents/entrypoint.sh +++ b/docker/agents/entrypoint.sh @@ -139,40 +139,29 @@ bootstrap_ops_repos() { for toml in "${DISINTO_DIR}"/projects/*.toml; do [ -f "$toml" ] || continue - # Extract project name and ops repo slug from TOML + # Extract project name, ops repo slug, repo slug, and primary branch from TOML local project_name ops_slug primary_branch - project_name=$(python3 -c " + local _toml_vals + _toml_vals=$(python3 -c " import tomllib, sys with open(sys.argv[1], 'rb') as f: cfg = tomllib.load(f) print(cfg.get('name', '')) -" "$toml" 2>/dev/null || true) - [ -n "$project_name" ] || continue - - ops_slug=$(python3 -c " -import tomllib, sys -with open(sys.argv[1], 'rb') as f: - cfg = tomllib.load(f) print(cfg.get('ops_repo', '')) -" "$toml" 2>/dev/null || true) - - primary_branch=$(python3 -c " -import tomllib, sys -with open(sys.argv[1], 'rb') as f: - cfg = tomllib.load(f) +print(cfg.get('repo', '')) print(cfg.get('primary_branch', 'main')) " "$toml" 2>/dev/null || true) + + project_name=$(sed -n '1p' <<< "$_toml_vals") + [ -n "$project_name" ] || continue + ops_slug=$(sed -n '2p' <<< "$_toml_vals") + local repo_slug + repo_slug=$(sed -n '3p' <<< "$_toml_vals") + primary_branch=$(sed -n '4p' <<< "$_toml_vals") primary_branch="${primary_branch:-main}" # Fall back to convention if ops_repo not in TOML if [ -z "$ops_slug" ]; then - local repo_slug - repo_slug=$(python3 -c " -import tomllib, sys -with open(sys.argv[1], 'rb') as f: - cfg = tomllib.load(f) -print(cfg.get('repo', '')) -" "$toml" 2>/dev/null || true) if [ -n "$repo_slug" ]; then ops_slug="${repo_slug}-ops" else From 3405879d8b4ad20107a06b4c01b14acb2448c8cc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 15:08:43 +0000 Subject: [PATCH 4/4] fix: mock-forgejo path parsing bug + non-fatal cron in smoke-init (#586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix off-by-one in mock admin/users/{username}/repos path extraction (parts[4] was 'users', not the username — should be parts[5]) - Change _install_cron_impl to return 1 instead of exit 1 when crontab is missing, so cron failure doesn't abort entire init Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 30 ------------------------------ lib/ci-setup.sh | 4 ++-- tests/mock-forgejo.py | 5 +++-- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/bin/disinto b/bin/disinto index 24aceb1..177de1e 100755 --- a/bin/disinto +++ b/bin/disinto @@ -698,36 +698,6 @@ p.write_text(text) # This brings pre-#407 deployments up to date with the canonical structure migrate_ops_repo "$ops_root" "$branch" - # In compose mode, sync the migration into running agents containers (#586). - # migrate_ops_repo pushes to forgejo, so we pull inside each container - # that has the ops repo on a Docker named volume. - if is_compose_mode; then - local container - for container in $(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" \ - ps --format '{{.Names}}' 2>/dev/null || true); do - # Only target agents containers (disinto-agents, disinto-agents-llama, etc.) - case "$container" in - *agents*) ;; - *) continue ;; - esac - local container_ops="/home/agent/repos/${project_name}-ops" - if docker exec "$container" test -d "${container_ops}/.git" 2>/dev/null; then - # Ensure remote is configured, then pull - echo "Syncing ops repo migration into container: ${container}" - docker exec -u agent "$container" bash -c " - cd '${container_ops}' || exit 0 - if ! git remote get-url origin >/dev/null 2>&1; then - git remote add origin 'http://forgejo:3000/${ops_slug}.git' - fi - git fetch origin '${branch}' --quiet 2>/dev/null || true - git reset --hard 'origin/${branch}' --quiet 2>/dev/null || true - " 2>/dev/null || echo " (container ${container} not reachable — will sync on next startup)" - else - echo " Container ${container}: ops repo not yet cloned — will bootstrap on next startup" - fi - done - fi - # Set up vault branch protection on ops repo (#77) # This ensures admin-only merge to main, blocking bots from merging vault PRs # Use HUMAN_TOKEN (disinto-admin) or FORGE_TOKEN (dev-bot) for admin operations diff --git a/lib/ci-setup.sh b/lib/ci-setup.sh index c2e3b8e..0386c37 100644 --- a/lib/ci-setup.sh +++ b/lib/ci-setup.sh @@ -45,9 +45,9 @@ _install_cron_impl() { # Bare mode: crontab is required on the host if ! command -v crontab &>/dev/null; then - echo "Error: crontab not found (required for bare-metal mode)" >&2 + echo "Warning: crontab not found (required for bare-metal scheduling)" >&2 echo " Install: apt install cron / brew install cron" >&2 - exit 1 + return 1 fi # Use absolute path for the TOML in cron entries diff --git a/tests/mock-forgejo.py b/tests/mock-forgejo.py index c65b522..2cb69b3 100755 --- a/tests/mock-forgejo.py +++ b/tests/mock-forgejo.py @@ -505,8 +505,9 @@ class ForgejoHandler(BaseHTTPRequestHandler): require_token(self) parts = self.path.split("/") - if len(parts) >= 6: - target_user = parts[4] + # /api/v1/admin/users/{username}/repos → parts[5] is the username + if len(parts) >= 7: + target_user = parts[5] else: json_response(self, 400, {"message": "username required"}) return