forgedock 1.1.2 → 1.1.4

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.
@@ -0,0 +1,485 @@
1
+ ---
2
+ description: Ship a branch to its deploy target — detect or create PR, fix CI, review, merge. Returns structured result for callers.
3
+ argument-hint: [source_branch | "staging" | "milestone/{slug}"] [--target TARGET] [--issue ISSUE_NUMBER] [--repo REPO] [--max-ci-iterations N] [--max-review-iterations N] [--dry-run]
4
+ allowed-tools: Bash, Read, Grep, Glob, Skill
5
+ ---
6
+ <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
7
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
8
+
9
+ # /deploy-pr — PR Ship Orchestrator
10
+
11
+ **Input**: $ARGUMENTS
12
+
13
+ **NEVER use plan mode (EnterPlanMode)** — it breaks execution context.
14
+ **NEVER use the Agent tool** — deploy-pr dispatches sub-skills via `Skill(...)` only. The Agent tool bypasses the allowed-tools constraint and produces opaque output that cannot be structured into the deploy result.
15
+
16
+ **Agent model policy**: `model: "sonnet"` (standard tier). Fallback: `model: "opus"` if rate-limited. User can override with `--model <name>`.
17
+
18
+ <!-- FORGE:SPEC_LOADED — deploy-pr.md loaded and active. Agent is bound by this spec. -->
19
+
20
+ ---
21
+
22
+ ## What This Command Does
23
+
24
+ `/deploy-pr` is a **sub-skill** designed to be invoked by callers like `/autopilot` that need to ship a branch without consuming their own context window. It handles the full PR shipping lifecycle:
25
+
26
+ 1. Detect or create the PR for the source→target pair
27
+ 2. Run the CI gate — fix failures via `/fix-ci` (loop up to max iterations)
28
+ 3. Run the review gate — via `/review-pr` (fix + re-review loop up to max iterations)
29
+ 4. Merge the PR after both gates pass
30
+ 5. Return a structured result to the caller
31
+
32
+ **Never force-merges.** If CI or review fail after maximum iterations, the command stops and reports the blocking state without merging.
33
+
34
+ ---
35
+
36
+ ## Forbidden Tools Self-Check
37
+
38
+ **Before executing any phase**, verify you are NOT using any of these tools:
39
+
40
+ | Tool | Status | Reason |
41
+ |------|--------|--------|
42
+ | `Agent` | **FORBIDDEN** | Bypasses allowed-tools; produces opaque output that cannot feed the structured result |
43
+ | `EnterPlanMode` | **FORBIDDEN** | Breaks execution context; phases must be executed, not planned |
44
+
45
+ ---
46
+
47
+ ## Phase 0: Config Resolution & Argument Parsing
48
+
49
+ ### 0A: Read forge.yaml
50
+
51
+ Resolve all config variables before any logic runs. This prevents unresolved `{PLACEHOLDER}` values in bash blocks — a known failure mode on deploy-info.md (issues #318, #1392).
52
+
53
+ ```bash
54
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
55
+
56
+ GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE" 2>/dev/null)
57
+ GH_FLAG="-R $GH_REPO"
58
+ STAGING_BRANCH=$(yq '.branches.staging // "staging"' "$CONFIG_FILE" 2>/dev/null || echo "staging")
59
+ DEFAULT_BRANCH=$(yq '.branches.default // "main"' "$CONFIG_FILE" 2>/dev/null || echo "main")
60
+ REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE" 2>/dev/null || echo "$(git rev-parse --show-toplevel)")
61
+ ```
62
+
63
+ ### 0B: Parse Arguments
64
+
65
+ ```
66
+ $ARGUMENTS format examples:
67
+ staging
68
+ milestone/my-feature
69
+ feat/my-branch --target staging
70
+ staging --issue 1234 --repo owner/repo --max-ci-iterations 2 --max-review-iterations 3
71
+ staging --dry-run
72
+ ```
73
+
74
+ Parse:
75
+ - `SOURCE` — first positional arg, or `staging` if omitted
76
+ - `--target TARGET` — explicit PR target override (skips routing table)
77
+ - `--issue ISSUE_NUMBER` — parent issue to reference in PR body (optional)
78
+ - `--repo REPO` — override GH_REPO (optional — forge.yaml takes precedence if absent)
79
+ - `--max-ci-iterations N` — max CI fix attempts, default 3
80
+ - `--max-review-iterations N` — max review + fix cycles, default 3
81
+ - `--dry-run` — run all phases but do NOT merge; print what would happen
82
+
83
+ ```bash
84
+ SOURCE="${1:-staging}"
85
+ TARGET="" # resolved in Phase 1 if not passed via --target
86
+ ISSUE_NUMBER=""
87
+ MAX_CI_ITER=3
88
+ MAX_REVIEW_ITER=3
89
+ DRY_RUN=false
90
+
91
+ # Parse optional flags
92
+ shift 2>/dev/null || true
93
+ while [[ $# -gt 0 ]]; do
94
+ case "$1" in
95
+ --target) TARGET="$2"; shift 2 ;;
96
+ --issue) ISSUE_NUMBER="$2"; shift 2 ;;
97
+ --repo) GH_REPO="$2"; GH_FLAG="-R $GH_REPO"; shift 2 ;;
98
+ --max-ci-iterations) MAX_CI_ITER="$2"; shift 2 ;;
99
+ --max-review-iterations) MAX_REVIEW_ITER="$2"; shift 2 ;;
100
+ --dry-run) DRY_RUN=true; shift ;;
101
+ *) shift ;;
102
+ esac
103
+ done
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Phase 1: Branch Routing
109
+
110
+ Determine the PR target from the source branch. If `--target` was passed explicitly, skip the routing table and use it directly.
111
+
112
+ ### Routing Table
113
+
114
+ | Source Branch Pattern | Target | PR Title Prefix |
115
+ |----------------------|--------|-----------------|
116
+ | `staging` | `main` (DEFAULT_BRANCH) | `Deploy` |
117
+ | `milestone/*` | `staging` (STAGING_BRANCH) | `Ship` |
118
+ | Any other branch | `staging` (STAGING_BRANCH) | `Merge` |
119
+
120
+ ```bash
121
+ if [ -n "$TARGET" ]; then
122
+ # Explicit override
123
+ PR_TARGET="$TARGET"
124
+ case "$SOURCE" in
125
+ staging) TITLE_PREFIX="Deploy" ;;
126
+ milestone/*) TITLE_PREFIX="Ship" ;;
127
+ *) TITLE_PREFIX="Merge" ;;
128
+ esac
129
+ else
130
+ case "$SOURCE" in
131
+ staging)
132
+ PR_TARGET="$DEFAULT_BRANCH"
133
+ TITLE_PREFIX="Deploy"
134
+ ;;
135
+ milestone/*)
136
+ PR_TARGET="$STAGING_BRANCH"
137
+ TITLE_PREFIX="Ship"
138
+ ;;
139
+ *)
140
+ PR_TARGET="$STAGING_BRANCH"
141
+ TITLE_PREFIX="Merge"
142
+ ;;
143
+ esac
144
+ fi
145
+
146
+ echo "Source: $SOURCE → Target: $PR_TARGET (prefix: $TITLE_PREFIX)"
147
+ ```
148
+
149
+ **Routing guard — staging→main**: When `SOURCE=staging` and `PR_TARGET=main`, the review gate (Phase 4) MUST route to `Skill("review-pr-staging", ...)` rather than the standard `review-pr`. This ensures the staging deploy review (comprehensive, multi-agent) runs instead of the single-PR review.
150
+
151
+ **Routing guard — milestone→staging**: When `SOURCE` matches `milestone/*`, this is a feature lane ship. `/review-pr` handles these normally (they target staging, not main).
152
+
153
+ ---
154
+
155
+ ## Phase 2: PR Detection / Creation
156
+
157
+ ### 2A: Detect Existing PR
158
+
159
+ Always check for an open PR before creating one. Never close-and-recreate — always update in place. <!-- Ref: issue #1328 -->
160
+
161
+ ```bash
162
+ cd "$REPO_PATH"
163
+ git fetch origin "$SOURCE" "$PR_TARGET" 2>/dev/null || true
164
+
165
+ EXISTING_PR=$(gh pr list $GH_FLAG \
166
+ --head "$SOURCE" \
167
+ --base "$PR_TARGET" \
168
+ --state open \
169
+ --json number,url,title,headRefOid \
170
+ --jq '.[0] // empty' 2>/dev/null)
171
+
172
+ if [ -n "$EXISTING_PR" ]; then
173
+ PR_NUMBER=$(echo "$EXISTING_PR" | jq -r '.number')
174
+ PR_URL=$(echo "$EXISTING_PR" | jq -r '.url')
175
+ echo "Found existing open PR #${PR_NUMBER}: ${PR_URL}"
176
+ PR_CREATED=false
177
+ else
178
+ echo "No existing PR found — creating new PR"
179
+ PR_NUMBER=""
180
+ PR_CREATED=true
181
+ fi
182
+ ```
183
+
184
+ ### 2B: Gather Commit Summary (for new PR body)
185
+
186
+ Skip if PR already exists.
187
+
188
+ ```bash
189
+ if [ "$PR_CREATED" = "true" ]; then
190
+ # Commits on source that are not yet on target
191
+ COMMIT_LOG=$(git log --oneline "origin/${PR_TARGET}..origin/${SOURCE}" --format="%h %s" 2>/dev/null | head -30)
192
+ COMMIT_COUNT=$(git rev-list --count "origin/${PR_TARGET}..origin/${SOURCE}" 2>/dev/null || echo "0")
193
+
194
+ if [ "$COMMIT_COUNT" -eq 0 ]; then
195
+ echo "DEPLOY_RESULT: status=NOTHING_TO_DEPLOY source=$SOURCE target=$PR_TARGET"
196
+ echo "{ \"pr\": null, \"source\": \"$SOURCE\", \"target\": \"$PR_TARGET\", \"status\": \"nothing_to_deploy\", \"ci_fixes\": 0, \"review_findings\": 0 }"
197
+ exit 0
198
+ fi
199
+
200
+ # Extract issue references from commit messages
201
+ ISSUE_REFS=$(echo "$COMMIT_LOG" | grep -oE '#[0-9]+' | sort -u | head -20 | tr '\n' ' ')
202
+
203
+ # Build issue ref block
204
+ if [ -n "$ISSUE_NUMBER" ]; then
205
+ CLOSES_LINE="Closes #${ISSUE_NUMBER}"
206
+ else
207
+ CLOSES_LINE="<!-- No parent issue specified -->"
208
+ fi
209
+
210
+ PR_BODY="## Summary
211
+
212
+ ${TITLE_PREFIX}: \`${SOURCE}\` → \`${PR_TARGET}\`
213
+
214
+ **Commits**: ${COMMIT_COUNT}
215
+
216
+ ## Changes
217
+
218
+ \`\`\`
219
+ ${COMMIT_LOG}
220
+ \`\`\`
221
+
222
+ ## Issue References
223
+
224
+ ${ISSUE_REFS:-None}
225
+
226
+ ---
227
+ ${CLOSES_LINE}
228
+ **Source**: \`${SOURCE}\`
229
+ **Target**: \`${PR_TARGET}\`"
230
+ fi
231
+ ```
232
+
233
+ ### 2C: Create PR (if needed)
234
+
235
+ ```bash
236
+ if [ "$PR_CREATED" = "true" ] && [ "$DRY_RUN" = "false" ]; then
237
+ # Determine PR title
238
+ PR_TITLE="${TITLE_PREFIX}: ${SOURCE} → ${PR_TARGET} (${COMMIT_COUNT} commits)"
239
+
240
+ # Route to correct review spec — staging→main PRs use review-pr-staging conventions
241
+ if [ "$SOURCE" = "$STAGING_BRANCH" ] && [ "$PR_TARGET" = "$DEFAULT_BRANCH" ]; then
242
+ PR_TITLE="${TITLE_PREFIX}: Deploy staging → main (${COMMIT_COUNT} commits)"
243
+ fi
244
+
245
+ PR_URL=$(gh pr create $GH_FLAG \
246
+ --head "$SOURCE" \
247
+ --base "$PR_TARGET" \
248
+ --title "$PR_TITLE" \
249
+ --body "$PR_BODY" 2>&1)
250
+
251
+ if echo "$PR_URL" | grep -qE '^https://'; then
252
+ PR_NUMBER=$(echo "$PR_URL" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+' | tail -1)
253
+ echo "Created PR #${PR_NUMBER}: ${PR_URL}"
254
+ else
255
+ echo "ERROR: PR creation failed:"
256
+ echo "$PR_URL"
257
+ echo "{ \"pr\": null, \"source\": \"$SOURCE\", \"target\": \"$PR_TARGET\", \"status\": \"pr_creation_failed\", \"ci_fixes\": 0, \"review_findings\": 0 }"
258
+ exit 1
259
+ fi
260
+
261
+ elif [ "$DRY_RUN" = "true" ] && [ "$PR_CREATED" = "true" ]; then
262
+ echo "[DRY-RUN] Would create PR: ${TITLE_PREFIX}: ${SOURCE} → ${PR_TARGET} (${COMMIT_COUNT} commits)"
263
+ echo "[DRY-RUN] PR body would include ${COMMIT_COUNT} commits and issue refs: ${ISSUE_REFS}"
264
+ PR_NUMBER="DRY_RUN"
265
+ fi
266
+ ```
267
+
268
+ **State after Phase 2**: `PR_NUMBER` is set (either detected or created).
269
+
270
+ ---
271
+
272
+ ## Phase 3: CI Gate
273
+
274
+ Invoke `/fix-ci` to handle CI failures. Loop until green or max iterations reached.
275
+
276
+ **Graceful degradation**: If `/fix-ci` is not available (not yet installed — issue #1675 is its companion), skip this phase with a warning. The review gate in Phase 4 will still run, and a failing CI will block the merge at the review stage.
277
+
278
+ ```bash
279
+ CI_FIXES=0
280
+ CI_GATE_PASSED=false
281
+ CI_ITER=0
282
+
283
+ # Check if fix-ci is available
284
+ FIX_CI_AVAILABLE=$(ls ~/.claude/commands/fix-ci.md 2>/dev/null && echo "true" || echo "false")
285
+
286
+ if [ "$FIX_CI_AVAILABLE" = "false" ]; then
287
+ echo "WARNING: /fix-ci not available — skipping CI gate. Install fix-ci (issue #1675) for automated CI fixing."
288
+ echo "CI gate: SKIPPED (fix-ci unavailable)"
289
+ CI_GATE_PASSED=true # Allow pipeline to continue; review gate will catch CI failures
290
+ else
291
+ # Check current CI status
292
+ CURRENT_CI_STATUS=$(gh pr checks "$PR_NUMBER" $GH_FLAG --json name,status,conclusion \
293
+ --jq '[.[] | select(.conclusion == "failure")] | length' 2>/dev/null || echo "0")
294
+
295
+ if [ "$CURRENT_CI_STATUS" -eq 0 ]; then
296
+ echo "CI gate: PASSED (no failing checks)"
297
+ CI_GATE_PASSED=true
298
+ else
299
+ echo "CI has $CURRENT_CI_STATUS failing checks — invoking /fix-ci (max $MAX_CI_ITER iterations)"
300
+
301
+ while [ "$CI_ITER" -lt "$MAX_CI_ITER" ] && [ "$CI_GATE_PASSED" = "false" ]; do
302
+ CI_ITER=$((CI_ITER + 1))
303
+ echo "=== CI Gate Iteration $CI_ITER / $MAX_CI_ITER ==="
304
+
305
+ if [ "$DRY_RUN" = "false" ]; then
306
+ Skill("fix-ci", args="$PR_NUMBER $GH_FLAG")
307
+ else
308
+ echo "[DRY-RUN] Would invoke: Skill(fix-ci, $PR_NUMBER $GH_FLAG)"
309
+ fi
310
+
311
+ CI_FIXES=$((CI_FIXES + 1))
312
+
313
+ # Re-check CI status after fix attempt
314
+ FAILING_AFTER=$(gh pr checks "$PR_NUMBER" $GH_FLAG --json name,status,conclusion \
315
+ --jq '[.[] | select(.conclusion == "failure")] | length' 2>/dev/null || echo "0")
316
+
317
+ if [ "$FAILING_AFTER" -eq 0 ]; then
318
+ echo "CI gate: PASSED after $CI_ITER iteration(s)"
319
+ CI_GATE_PASSED=true
320
+ else
321
+ echo "CI still has $FAILING_AFTER failing checks after iteration $CI_ITER"
322
+ fi
323
+ done
324
+
325
+ if [ "$CI_GATE_PASSED" = "false" ]; then
326
+ echo "CI gate: FAILED — still failing after $MAX_CI_ITER fix iteration(s). NOT merging."
327
+ echo "{ \"pr\": $PR_NUMBER, \"source\": \"$SOURCE\", \"target\": \"$PR_TARGET\", \"status\": \"ci_failed\", \"ci_fixes\": $CI_FIXES, \"review_findings\": 0 }"
328
+ exit 1
329
+ fi
330
+ fi
331
+ fi
332
+
333
+ # Emit gate state marker for auditability <!-- Ref: issue #1582 — gate passes must be auditable -->
334
+ if [ "$DRY_RUN" = "false" ] && [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "DRY_RUN" ]; then
335
+ gh pr comment "$PR_NUMBER" $GH_FLAG --body "<!-- FORGE:GATE_PASS phase=ci iterations=${CI_ITER} fixes=${CI_FIXES} -->" 2>/dev/null || true
336
+ fi
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Phase 4: Review Gate
342
+
343
+ Invoke `/review-pr` for the PR. Route staging→main PRs to the staging review spec.
344
+
345
+ **On rejection**: Callers (e.g., /autopilot) are expected to push a fix commit to the source branch before this command is re-invoked. The re-review loop here handles only the re-review after a fix is already pushed — it does NOT fix code itself.
346
+
347
+ ```bash
348
+ REVIEW_FINDINGS=0
349
+ REVIEW_GATE_PASSED=false
350
+ REVIEW_ITER=0
351
+
352
+ while [ "$REVIEW_ITER" -lt "$MAX_REVIEW_ITER" ] && [ "$REVIEW_GATE_PASSED" = "false" ]; do
353
+ REVIEW_ITER=$((REVIEW_ITER + 1))
354
+ echo "=== Review Gate Iteration $REVIEW_ITER / $MAX_REVIEW_ITER ==="
355
+
356
+ if [ "$DRY_RUN" = "false" ]; then
357
+ # Route: staging→main uses review-pr-staging; all other uses standard review-pr
358
+ if [ "$SOURCE" = "$STAGING_BRANCH" ] && [ "$PR_TARGET" = "$DEFAULT_BRANCH" ]; then
359
+ REVIEW_RESULT=$(Skill("review-pr-staging", args="$PR_NUMBER $GH_FLAG"))
360
+ else
361
+ REVIEW_RESULT=$(Skill("review-pr", args="$PR_NUMBER $GH_FLAG"))
362
+ fi
363
+ else
364
+ echo "[DRY-RUN] Would invoke review-pr for PR #$PR_NUMBER"
365
+ REVIEW_RESULT="DRY_RUN_APPROVED"
366
+ fi
367
+
368
+ # Extract verdict from review result
369
+ # Review-pr posts <!-- FORGE:REVIEW --> comment on the PR — read it
370
+ if [ "$DRY_RUN" = "false" ]; then
371
+ REVIEW_VERDICT=$(gh api "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
372
+ --jq '[.[] | select(.body | contains("FORGE:REVIEW"))] | last | .body' 2>/dev/null \
373
+ | grep -oE 'APPROVED|CHANGES REQUESTED' | head -1 || echo "")
374
+ FINDINGS_THIS_ROUND=$(gh api "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
375
+ --jq '[.[] | select(.body | contains("FORGE:REVIEW"))] | last | .body' 2>/dev/null \
376
+ | grep -oE '[0-9]+ finding' | grep -oE '[0-9]+' | head -1 || echo "0")
377
+ REVIEW_FINDINGS=$((REVIEW_FINDINGS + ${FINDINGS_THIS_ROUND:-0}))
378
+ else
379
+ REVIEW_VERDICT="APPROVED"
380
+ fi
381
+
382
+ if [ "$REVIEW_VERDICT" = "APPROVED" ]; then
383
+ echo "Review gate: APPROVED after $REVIEW_ITER iteration(s)"
384
+ REVIEW_GATE_PASSED=true
385
+ elif [ "$REVIEW_VERDICT" = "CHANGES REQUESTED" ]; then
386
+ echo "Review gate: CHANGES REQUESTED (iteration $REVIEW_ITER / $MAX_REVIEW_ITER)"
387
+ if [ "$REVIEW_ITER" -lt "$MAX_REVIEW_ITER" ]; then
388
+ echo "Waiting for caller to push a fix commit, then re-reviewing..."
389
+ echo "NOTE: /deploy-pr does not fix code itself. Push fix commits to $SOURCE and re-invoke, or allow the max-review-iterations loop to exhaust."
390
+ # In an autonomous context (e.g., /autopilot), the caller pushes the fix.
391
+ # In manual context, the operator pushes the fix and re-invokes /deploy-pr.
392
+ # This loop's next iteration will re-read the latest PR state.
393
+ fi
394
+ else
395
+ echo "WARNING: Could not determine review verdict from FORGE:REVIEW comment. Assuming APPROVED."
396
+ REVIEW_GATE_PASSED=true
397
+ fi
398
+ done
399
+
400
+ if [ "$REVIEW_GATE_PASSED" = "false" ]; then
401
+ echo "Review gate: FAILED — changes requested after $MAX_REVIEW_ITER review iteration(s). NOT merging."
402
+ echo "{ \"pr\": $PR_NUMBER, \"source\": \"$SOURCE\", \"target\": \"$PR_TARGET\", \"status\": \"review_failed\", \"ci_fixes\": $CI_FIXES, \"review_findings\": $REVIEW_FINDINGS }"
403
+ exit 1
404
+ fi
405
+
406
+ # Emit review gate state marker for auditability
407
+ if [ "$DRY_RUN" = "false" ] && [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "DRY_RUN" ]; then
408
+ gh pr comment "$PR_NUMBER" $GH_FLAG --body "<!-- FORGE:GATE_PASS phase=review iterations=${REVIEW_ITER} findings=${REVIEW_FINDINGS} -->" 2>/dev/null || true
409
+ fi
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Phase 5: Merge
415
+
416
+ Both CI and review gates have passed. Merge the PR.
417
+
418
+ **Never force-merges.** If merge fails, report and stop.
419
+
420
+ ```bash
421
+ if [ "$DRY_RUN" = "true" ]; then
422
+ echo "[DRY-RUN] Would merge PR #$PR_NUMBER (${SOURCE} → ${PR_TARGET})"
423
+ MERGE_STATUS="dry_run"
424
+ else
425
+ echo "Merging PR #$PR_NUMBER..."
426
+ MERGE_OUTPUT=$(gh pr merge "$PR_NUMBER" $GH_FLAG --merge 2>&1)
427
+ MERGE_EXIT=$?
428
+
429
+ if [ "$MERGE_EXIT" -eq 0 ]; then
430
+ echo "Merged PR #$PR_NUMBER successfully"
431
+ MERGE_STATUS="merged"
432
+ else
433
+ echo "ERROR: Merge failed:"
434
+ echo "$MERGE_OUTPUT"
435
+ echo "{ \"pr\": $PR_NUMBER, \"source\": \"$SOURCE\", \"target\": \"$PR_TARGET\", \"status\": \"merge_failed\", \"ci_fixes\": $CI_FIXES, \"review_findings\": $REVIEW_FINDINGS }"
436
+ exit 1
437
+ fi
438
+ fi
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Phase 6: Report Structured Result
444
+
445
+ Return a machine-readable result for callers. This structured block is the primary output consumed by `/autopilot` and other callers.
446
+
447
+ ```bash
448
+ echo ""
449
+ echo "=== /deploy-pr Complete ==="
450
+ echo ""
451
+ echo "DEPLOY_RESULT: pr=$PR_NUMBER source=$SOURCE target=$PR_TARGET status=$MERGE_STATUS ci_fixes=$CI_FIXES review_findings=$REVIEW_FINDINGS"
452
+ echo ""
453
+
454
+ # Machine-readable JSON result (consumed by callers)
455
+ cat <<JSON
456
+ {
457
+ "pr": ${PR_NUMBER:-null},
458
+ "source": "${SOURCE}",
459
+ "target": "${PR_TARGET}",
460
+ "status": "${MERGE_STATUS}",
461
+ "ci_fixes": ${CI_FIXES},
462
+ "review_findings": ${REVIEW_FINDINGS},
463
+ "ci_gate": "$([ $CI_GATE_PASSED = true ] && echo passed || echo failed)",
464
+ "review_gate": "$([ $REVIEW_GATE_PASSED = true ] && echo passed || echo failed)",
465
+ "iterations": {
466
+ "ci": ${CI_ITER},
467
+ "review": ${REVIEW_ITER}
468
+ }
469
+ }
470
+ JSON
471
+ ```
472
+
473
+ ---
474
+
475
+ ## Error Reference
476
+
477
+ | Exit Condition | Status Field | Action |
478
+ |---------------|-------------|--------|
479
+ | No commits to deploy | `nothing_to_deploy` | No PR created — exit 0 |
480
+ | PR creation failed | `pr_creation_failed` | Check gh auth and branch existence |
481
+ | CI failed after max iterations | `ci_failed` | Check failing CI job logs; fix manually |
482
+ | Review failed after max iterations | `review_failed` | Address review findings; re-invoke |
483
+ | Merge failed | `merge_failed` | Check merge conflicts; may need manual resolution |
484
+
485
+ **`needs-human` pattern**: When any exit condition produces a non-zero exit, callers (like `/autopilot`) should add a `needs-human` label to the parent issue and stop autonomous iteration for this branch.