shipit 1.4.8

Shipit is an open source command line interface for managing merge requests, changelogs, tags, and releases using a plan and apply interface. Built with coding agent integration in mind.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
# Shipit — AI Agent Guide

This document describes how a coding agent can use shipit to drive a
**git flow lite** release process: feature branches land in `dev`, `dev`
promotes to `main`, and `main` is tagged for release.

---

## Workflow Overview

```
feature/my-feature  ──b2b──►  dev  ──b2b──►  main  ──b2t──►  v1.2.3
```

**When a user asks you to use shipit to do something** (e.g. "use shipit to
release X"), begin your response by telling them: "I'll show you a plan before
applying anything — nothing will be created on the platform until you approve."

Each stage follows the same two-step **plan / apply** pattern:

1. **Plan** — collect commits, generate a description/title/notes, write a
   YAML file to `.shipit/plans/<hash>.yml`. Nothing is created on the platform yet.
2. **Apply** — read the plan file and execute: open a PR/MR, or create and push
   the annotated tag.

The plan file is the agent's opportunity to review, enrich, or rewrite any field
before anything is published.

---

## Setup

```bash
shipit init
```

This writes `shipit.toml` and creates `.shipit/plans/`.

Both `--platform-domain` and `--platform-token` are optional — shipit discovers
sensible defaults automatically:

- **Domain** — inferred from the `origin` remote URL (or the remote named by
  `--remote`). SSH (`git@github.com:…`) and HTTPS (`https://github.com/…`)
  formats are both recognised.
- **Token** — looked up from the environment based on the resolved domain:
  - GitHub: `GITHUB_TOKEN`, then `GH_TOKEN`
  - GitLab: `GITLAB_TOKEN`, then `GITLAB_PRIVATE_TOKEN`

When running `shipit init` non-interactively (e.g. in CI or as part of an
agent workflow), pass the flags explicitly to skip all prompts:

```bash
shipit init --platform-domain github.com --platform-token "$GITHUB_TOKEN"
```

---

## Stage 1 — Feature Branch → Dev

### 1a. Generate a plan

```bash
shipit b2b plan feature/my-feature dev
```

Shipit collects commits on `feature/my-feature` that are not yet on `dev`,
enriches them with PR/MR titles from the platform API, and writes a plan file.

**Conventional-commit structured description** (recommended when commits follow
the conventional-commit convention):

```bash
shipit b2b plan feature/my-feature dev --conventional-commits
```

The description will be grouped into sections (`## Features`, `## Bug Fixes`,
`## Infrastructure`, etc.).

**Agent-provided title and description** (skip commit collection entirely):

```bash
shipit b2b plan feature/my-feature dev \
  --title "feat: add payment integration" \
  --description "$(cat <<'EOF'
## Summary
- Adds Stripe checkout flow
- Introduces `PaymentService` with retry logic
- Updates API contract in `openapi.yml`
EOF
)"
```

When both `--title` and `--description` are supplied, no commits are collected
and the plan is written immediately.

### 1b. Apply the plan

```bash
shipit b2b apply <plan-filename>.yml
```

`<plan-filename>` is the filename (not a full path) of the file written to
`.shipit/plans/` in the previous step. Shipit opens the pull/merge request
and prints the URL.

**Capturing the plan for agent use** — pass `--yaml` to receive the full plan
on stdout. The output includes a `plan_file` field with the filename ready for
`apply`, and the `commits` list for extracting commit SHAs:

```bash
PLAN=$(shipit b2b plan feature/my-feature dev --yaml -y)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
shipit b2b apply "$PLAN_FILE"
```

---

## Stage 2 — Dev → Main

Same commands, different branches:

```bash
# Auto-generate from commits (conventional-commit format)
PLAN=$(shipit b2b plan dev main --conventional-commits -y --yaml)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')

# Apply
shipit b2b apply "$PLAN_FILE"
```

The `-y` flag skips the interactive title prompt and accepts the suggested
`"Release Candidate vX.Y.Z"` title derived from the commit history.

---

## Stage 3 — Main → Tag

### 3a. Generate a tag plan

```bash
shipit b2t plan main
```

Shipit finds the most recent tag reachable from `main`, collects commits since
that tag, and suggests the next semantic version.

**Conventional-commit structured notes:**

```bash
shipit b2t plan main --conventional-commits -y
```

**Agent-provided tag name and notes:**

```bash
shipit b2t plan main v1.2.3 \
  --description "$(cat <<'EOF'
## What's Changed
- New payment integration (#42)
- Fixed session timeout bug (#38)
EOF
)"
```

### 3b. Apply the tag plan

```bash
shipit b2t apply <plan-filename>.yml
```

Creates an annotated local tag and pushes `refs/tags/<name>` to the remote.

**Capturing the tag plan for agent use:**

```bash
PLAN=$(shipit b2t plan main --conventional-commits -y --yaml)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
shipit b2t apply "$PLAN_FILE"
```

---

## Agent-Enriched Plans (Recommended Pattern)

> **Important for AI agents:** Always present the final plan to the user and
> wait for explicit approval before running `apply`. Opening a pull/merge
> request or pushing a tag is irreversible — the plan step exists precisely
> to give the user a review checkpoint. Never call `apply` autonomously.

The most powerful use of shipit for an agent is:

1. Run `b2b plan` or `b2t plan` with `--yaml` to collect commits, write the
   plan file, and receive the plan on stdout.
2. Use `yq` to extract the `plan_file` name (for the `apply` step) and the
   boundary commit SHAs (for `git diff`).
3. Run `git diff <first-sha>^..<last-sha>` to get the full diff for the range.
4. Summarise the diff and rerun `plan` with `--description` and/or `--title` to
   overwrite the auto-generated content with a human-quality summary.
5. Run `apply` on the enriched plan.

### Example: agent-enriched b2b plan

```bash
# Step 1 — write the initial plan and capture the YAML output
PLAN=$(shipit b2b plan feature/payments dev --conventional-commits -y --yaml --allow-dirty)

# Step 2 — extract the plan filename and boundary commit SHAs with yq
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
LAST_SHA=$(echo "$PLAN"  | yq '.commits[0]'  | awk '{print $NF}')
FIRST_SHA=$(echo "$PLAN" | yq '.commits[-1]' | awk '{print $NF}')

# Step 3 — get the diff
DIFF=$(git diff "${FIRST_SHA}^".."${LAST_SHA}")

# Step 4 — ask the agent to summarise the diff, then rerun plan with the result
SUMMARY="<agent-generated summary goes here>"

PLAN=$(shipit b2b plan feature/payments dev \
  --title "feat(payments): Stripe checkout integration" \
  --description "$SUMMARY" \
  --yaml --yes --allow-dirty)
PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')

# Step 5 — present the plan to the user for confirmation before applying
echo "$PLAN"

# Step 6 — apply only after the user approves
shipit b2b apply "$PLAN_FILE" --allow-dirty
```

### Commit ordering in the `commits` list

Commits are stored newest-first under the `commits:` key. Each entry is the
string `"<message> <sha>"` — the SHA is always the last whitespace-separated
token. Use index `0` for the newest commit and `-1` for the oldest:

```bash
PLAN=$(shipit b2b plan feature/payments dev --yaml -y)

LAST_SHA=$(echo "$PLAN"  | yq '.commits[0]'  | awk '{print $NF}')   # newest
FIRST_SHA=$(echo "$PLAN" | yq '.commits[-1]' | awk '{print $NF}')   # oldest

git diff "${FIRST_SHA}^".."${LAST_SHA}"
```

The diff can then be passed to the agent's language model to generate a
structured description before calling `plan` again with `--description`.

---

## Flag Reference

### `shipit init`

| Flag | Description |
|---|---|
| `--platform-domain <domain>` | Platform domain (default: inferred from remote URL) |
| `--platform-token <token>` | Platform personal access token (default: inferred from env — see below) |
| `--remote <name>` | Git remote to infer the domain from (default: `origin`) |
| `--dir <path>` | Directory to write config to (default: cwd) |

**Token environment variable lookup order:**

| Platform | Variables checked (in order) |
|---|---|
| GitHub | `GITHUB_TOKEN`, `GH_TOKEN` |
| GitLab | `GITLAB_TOKEN`, `GITLAB_PRIVATE_TOKEN` |

### `shipit b2b plan <source> <target>`

| Flag | Short | Description |
|---|---|---|
| `--conventional-commits` | `-c` | Group description by commit type |
| `--title <text>` | | Override the suggested PR/MR title |
| `--description <text>` | | Override the auto-generated description |
| `--only-merges` | | Restrict commit collection to merge commits |
| `--no-sign` | | Omit the "generated by Shipit" footer |
| `--yes` | `-y` | Accept all prompts non-interactively |
| `--yaml` | | Emit the plan as YAML to stdout (includes `plan_file` field) |
| `--allow-dirty` | | Continue even if the working directory has uncommitted changes |
| `--remote <name>` | | Git remote name (default: `origin`) |
| `--dir <path>` | | Repository root (default: cwd) |

### `shipit b2b apply <plan-file>`

| Flag | Description |
|---|---|
| `--allow-dirty` | Continue even if the working directory has uncommitted changes |
| `--remote <name>` | Git remote name (default: `origin`) |
| `--dir <path>` | Repository root (default: cwd) |

### `shipit b2t plan <branch> [tag]`

| Argument | Description |
|---|---|
| `[tag]` | Tag name to create (default: next semantic version derived from commits) |

| Flag | Short | Description |
|---|---|---|
| `--conventional-commits` | `-c` | Group notes by commit type |
| `--description <text>` | | Override the auto-generated tag notes |
| `--latest-tag <name>` | | Compare against a specific tag instead of auto-detecting |
| `--only-merges` | | Restrict commit collection to merge commits |
| `--no-sign` | | Omit the "generated by Shipit" footer |
| `--yes` | `-y` | Accept all prompts non-interactively |
| `--yaml` | | Emit the plan as YAML to stdout (includes `plan_file` field) |
| `--allow-dirty` | | Continue even if the working directory has uncommitted changes |
| `--remote <name>` | | Git remote name (default: `origin`) |
| `--dir <path>` | | Repository root (default: cwd) |

### `shipit b2t apply <plan-file>`

| Flag | Description |
|---|---|
| `--allow-dirty` | Continue even if the working directory has uncommitted changes |
| `--remote <name>` | Git remote name (default: `origin`) |
| `--dir <path>` | Repository root (default: cwd) |

---

## Plan File Format

Files written to `.shipit/plans/` look like this:

```yaml
# Shipit Plan - Generated by shipit v0.5.0 on 2024-06-01T12:00:00Z
shipit_version: 0.5.0
generated_at: "2024-06-01T12:00:00Z"
source: feature/payments
target: dev
title:
  value: "Release Candidate v1.2.0"
  generated_by: default        # "user" | "default" | "conventional-commits" | "raw"
description:
  value: |
    ## Features
    - feat: add Stripe checkout flow abc123
  generated_by: conventional-commits
commits:
  - "feat: add Stripe checkout flow abc123 a1b2c3d4"
  - "fix: handle webhook timeout def456 e5f6a7b8"
```

The `generated_by` field records what produced each value so downstream
tooling (and the agent) can decide whether to trust it or regenerate it.

When `--yaml` is passed, the stdout output adds a `plan_file` field not
present in the written file:

```yaml
plan_file: 3f9a1c2e4d7b0e5f.yml
```

Use this field to drive the `apply` step without filesystem globbing.

---

## Complete Git Flow Lite Example

```bash
# ── Feature → Dev ──────────────────────────────────────────────────────────
PLAN=$(shipit b2b plan feature/payments dev --conventional-commits -y --yaml --allow-dirty)
shipit b2b apply "$(echo "$PLAN" | yq '.plan_file')" --allow-dirty

# ── Dev → Main ─────────────────────────────────────────────────────────────
PLAN=$(shipit b2b plan dev main --conventional-commits -y --yaml --allow-dirty)
shipit b2b apply "$(echo "$PLAN" | yq '.plan_file')" --allow-dirty

# ── Main → Tag ─────────────────────────────────────────────────────────────
PLAN=$(shipit b2t plan main --conventional-commits -y --yaml --allow-dirty)
shipit b2t apply "$(echo "$PLAN" | yq '.plan_file')" --allow-dirty
```

---

## Multi-Project Release Workflow

This section describes how an agent can coordinate releases across **multiple
repositories** in a defined order. Each project has its own environment
pipeline (the ordered sequence of branch-to-branch promotions and/or
branch-to-tag operations). A shared config file persists these settings
across workflow runs.

---

### Config File

The multi-project config lives at `.shipit/multi-release.yml` relative to the
directory where the agent is invoked (typically a workspace or monorepo root,
but it can also be any convenient location).

**Format:**

```yaml
# .shipit/multi-release.yml
# Projects are released in the order they appear in this list.
projects:
  - name: api-service
    dir: /absolute/path/to/api-service   # directory that contains shipit.toml
    pipeline:
      - type: b2b        # branch-to-branch PR/MR
        source: dev
        target: qa
      - type: b2b
        source: qa
        target: main
      - type: b2t        # branch-to-tag
        source: main

  - name: frontend
    dir: /absolute/path/to/frontend
    pipeline:
      - type: b2b
        source: dev
        target: staging
      - type: b2b
        source: staging
        target: main
      - type: b2t
        source: main

  - name: infra
    dir: /absolute/path/to/infra
    pipeline:
      - type: b2b
        source: dev
        target: main
      - type: b2t
        source: main
```

**Field reference:**

| Field | Description |
|---|---|
| `name` | Human-readable project label used in agent prompts |
| `dir` | Absolute path to the project root (must contain `shipit.toml`) |
| `pipeline` | Ordered list of release steps for this project |
| `pipeline[].type` | `b2b` (branch-to-branch) or `b2t` (branch-to-tag) |
| `pipeline[].source` | Source branch |
| `pipeline[].target` | Target branch (`b2b` only) |

---

### Agent Decision Tree

Every time the multi-project workflow is triggered, the agent follows this
decision tree before executing any release steps.

```
START
    Does .shipit/multi-release.yml exist?
    ├─ NO ──► [First-Run Setup] ──► write config ──► continue
    └─ YES
                   Ask the user:
       "I found the following release config:
          1. api-service  (dev → qa → main → tag)
          2. frontend     (dev → staging → main → tag)
          3. infra        (dev → main → tag)
        Do you want to run the full release for all projects, or is this
        an atypical run (e.g. a subset of projects, or fewer environment
        steps)?"
              ├─ FULL ──► use config as-is ──► execute
              └─ ATYPICAL
                                        Collect overrides from user:
              - Which projects? (subset / reordering)
              - For each project, which pipeline steps? (subset / reordering)
            Do NOT write changes back to config.
                                        Execute with overrides only
```

---

### First-Run Setup

When `.shipit/multi-release.yml` does not exist, the agent must collect
configuration from the user interactively before proceeding.

**Prompts to ask (in order):**

1. "How many projects do you want to include in the multi-project release
   workflow? Please list them in release order."
2. For each project in order:
   - "What is the name of this project?"
   - "What is the absolute path to this project's directory?"
   - "What is the environment pipeline for this project?  
     List the steps in order, e.g.:  
     `dev → qa` (b2b), `qa → main` (b2b), `main → tag` (b2t)"
3. Show the agent's interpretation of the collected config in YAML form and
   ask the user to confirm before writing the file.

Once confirmed, write `.shipit/multi-release.yml` and proceed with the
release using the new config.

---

### Executing the Workflow

**Before iterating over pipeline steps**, verify that every project in the
effective run has been initialised with `shipit init` by checking for a
`shipit.toml` file in its `dir`:

```bash
# For each project
test -f <project-dir>/shipit.toml || echo "MISSING"
```

If any project is missing `shipit.toml`, run `shipit init` for it before
continuing:

```bash
shipit init --dir <project-dir>
```

Do not proceed with any pipeline steps until all projects report a valid
`shipit.toml`.

---

For each project (in config order, or user-specified order for atypical runs),
for each pipeline step (in config order, or user-specified subset):

1. `cd` into the project's `dir`.
2. Check out the source branch for the current step:
   ```bash
   git -C <project-dir> checkout <source>
   ```
3. If the step is `b2b`:
   ```bash
   PLAN=$(shipit b2b plan <source> <target> \
     --conventional-commits -y --yaml --allow-dirty \
     --dir <project-dir>)
   PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
   ```
   If the step is `b2t`:
   ```bash
   PLAN=$(shipit b2t plan <source> \
     --conventional-commits -y --yaml --allow-dirty \
     --dir <project-dir>)
   PLAN_FILE=$(echo "$PLAN" | yq '.plan_file')
   ```
4. Record the plan outcome for the summary table (see step 9). A step has:
   - **No changes** — the plan's `commits` list is empty.
   - **Success** — the plan was generated without error and has commits.
   - **Failed** — the plan command exited with an error.
5. Present the plan to the user and **wait for explicit approval** before
   calling `apply`. This is mandatory — see the warning in the
   [Agent-Enriched Plans]#agent-enriched-plans-recommended-pattern section.
   Every plan summary presented to the user **must** include:
   - **Plan ID** — the `plan_file` value from the YAML output (e.g. `3f9a1c2e4d7b0e5f.yml`)
   - **Plan path** — the full path to the written plan file (e.g. `<project-dir>/.shipit/plans/3f9a1c2e4d7b0e5f.yml`)
6. On approval:
   ```bash
   shipit b2b apply "$PLAN_FILE" --allow-dirty --dir <project-dir>
   # or
   shipit b2t apply "$PLAN_FILE" --allow-dirty --dir <project-dir>
   ```
7. After **all** pipeline steps across **all** projects have been planned
   (regardless of how many were applied), output a Markdown summary table:

   | Project | Step | Plan ID | Result | Title / Tag |
   |---|---|---|---|---|
   | api-service | dev → qa | `3f9a1c2e4d7b0e5f.yml` | ✓ success | feat: add payment integration |
   | api-service | qa → main | `a1b2c3d4e5f60718.yml` | ✓ success | Release Candidate v1.4.0 |
   | api-service | main → tag | `9e8d7c6b5a4f3e2d.yml` | ✓ success | v1.4.0 |
   | frontend | dev → staging | — | — no changes | — |
   | infra | dev → main | — | ✗ failed | — |

   **Result values:**
   - `✓ success` — plan generated with commits present
   - `— no changes` — plan's `commits` list was empty; nothing to release
   - `✗ failed` — plan command exited with an error (include the error message in a note below the table)

   The **Plan ID** column shows the `plan_file` value from the plan YAML (e.g.
   `3f9a1c2e4d7b0e5f.yml`). Use `` when no plan was generated (no changes or
   failed).

   The **Title / Tag** column shows the `title.value` from the plan YAML for
   `b2b` steps, or the tag name for `b2t` steps. Use `` when there is
   nothing to show (no changes or failed).

8. After all projects and pipeline steps have been processed, generate a
   release summary file and write it to the directory where
   `.shipit/multi-release.yml` lives.

   > **Important for AI agents:** Every tag and every pull/merge request
   > created during the release **must** appear as a clickable Markdown link
   > in the summary. Never write a bare tag name or PR number — always wrap it
   > in a hyperlink. Use the URL printed by `b2b apply` for pull/merge requests
   > and construct the tag URL from the remote host
   > (e.g. `https://github.com/org/repo/releases/tag/v1.4.0`). A summary
   > without links is incomplete.

   **File naming:** `release-summary-<ISO-8601-timestamp>.md`  
   Use the current local time in `YYYYMMDDTHHMMSS` format (no colons or spaces):
   ```
   release-summary-20240601T143022.md
   ```

   **Summary file structure:**

   ```markdown
   # 🚀 Release Summary — <human-readable date and time>

   ## ✅ Projects With Changes

   | Project | Step | Tag / PR | Title |
   |---|---|---|---|
   | api-service | main → tag | `v1.4.0` | — |
   | api-service | dev → qa | [#42](https://github.com/org/api-service/pull/42) | feat: add payment integration |
   | infra | dev → main | [#11](https://github.com/org/infra/pull/11) | chore: update terraform modules |

   ## ⏩ Projects Without Changes

   - **frontend** — no commits found across all pipeline steps; nothing released.

   ## 🔍 Tagged Release Highlights

   ### ✨ New Features
   - <concise bullet summarising feature commits across all projects>

   ### 🐛 Bug Fixes
   - <concise bullet summarising fix commits across all projects>

   ### 🔧 Infrastructure
   - <concise bullet summarising chore/infra commits across all projects>

   ### 📄 Documentation
   - <concise bullet summarising docs commits across all projects>

   > Sections with no entries should be omitted entirely.
   ```

   **How to populate the summary:**
   - **Projects With Changes** — one row per pipeline step that produced a
     successful apply. Use the PR/MR URL returned by `b2b apply`, or the tag
     name returned by `b2t apply`, as the link/value in the **Tag / PR** column.
     Link to the created resource whenever possible: use a Markdown hyperlink
     for pull/merge requests (e.g. `[#42](https://github.com/org/repo/pull/42)`)
     and for tags if the remote host provides a tag URL
     (e.g. `[v1.4.0](https://github.com/org/repo/releases/tag/v1.4.0)`).
   - **Projects Without Changes** — list every project (or individual step)
     where the plan's `commits` list was empty across the entire session.
   - **Release Highlights** — aggregate the `commits` lists from every
     successful b2t plan across all projects. Only changes associated with 
     tags should appear here. Group them by conventional-commit
     prefix (`feat`, `fix`, `chore`/`infra`, `docs`, etc.) and write one
     concise bullet per logical theme. Mention the project the highlight is 
     relevant to in the bullet point description. Omit any section that has 
     no entries.

   Present the generated file path to the user once it has been written.

**Do not proceed to the next pipeline step or the next project until the
current step succeeds and the user approves.**

---

### Handling Atypical Runs

When the user indicates a non-standard run (a subset of projects, fewer
environment steps, a different order), the agent must:

1. Collect the exact scope from the user — confirm each override explicitly.
2. Echo back the effective plan ("I will release `api-service` dev→qa only,
   then `infra` dev→main→tag") and wait for confirmation.
3. Execute using the collected overrides.
4. **Never write overrides back to `.shipit/multi-release.yml`.**  The
   persisted config always reflects the canonical full-release workflow.

---

### Deferred Tagging

When starting a multi-project release session, inform the user of the following
option before executing any steps:

> "Sometimes you may want a `b2b` release (a pull/merge request) to be reviewed
> and deployed before creating the tag with `b2t`. If that applies to any
> project in this run, let me know and I will skip the `b2t` step for it now.
> I will add it to a **Pending** section in the release summary so you can
> resume your session later and I will run the tag creation at that point."

When a `b2t` step is deferred:

1. Skip the `b2t` plan and apply for that project entirely.
2. Add a `## ⏳ Pending Tag Creation` section to the release summary file
   beneath the "Projects Without Changes" section:

   ```markdown
   ## ⏳ Pending Tag Creation

   The following projects had their `b2t` step deferred pending review and
   deployment of the `b2b` release above. Resume your session and reference
   the release summary file to trigger tag creation when ready.

   | Project | Deferred Step | Release Summary File |
   |---|---|---|
   | api-service | main → tag | `release-summary-20240601T143022.md` |
   ```

3. Tell the user:
   > "The `b2t` step for **\<project\>** has been deferred. When you are ready
   > to create the tag, start a new session and tell me to resume from
   > `release-summary-<timestamp>.md`. I will move the pending entry up into
   > the **Projects With Changes** section once the tag has been created."

**Resuming a deferred tag session:**

When the user references a release summary file to resume a deferred `b2t`
step, the agent must:

1. Read the release summary file to identify all entries in the
   `## ⏳ Pending Tag Creation` section.
2. For each pending entry, run the `b2t` plan and apply flow exactly as
   described in [Stage 3 — Main → Tag]#stage-3--main--tag, presenting the
   plan to the user for approval before applying.
3. On success, update the release summary file:
   - Move the completed entry from `## ⏳ Pending Tag Creation` into the
     `## ✅ Projects With Changes` table, adding the tag link in the
     **Tag / PR** column.
   - Remove the `## ⏳ Pending Tag Creation` section entirely if no pending
     entries remain.
4. Present the updated summary file path to the user.

---

### Example: Full Release Session

```
Agent: I found the following release config:
         1. api-service  — dev → qa (b2b) → main (b2b) → tag (b2t)
         2. frontend     — dev → staging (b2b) → main (b2b) → tag (b2t)
         3. infra        — dev → main (b2b) → tag (b2t)
       Is this a full release across all projects and environments, or do
       you want to adjust the scope?

User:  Full release.

Agent: Starting release for project 1/3: api-service

       Step 1/3: dev → qa
       [generates plan, shows summary]
       Proceed with applying this plan? (yes/no)

User:  yes

Agent: ✓ PR opened: https://github.com/org/api-service/pull/42

       Step 2/3: qa → main
       [generates plan, shows summary]
       Proceed with applying this plan? (yes/no)

User:  yes

Agent: ✓ PR opened: https://github.com/org/api-service/pull/43

       Step 3/3: main → tag
       [generates tag plan, shows summary]
       Proceed with applying this plan? (yes/no)

User:  yes

Agent: ✓ Tag v1.4.0 created and pushed.

       Moving to project 2/3: frontend
       ...
```

---

### Example: Atypical Run Session

```
Agent: I found the following release config:
         1. api-service  — dev → qa (b2b) → main (b2b) → tag (b2t)
         2. frontend     — dev → staging (b2b) → main (b2b) → tag (b2t)
         3. infra        — dev → main (b2b) → tag (b2t)
       Is this a full release across all projects and environments, or do
       you want to adjust the scope?

User:  Just release api-service and infra, skip tagging infra.

Agent: Got it — atypical run (will not update your saved config).
       Here is the effective plan:
         1. api-service  — dev → qa (b2b) → main (b2b) → tag (b2t)
         2. infra        — dev → main (b2b)  [tag step skipped]
       Confirm? (yes/no)

User:  yes

Agent: Starting release for project 1/2: api-service
       ...
```