diff --git a/AGENTS.md b/AGENTS.md
index 5f63564..939109c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -46,12 +46,12 @@ disinto/
```bash
# ShellCheck all scripts
-shellcheck dev/dev-poll.sh dev/dev-agent.sh dev/phase-test.sh \
+shellcheck dev/dev-poll.sh dev/dev-agent.sh dev/phase-handler.sh dev/phase-test.sh \
review/review-poll.sh review/review-pr.sh \
gardener/gardener-poll.sh gardener/gardener-agent.sh \
supervisor/supervisor-poll.sh supervisor/update-prompt.sh \
- lib/env.sh lib/ci-debug.sh lib/ci-helpers.sh lib/load-project.sh \
- lib/parse-deps.sh lib/matrix_listener.sh lib/agent-session.sh
+ lib/env.sh lib/agent-session.sh lib/ci-debug.sh lib/ci-helpers.sh lib/load-project.sh \
+ lib/parse-deps.sh lib/matrix_listener.sh
# Run phase protocol test
bash dev/phase-test.sh
diff --git a/dev/dev-agent.sh b/dev/dev-agent.sh
index 930b624..13ad612 100755
--- a/dev/dev-agent.sh
+++ b/dev/dev-agent.sh
@@ -23,6 +23,8 @@ set -euo pipefail
# Load shared environment
source "$(dirname "$0")/../lib/env.sh"
source "$(dirname "$0")/../lib/agent-session.sh"
+# shellcheck source=./phase-handler.sh
+source "$(dirname "$0")/phase-handler.sh"
# Auto-pull factory code to pick up merged fixes before any logic runs
git -C "$FACTORY_ROOT" pull --ff-only origin main 2>/dev/null || true
@@ -53,48 +55,26 @@ THREAD_FILE="/tmp/dev-thread-${PROJECT_NAME}-${ISSUE}"
# Timing
export PHASE_POLL_INTERVAL=30 # seconds between phase checks (read by agent-session.sh)
IDLE_TIMEOUT=7200 # 2h: kill session if phase stale this long
+# shellcheck disable=SC2034 # used by phase-handler.sh
CI_POLL_TIMEOUT=1800 # 30min max for CI to complete
+# shellcheck disable=SC2034 # used by phase-handler.sh
REVIEW_POLL_TIMEOUT=10800 # 3h max wait for review
# Limits
+# shellcheck disable=SC2034 # used by phase-handler.sh
MAX_CI_FIXES=3
+# shellcheck disable=SC2034 # used by phase-handler.sh
MAX_REVIEW_ROUNDS=5
-# Counters — global state across phase transitions
+# Counters — global state shared with phase-handler.sh across phase transitions
+# shellcheck disable=SC2034
CI_RETRY_COUNT=0
+# shellcheck disable=SC2034
CI_FIX_COUNT=0
+# shellcheck disable=SC2034
REVIEW_ROUND=0
PR_NUMBER=""
-# --- Refusal comment helper (used in PHASE:failed handler) ---
-post_refusal_comment() {
- local emoji="$1" title="$2" body="$3"
- local last_has_title
- last_has_title=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/issues/${ISSUE}/comments?limit=5" | \
- jq -r --arg t "Dev-agent: ${title}" '[.[] | .body // ""] | any(contains($t)) | tostring') || true
- if [ "$last_has_title" = "true" ]; then
- log "skipping duplicate refusal comment: ${title}"
- return 0
- fi
- local comment
- comment="${emoji} **Dev-agent: ${title}**
-
-${body}
-
----
-*Automated assessment by dev-agent · $(date -u '+%Y-%m-%d %H:%M UTC')*"
- printf '%s' "$comment" > "/tmp/refusal-comment.txt"
- jq -Rs '{body: .}' < "/tmp/refusal-comment.txt" > "/tmp/refusal-comment.json"
- curl -sf -o /dev/null -X POST \
- -H "Authorization: token ${CODEBERG_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API}/issues/${ISSUE}/comments" \
- --data-binary @"/tmp/refusal-comment.json" 2>/dev/null || \
- log "WARNING: failed to post refusal comment"
- rm -f "/tmp/refusal-comment.txt" "/tmp/refusal-comment.json"
-}
-
# --- Cleanup helpers ---
cleanup_worktree() {
cd "$REPO_ROOT"
@@ -131,137 +111,6 @@ cleanup() {
}
trap cleanup EXIT
-# =============================================================================
-# MERGE HELPER
-# =============================================================================
-do_merge() {
- local sha="$1"
- local pr="${PR_NUMBER}"
-
- for _m in $(seq 1 20); do
- local ci
- ci=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/commits/${sha}/status" | jq -r '.state // "unknown"')
- [ "$ci" = "success" ] && break
- if [ "$ci" = "failure" ] || [ "$ci" = "error" ]; then
- log "CI is red before merge attempt — aborting"
- notify "PR #${pr} CI is failing; cannot merge."
- return 1
- fi
- sleep 30
- done
-
- # Pre-emptive rebase to avoid merge conflicts
- local mergeable
- mergeable=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/pulls/${pr}" | jq -r '.mergeable // true')
- if [ "$mergeable" = "false" ]; then
- log "PR #${pr} has merge conflicts — attempting rebase"
- local work_dir="${WORKTREE:-$REPO_ROOT}"
- if (cd "$work_dir" && git fetch origin "${PRIMARY_BRANCH}" && git rebase "origin/${PRIMARY_BRANCH}" 2>&1); then
- log "rebase succeeded — force pushing"
- (cd "$work_dir" && git push origin "${BRANCH}" --force-with-lease 2>&1) || true
- sha=$(cd "$work_dir" && git rev-parse HEAD)
- log "waiting for CI on rebased commit ${sha:0:7}"
- local r_ci
- for _r in $(seq 1 20); do
- r_ci=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/commits/${sha}/status" | jq -r '.state // "unknown"')
- [ "$r_ci" = "success" ] && break
- if [ "$r_ci" = "failure" ] || [ "$r_ci" = "error" ]; then
- log "CI failed after rebase"
- notify "PR #${pr} CI failed after rebase. Needs manual fix."
- return 1
- fi
- sleep 30
- done
- else
- log "rebase failed — aborting and escalating"
- (cd "$work_dir" && git rebase --abort 2>/dev/null) || true
- notify "PR #${pr} has merge conflicts that need manual resolution."
- return 1
- fi
- fi
-
- local http_code
- http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
- -H "Authorization: token ${CODEBERG_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API}/pulls/${pr}/merge" \
- -d '{"Do":"merge","delete_branch_after_merge":true}')
-
- if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
- log "PR #${pr} merged!"
- curl -sf -X DELETE \
- -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/branches/${BRANCH}" >/dev/null 2>&1 || true
- curl -sf -X PATCH \
- -H "Authorization: token ${CODEBERG_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API}/issues/${ISSUE}" \
- -d '{"state":"closed"}' >/dev/null 2>&1 || true
- cleanup_labels
- notify_ctx \
- "✅ PR #${pr} merged! Issue #${ISSUE} done." \
- "✅ PR #${pr} merged! Issue #${ISSUE} done."
- kill_tmux_session
- cleanup_worktree
- rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
- exit 0
- else
- log "merge failed (HTTP ${http_code}) — attempting rebase and retry"
- local work_dir="${WORKTREE:-$REPO_ROOT}"
- if (cd "$work_dir" && git fetch origin "${PRIMARY_BRANCH}" && git rebase "origin/${PRIMARY_BRANCH}" 2>&1); then
- log "rebase succeeded — force pushing"
- (cd "$work_dir" && git push origin "${BRANCH}" --force-with-lease 2>&1) || true
- sha=$(cd "$work_dir" && git rev-parse HEAD)
- log "waiting for CI on rebased commit ${sha:0:7}"
- local r2_ci
- for _r2 in $(seq 1 20); do
- r2_ci=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/commits/${sha}/status" | jq -r '.state // "unknown"')
- [ "$r2_ci" = "success" ] && break
- if [ "$r2_ci" = "failure" ] || [ "$r2_ci" = "error" ]; then
- log "CI failed after merge-retry rebase"
- notify "PR #${pr} CI failed after rebase. Needs manual fix."
- return 1
- fi
- sleep 30
- done
- # Re-approve (force push dismisses stale approvals)
- curl -sf -X POST \
- -H "Authorization: token ${REVIEW_BOT_TOKEN:-${CODEBERG_TOKEN}}" \
- -H "Content-Type: application/json" \
- "${API}/pulls/${pr}/reviews" \
- -d '{"event":"APPROVED","body":"Auto-approved after rebase."}' >/dev/null 2>&1 || true
- # Retry merge
- http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
- -H "Authorization: token ${CODEBERG_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API}/pulls/${pr}/merge" \
- -d '{"Do":"merge","delete_branch_after_merge":true}')
- if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then
- log "PR #${pr} merged after rebase!"
- notify_ctx \
- "✅ PR #${pr} merged! Issue #${ISSUE} done." \
- "✅ PR #${pr} merged! Issue #${ISSUE} done."
- curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API}/issues/${ISSUE}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
- cleanup_labels
- kill_tmux_session
- cleanup_worktree
- rm -f "$PHASE_FILE" "$IMPL_SUMMARY_FILE" "$THREAD_FILE"
- exit 0
- fi
- else
- (cd "$work_dir" && git rebase --abort 2>/dev/null) || true
- fi
- log "merge still failing after rebase (HTTP ${http_code})"
- notify "PR #${pr} merge failed after rebase (HTTP ${http_code}). Needs human attention."
- return 1
- fi
-}
# =============================================================================
# LOG ROTATION
@@ -331,24 +180,8 @@ fi
# =============================================================================
status "preflight check"
-# Extract dependency references from issue body
-# Only from ## Dependencies / ## Depends on / ## Blocked by sections
-# and inline "depends on #NNN" / "blocked by #NNN" phrases.
-# NEVER extract from ## Related or other sections.
-DEP_NUMBERS=""
-
-# 1. Inline phrases anywhere in body (explicit dep language only)
-INLINE_DEPS=$(echo "$ISSUE_BODY" | \
- grep -ioP '(?:depends on|blocked by)\s+#\K[0-9]+' | \
- sort -un || true)
-[ -n "$INLINE_DEPS" ] && DEP_NUMBERS="$INLINE_DEPS"
-
-# 2. ## Dependencies / ## Depends on / ## Blocked by section (bullet items)
-DEP_SECTION=$(echo "$ISSUE_BODY" | sed -n '/^##\?\s*\(Dependencies\|Depends on\|Blocked by\)/I,/^##/p' | sed '1d;$d')
-if [ -n "$DEP_SECTION" ]; then
- SECTION_DEPS=$(echo "$DEP_SECTION" | grep -oP '#\K[0-9]+' | sort -un || true)
- DEP_NUMBERS=$(printf '%s\n%s' "$DEP_NUMBERS" "$SECTION_DEPS" | sort -un | grep -v '^$' || true)
-fi
+# Extract dependency references using shared parser
+DEP_NUMBERS=$(echo "$ISSUE_BODY" | bash "${FACTORY_ROOT}/lib/parse-deps.sh")
BLOCKED_BY=()
if [ -n "$DEP_NUMBERS" ]; then
@@ -815,526 +648,6 @@ if [ ! -f "${THREAD_FILE}" ] || [ -z "$(cat "$THREAD_FILE" 2>/dev/null)" ]; then
fi
notify "tmux session ${SESSION_NAME} started for issue #${ISSUE}: ${ISSUE_TITLE}"
-# =============================================================================
-# PHASE MONITORING LOOP
-# =============================================================================
-
-# _on_phase_change — Phase dispatch callback for monitor_phase_loop
-# Receives the current phase as $1.
-# Returns 0 to continue the loop, 1 to break (terminal phase reached).
-_on_phase_change() {
- local phase="$1"
-
- # ── PHASE: awaiting_ci ──────────────────────────────────────────────────────
- if [ "$phase" = "PHASE:awaiting_ci" ]; then
-
- # Create PR if not yet created
- if [ -z "${PR_NUMBER:-}" ]; then
- status "creating PR for issue #${ISSUE}"
- IMPL_SUMMARY=""
- if [ -f "$IMPL_SUMMARY_FILE" ]; then
- # Don't treat refusal JSON as a PR summary
- if ! jq -e '.status' < "$IMPL_SUMMARY_FILE" >/dev/null 2>&1; then
- IMPL_SUMMARY=$(head -c 4000 "$IMPL_SUMMARY_FILE")
- fi
- fi
-
- printf 'Fixes #%s\n\n## Changes\n%s' "$ISSUE" "$IMPL_SUMMARY" > "/tmp/pr-body-${ISSUE}.txt"
- jq -n \
- --arg title "fix: ${ISSUE_TITLE} (#${ISSUE})" \
- --rawfile body "/tmp/pr-body-${ISSUE}.txt" \
- --arg head "$BRANCH" \
- --arg base "${PRIMARY_BRANCH}" \
- '{title: $title, body: $body, head: $head, base: $base}' > "/tmp/pr-request-${ISSUE}.json"
-
- PR_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
- -H "Authorization: token ${CODEBERG_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API}/pulls" \
- --data-binary @"/tmp/pr-request-${ISSUE}.json")
-
- PR_HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
- PR_RESPONSE_BODY=$(echo "$PR_RESPONSE" | sed '$d')
- rm -f "/tmp/pr-body-${ISSUE}.txt" "/tmp/pr-request-${ISSUE}.json"
-
- if [ "$PR_HTTP_CODE" = "201" ] || [ "$PR_HTTP_CODE" = "200" ]; then
- PR_NUMBER=$(echo "$PR_RESPONSE_BODY" | jq -r '.number')
- log "created PR #${PR_NUMBER}"
- PR_URL="${CODEBERG_WEB}/pulls/${PR_NUMBER}"
- notify_ctx \
- "PR #${PR_NUMBER} created: ${ISSUE_TITLE}" \
- "PR #${PR_NUMBER} created: ${ISSUE_TITLE}"
- elif [ "$PR_HTTP_CODE" = "409" ]; then
- # PR already exists (race condition) — find it
- FOUND_PR=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/pulls?state=open&limit=20" | \
- jq -r --arg branch "$BRANCH" \
- '.[] | select(.head.ref == $branch) | .number' | head -1) || true
- if [ -n "$FOUND_PR" ]; then
- PR_NUMBER="$FOUND_PR"
- log "PR already exists: #${PR_NUMBER}"
- else
- log "ERROR: PR creation got 409 but no existing PR found"
- inject_into_session "ERROR: Could not create PR (HTTP 409, no existing PR found). Check the Codeberg API. Retry by writing PHASE:awaiting_ci again after verifying the branch was pushed."
- return 0
- fi
- else
- log "ERROR: PR creation failed (HTTP ${PR_HTTP_CODE})"
- notify "failed to create PR (HTTP ${PR_HTTP_CODE})"
- inject_into_session "ERROR: Could not create PR (HTTP ${PR_HTTP_CODE}). Check branch was pushed: git push origin ${BRANCH}. Then write PHASE:awaiting_ci again."
- return 0
- fi
- fi
-
- # No CI configured? Treat as success immediately
- if [ "${WOODPECKER_REPO_ID:-2}" = "0" ]; then
- log "no CI configured — treating as passed"
- inject_into_session "CI passed on PR #${PR_NUMBER} (no CI configured for this project).
-Write PHASE:awaiting_review to the phase file, then stop and wait for review feedback."
- return 0
- fi
-
- # Poll CI until done or timeout
- status "waiting for CI on PR #${PR_NUMBER}"
- CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || \
- curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/pulls/${PR_NUMBER}" | jq -r '.head.sha')
-
- CI_DONE=false
- CI_STATE="unknown"
- CI_POLL_ELAPSED=0
- while [ "$CI_POLL_ELAPSED" -lt "$CI_POLL_TIMEOUT" ]; do
- sleep 30
- CI_POLL_ELAPSED=$(( CI_POLL_ELAPSED + 30 ))
-
- # Check session still alive during CI wait
- if ! tmux has-session -t "${SESSION_NAME}" 2>/dev/null; then
- log "session died during CI wait"
- break
- fi
-
- CI_STATE=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/commits/${CI_CURRENT_SHA}/status" | jq -r '.state // "unknown"')
- if [ "$CI_STATE" = "success" ] || [ "$CI_STATE" = "failure" ] || [ "$CI_STATE" = "error" ]; then
- CI_DONE=true
- [ "$CI_STATE" = "success" ] && CI_FIX_COUNT=0
- break
- fi
- done
-
- if ! $CI_DONE; then
- log "TIMEOUT: CI didn't complete in ${CI_POLL_TIMEOUT}s"
- notify "CI timeout on PR #${PR_NUMBER}"
- inject_into_session "CI TIMEOUT: CI did not complete within 30 minutes for PR #${PR_NUMBER} (SHA: ${CI_CURRENT_SHA:0:7}). This may be an infrastructure issue. Write PHASE:needs_human if you cannot proceed."
- return 0
- fi
-
- log "CI: ${CI_STATE}"
-
- if [ "$CI_STATE" = "success" ]; then
- inject_into_session "CI passed on PR #${PR_NUMBER}.
-Write PHASE:awaiting_review to the phase file, then stop and wait for review feedback:
- echo \"PHASE:awaiting_review\" > \"${PHASE_FILE}\""
- else
- # Fetch CI error details
- PIPELINE_NUM=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
- "${API}/commits/${CI_CURRENT_SHA}/status" | \
- jq -r '.statuses[0].target_url // ""' | grep -oP 'pipeline/\K[0-9]+' | head -1 || true)
-
- FAILED_STEP=""
- FAILED_EXIT=""
- IS_INFRA=false
- if [ -n "$PIPELINE_NUM" ]; then
- FAILED_INFO=$(curl -sf \
- -H "Authorization: Bearer ${WOODPECKER_TOKEN}" \
- "${WOODPECKER_SERVER}/api/repos/${WOODPECKER_REPO_ID}/pipelines/${PIPELINE_NUM}" | \
- jq -r '.workflows[]?.children[]? | select(.state=="failure") | "\(.name)|\(.exit_code)"' | head -1 || true)
- FAILED_STEP=$(echo "$FAILED_INFO" | cut -d'|' -f1)
- FAILED_EXIT=$(echo "$FAILED_INFO" | cut -d'|' -f2)
- fi
-
- log "CI failed: step=${FAILED_STEP:-unknown} exit=${FAILED_EXIT:-?}"
-
- case "${FAILED_STEP}" in git*) IS_INFRA=true ;; esac
- case "${FAILED_EXIT}" in 128|137) IS_INFRA=true ;; esac
-
- if [ "$IS_INFRA" = true ] && [ "${CI_RETRY_COUNT:-0}" -lt 1 ]; then
- CI_RETRY_COUNT=$(( CI_RETRY_COUNT + 1 ))
- log "infra failure — retrigger CI (retry ${CI_RETRY_COUNT})"
- (cd "$WORKTREE" && git commit --allow-empty \
- -m "ci: retrigger after infra failure (#${ISSUE})" --no-verify 2>&1 | tail -1)
- (cd "$WORKTREE" && git push origin "$BRANCH" --force 2>&1 | tail -3)
- # Touch phase file so we recheck CI on the new SHA
- # Do NOT update LAST_PHASE_MTIME here — let the main loop detect the fresh mtime
- touch "$PHASE_FILE"
- CI_CURRENT_SHA=$(git -C "${WORKTREE}" rev-parse HEAD 2>/dev/null || true)
- return 0
- fi
-
- CI_FIX_COUNT=$(( CI_FIX_COUNT + 1 ))
- _ci_pipeline_url="${WOODPECKER_SERVER}/repos/${WOODPECKER_REPO_ID}/pipeline/${PIPELINE_NUM:-0}"
- if [ "$CI_FIX_COUNT" -gt "$MAX_CI_FIXES" ]; then
- log "CI failure not recoverable after ${CI_FIX_COUNT} fix attempts — escalating"
- echo "{\"issue\":${ISSUE},\"pr\":${PR_NUMBER},\"reason\":\"ci_exhausted\",\"step\":\"${FAILED_STEP:-unknown}\",\"attempts\":${CI_FIX_COUNT},\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
- >> "${FACTORY_ROOT}/supervisor/escalations-${PROJECT_NAME}.jsonl"
- notify_ctx \
- "CI exhausted after ${CI_FIX_COUNT} attempts — escalated to supervisor" \
- "CI exhausted after ${CI_FIX_COUNT} attempts on PR #${PR_NUMBER} | Pipeline
Step: ${FAILED_STEP:-unknown} — escalated to supervisor"
- printf 'PHASE:failed\nReason: ci_exhausted after %d attempts\n' "$CI_FIX_COUNT" > "$PHASE_FILE"
- # Do NOT update LAST_PHASE_MTIME here — let the main loop detect PHASE:failed
- return 0
- fi
-
- CI_ERROR_LOG=""
- if [ -n "$PIPELINE_NUM" ]; then
- CI_ERROR_LOG=$(bash "${FACTORY_ROOT}/lib/ci-debug.sh" failures "$PIPELINE_NUM" 2>/dev/null | tail -80 | head -c 8000 || echo "")
- fi
-
- # Save CI result for crash recovery
- printf 'CI failed (attempt %d/%d)\nStep: %s\nExit: %s\n\n%s' \
- "$CI_FIX_COUNT" "$MAX_CI_FIXES" "${FAILED_STEP:-unknown}" "${FAILED_EXIT:-?}" "$CI_ERROR_LOG" \
- > "/tmp/ci-result-${PROJECT_NAME}-${ISSUE}.txt" 2>/dev/null || true
-
- # Notify Matrix with rich CI failure context
- _ci_snippet=$(printf '%s' "${CI_ERROR_LOG:-}" | tail -5 | head -c 500 | sed 's/&/\&/g; s/\</g; s/>/\>/g')
- notify_ctx \
- "CI failed on PR #${PR_NUMBER}: step=${FAILED_STEP:-unknown} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES})" \
- "CI failed on PR #${PR_NUMBER} | Pipeline #${PIPELINE_NUM:-?}
Step: ${FAILED_STEP:-unknown} (exit ${FAILED_EXIT:-?})
Attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}
${_ci_snippet:-no logs}"
-
- inject_into_session "CI failed on PR #${PR_NUMBER} (attempt ${CI_FIX_COUNT}/${MAX_CI_FIXES}).
-
-Failed step: ${FAILED_STEP:-unknown} (exit code ${FAILED_EXIT:-?}, pipeline #${PIPELINE_NUM:-?})
-
-CI debug tool:
- bash ${FACTORY_ROOT}/lib/ci-debug.sh failures ${PIPELINE_NUM:-0}
- bash ${FACTORY_ROOT}/lib/ci-debug.sh logs ${PIPELINE_NUM:-0}