@rallycry/conveyor-agent 10.0.5 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -270,6 +270,34 @@ CONVEYOR_USER_ID="${CONVEYOR_USER_ID:-}"
270
270
  CONVEYOR_PROJECT_ID_FROM_BUNDLE="${CONVEYOR_PROJECT_ID:-${PROJECT_ID:-}}"
271
271
  export CONVEYOR_USER_ID
272
272
 
273
+ # ── Seed Claude Code's first-run gates ──
274
+ # Pre-answer every interactive gate the CLI shows on a fresh (or partially
275
+ # initialized) config: the onboarding wizard (theme picker), the
276
+ # bypass-permissions warning, and the folder-trust dialog for the repo
277
+ # workspace. Any one of these parks a headless TUI forever — seen live
278
+ # 2026-07-02 as code reviews stuck at "Is this a project you trust?" until the
279
+ # server's cap burned the attempt. conveyor-agent also seeds the same keys before
280
+ # each spawn (ensureClaudeOnboarding), but this shell seed is version-independent
281
+ # (pods can run older baked agents) and lands before the CLI's first read — and
282
+ # it also covers an interactive `claude` opened over an SSH tunnel, which the
283
+ # agent seed never runs for. Merge via jq, never clobber: CLI-owned keys (cache
284
+ # keys, firstStartTime, other projects' trust entries) survive; a missing or
285
+ # corrupt file becomes a fresh seed — Claude Code treats a 0-byte/unparseable
286
+ # file as fatal ("Unexpected end of JSON input"), so replacing is a repair. Theme
287
+ # only seeds fresh configs (a user-picked theme is never overridden). Every step
288
+ # is rescued: config seeding must never kill the entrypoint under `set -eo pipefail`.
289
+ seed_claude_json() {
290
+ SEED_TARGET="$1"
291
+ SEED_GATES='{"hasCompletedOnboarding":true,"bypassPermissionsModeAccepted":true,"projects":{"/workspaces/repo":{"hasTrustDialogAccepted":true}}}'
292
+ if [ -f "${SEED_TARGET}" ] && jq -e . "${SEED_TARGET}" >/dev/null 2>&1; then
293
+ if SEED_MERGED="$(jq -c --argjson gates "${SEED_GATES}" '. * $gates' "${SEED_TARGET}" 2>/dev/null)" && [ -n "${SEED_MERGED}" ]; then
294
+ printf '%s' "${SEED_MERGED}" > "${SEED_TARGET}" 2>/dev/null || true
295
+ fi
296
+ else
297
+ printf '%s' "${SEED_GATES}" | jq -c '. + {theme:"dark"}' > "${SEED_TARGET}" 2>/dev/null || true
298
+ fi
299
+ }
300
+
273
301
  # ── Wire per-user Claude state from the GCS FUSE mount ──
274
302
  # When the pod has /mnt/conveyor-users mounted (GCS FUSE CSI), symlink
275
303
  # ~/.claude, ~/.claude.json, and ~/.config/claude to the user's subdir so
@@ -282,29 +310,7 @@ if [ -n "${CONVEYOR_USER_ID}" ] && [ -n "${USER_HOME_PROJECT_ID}" ] && [ -d "${U
282
310
  USER_HOME_ROOT="${USER_HOME_MOUNT}/users/${CONVEYOR_USER_ID}/${USER_HOME_PROJECT_ID}"
283
311
  echo "[pool] Linking Claude state to ${USER_HOME_ROOT}"
284
312
  mkdir -p "${USER_HOME_ROOT}/.claude" "${USER_HOME_ROOT}/.config/claude" 2>/dev/null || true
285
- # Seed .claude.json on first use. Claude Code treats a 0-byte file as
286
- # corrupted ("Unexpected end of JSON input") and exits code 1, killing the
287
- # very first query on any fresh user+project mount. We pre-complete onboarding
288
- # and pick a default theme so the spawned Claude Code TUI doesn't stop at the
289
- # first-run theme picker / login. Only seed when the file is missing/empty or a
290
- # bare "{}" placeholder — never overwrite a real config carried over via the
291
- # GCS-FUSE user-home mount from a prior pod session.
292
- CLAUDE_JSON="${USER_HOME_ROOT}/.claude.json"
293
- # Read defensively: on a first-time user+project mount the file does not
294
- # exist yet. `< "${CLAUDE_JSON}"` on a missing file is a SHELL-level
295
- # redirection error (the `2>/dev/null` only silences tr, not bash), which
296
- # under `set -eo pipefail` aborts the whole entrypoint with exit 1 — the
297
- # agent container then dies seconds after start and the pod is reaped
298
- # ("deleted externally"). Guard on existence and rescue any read error so a
299
- # missing/unreadable file falls through to the seed below instead of crashing.
300
- if [ -f "${CLAUDE_JSON}" ]; then
301
- CLAUDE_JSON_CURRENT="$(tr -d '[:space:]' < "${CLAUDE_JSON}" 2>/dev/null || true)"
302
- else
303
- CLAUDE_JSON_CURRENT=""
304
- fi
305
- if [ -z "${CLAUDE_JSON_CURRENT}" ] || [ "${CLAUDE_JSON_CURRENT}" = "{}" ]; then
306
- echo '{"hasCompletedOnboarding":true,"theme":"dark"}' > "${CLAUDE_JSON}" 2>/dev/null || true
307
- fi
313
+ seed_claude_json "${USER_HOME_ROOT}/.claude.json"
308
314
 
309
315
  # Replace any image defaults with live symlinks into the mount.
310
316
  rm -rf /home/conveyor/.claude /home/conveyor/.claude.json /home/conveyor/.config/claude 2>/dev/null || true
@@ -314,6 +320,9 @@ if [ -n "${CONVEYOR_USER_ID}" ] && [ -n "${USER_HOME_PROJECT_ID}" ] && [ -d "${U
314
320
  ln -sfn "${USER_HOME_ROOT}/.config/claude" /home/conveyor/.config/claude
315
321
  else
316
322
  echo "[pool] Skipping user-home symlink (userId='${CONVEYOR_USER_ID}', projectId='${USER_HOME_PROJECT_ID}', mount present: $([ -d "${USER_HOME_MOUNT}" ] && echo yes || echo no))"
323
+ # No persistent home — the CLI reads the pod-local config; seed it there so
324
+ # non-FUSE pods get the same first-run gate suppression as the mounted path.
325
+ seed_claude_json /home/conveyor/.claude.json
317
326
  fi
318
327
 
319
328
  # ═══════════════════════════════════════════════════════════════════════════
@@ -343,6 +352,24 @@ fi
343
352
  GIT_READY_MARKER="/workspaces/.conveyor-git-ready"
344
353
  GIT_FAILED_MARKER="/workspaces/.conveyor-git-failed"
345
354
 
355
+ reset_tracked_repo_changes_before_assignment_checkout() {
356
+ # The baked/pooled repo is not user-owned until this git gate succeeds. Reset
357
+ # stale tracked image-generated dirt so assignment checkout can move to the
358
+ # requested branch/ref instead of being blocked by files like dependency
359
+ # stamps. Do not `git clean`: untracked prebake artifacts may be intentional.
360
+ # The assignment checkouts additionally pass -f: an UNTRACKED bake artifact
361
+ # can collide with a path the TARGET ref tracks (e.g. a dependency stamp
362
+ # committed on an older PR branch), which this reset cannot clear — checkout
363
+ # then refuses with "untracked working tree files would be overwritten".
364
+ # Forcing is safe here for the same not-user-owned reason.
365
+ if ! _reset_out=$(git -C repo reset --hard HEAD 2>&1); then
366
+ echo "[pool] ERROR: failed to clean tracked repo changes before checkout: ${_reset_out}" >&2
367
+ printf '%s' "pre-checkout reset failed" > "$GIT_FAILED_MARKER"
368
+ return 1
369
+ fi
370
+ return 0
371
+ }
372
+
346
373
  # Shared by both the pod-image branch and the repo-present-non-pod-image branch
347
374
  # of prepare_workspace_git: refresh the remote token, fetch the base branch,
348
375
  # fetch/checkout the task branch (creating it from base if it doesn't exist on
@@ -365,6 +392,10 @@ sync_task_branch_to_repo() {
365
392
  echo "[pool] WARN: fetch origin/${DEV_BRANCH} failed: ${_fetch_dev_out}" >&2
366
393
  fi
367
394
 
395
+ if ! reset_tracked_repo_changes_before_assignment_checkout; then
396
+ return 1
397
+ fi
398
+
368
399
  if [ -n "${CHECKOUT_REF}" ]; then
369
400
  echo "[pool] Fetching checkout ref ${CHECKOUT_REF} for review branch ${BRANCH}..."
370
401
  if ! _fetch_checkout_out=$(git -C repo fetch origin "+${CHECKOUT_REF}:refs/remotes/origin/pr-checkout" 2>&1); then
@@ -372,7 +403,7 @@ sync_task_branch_to_repo() {
372
403
  printf '%s' "fetch checkout ref ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
373
404
  return 1
374
405
  fi
375
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
406
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
376
407
  echo "[pool] ERROR: Repo checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
377
408
  printf '%s' "checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
378
409
  return 1
@@ -387,7 +418,7 @@ sync_task_branch_to_repo() {
387
418
  if echo "${_fetch_out}" | grep -q "couldn't find remote ref"; then
388
419
  echo "[pool] Branch missing on origin — creating from base '${DEV_BRANCH}'"
389
420
  # Create the branch locally from the base branch
390
- if ! _create_out=$(git -C repo checkout -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
421
+ if ! _create_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
391
422
  echo "[pool] ERROR: Failed to create branch '${BRANCH}' from '${DEV_BRANCH}': ${_create_out}" >&2
392
423
  printf '%s' "create branch ${BRANCH} from ${DEV_BRANCH} failed" > "$GIT_FAILED_MARKER"
393
424
  return 1
@@ -409,7 +440,7 @@ sync_task_branch_to_repo() {
409
440
  # Use `checkout -B` to create or reset the local branch tracking origin.
410
441
  # This leaves HEAD attached to a real branch (not detached) so `git status`,
411
442
  # `git push`, and any tool that keys off branch state work correctly.
412
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
443
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
413
444
  echo "[pool] ERROR: Repo checkout of ${BRANCH} failed: ${_checkout_out}" >&2
414
445
  printf '%s' "checkout of ${BRANCH} failed" > "$GIT_FAILED_MARKER"
415
446
  return 1
@@ -482,7 +513,7 @@ prepare_workspace_git() {
482
513
  printf '%s' "post-assignment fetch of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
483
514
  return 1
484
515
  fi
485
- if ! _checkout_out=$(git -C repo checkout -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
516
+ if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
486
517
  echo "[pool] ERROR: post-assignment checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
487
518
  printf '%s' "post-assignment checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
488
519
  return 1