---
name: apt:ship
description: "Create PR with QA scores, decision traceability, and commit mapping"
apt-skill-version: {{APT_VERSION}}
stage: ship
intent: ship
when_to_use: "The build + review are done and the user wants a PR with QA scores, decision traceability, and commit mapping."
user_invocable: true
internal: false
spawns_agent: false
agent_name: null
task_context: require-existing
default_execution_mode: auto
execution_modes:
  - auto
  - step
allowed-tools: "Bash, Read, Write, Edit, Grep, Glob"
argument-hint: "apt:ship [--draft | --title \"...\"]"
gates:
  - constitution-read
  - verify-approved
  - review-clean
  - consistency-check
  - r-persona-alignment
  - gitignore-in-sync
config_keys:
  - pipeline.auto_branch
  - pipeline.branch_prefix
---
<objective>
Create a pull request (or publish a package) with full traceability from decisions to implementation. The PR body includes QA dimension scores, decision audit trail, and commit-to-subtask mapping. This is the final step in any track -- after verify passes, ship it.
</objective>

<your_environment>
- **Working directory:** The project root (where you were invoked)
- **apt-tools path:** `node packages/framework/bin/apt-tools.mjs` or the locally installed `apt-tools` binary
- **Constitution:** Read `AGENTS.md` in the project root if it exists
- **GitHub CLI:** `gh` must be installed and authenticated
</your_environment>

<state_files>
## State Files

**Reads:**
- `.aperant/state.json` — Active task, completed subtasks; resolve the active plan via `active_tasks[*].spec.path` / `implementation_plan` or the most-recent `.aperant/tasks/{task-id}/implementation_plan.json`.
- `review.json` — QA scores and findings (if review was run)
- `.aperant/context/notes/` — Decision traceability (gray areas, discuss outcomes)

**Writes:**
- Nothing locally -- creates PR externally via `gh pr create`
- Updates `.aperant/state.json` to clear active task after shipping
</state_files>

<process>

## 0. Hard Gate Check

Before gathering context or building any PR artifacts, run the hard-gate check for the `ship` skill:

```bash
node packages/framework/bin/apt-tools.mjs gate check . --task-id {task-id} --skill ship
```

Parse the JSON envelope:
- If `status == "ok"`: proceed to Section 0.5 / 1.
- If `status == "fail"` AND `forced` is empty (i.e. the user did NOT pass `--force`): STOP. Report the failed gates verbatim from `failed:[{id, severity, reason}]`, then emit:
  ```
  Hard gate failed. Fix the issues above, or re-run with:
    --force --reason "<explanation>"
  The override will be logged to .aperant/events/{today}.jsonl.
  ```
  Exit the skill. Do NOT auto-apply `--force` — force is an explicit human decision.

The ship gate covers constitution-read, verify-approved (qa_signoff.json verdict), review-clean (no unresolved critical/warning findings), gitignore-in-sync (.aperant/.gitignore conformance), and consistency-check (G8 — cross-artifact consistency between spec.md, implementation_plan.json, review.json, qa_signoff.json, and build-progress.json). Because these gates run here, Sections 1–3 below no longer need to re-check the verify verdict or block on review findings — the gate already did it.

### 0.1 Parse `profile` from the envelope

The envelope carries `profile: "lite" | "full-pipeline"` derived from `taskRecord.track` (QUICK / DEBUG → lite; STANDARD / DEEP → full-pipeline). Capture it now — Sections 1, 2, and 3 branch on it:

```bash
profile=$(echo "$gate_envelope" | jq -r '.profile // "full-pipeline"')
```

- **`profile === "lite"`** (QUICK or DEBUG track): the three full-pipeline gates (`verify-approved`, `review-clean`, `consistency-check`) were skipped by design — those tracks do not produce `qa_signoff.json`, `review.json`, or a multi-subtask `implementation_plan.json`. SKIP Sections 1 (review.json + decision notes reads) and 2 (commit-to-subtask map). JUMP to Section 3's lite body branch.
- **`profile === "full-pipeline"`** (STANDARD or DEEP track): full pipeline ran — Sections 1, 2, and 3's full-body template all apply unchanged.

## 0.5 Pre-PR Committable Drift Check (C56 B2)

Before gathering PR context, audit for uncommitted committable `.aperant/` state and stage anything the self-staging helper missed. This closes the artifact-leak class that the framework shipped with pre-C56: phases flipped in-progress, roadmap JSON touched, decision notes written — none of which followed into the PR's commits.

```bash
node packages/framework/bin/apt-tools.mjs audit pending-artifacts . --stage
```

Parse the JSON envelope:

- **`unknown[]` is non-empty** → HARD STOP. A file changed under `.aperant/` that the classification contract does not recognize. Either the file is committable and the contract needs to be extended (framework change), or the file is ephemeral and must be added to the ignored-path set. Report the list, exit the skill:
  ```
  [APT:ship] Cannot ship — .aperant/ contains files the classification contract does not recognize:
    - {path1}
    - {path2}
  Extend packages/framework/src/cli/artifacts/classification.mjs or add to ephemeral set, then re-run /apt:ship.
  ```
- **`committable[]` is non-empty AFTER --stage ran** → `audit pending-artifacts --stage` already `git add`-ed those paths on your behalf. Amend them into the next commit before PR creation, OR create a one-line `chore(aperant): artifact drift pre-ship` commit if the rest of the branch is already landed cleanly:
  ```bash
  if ! git diff --cached --quiet; then
    git commit -m "chore(aperant): pre-ship artifact drift capture"
  fi
  ```
- **Envelope status == `clean`** → proceed to Section 1.

Classification reference: `packages/framework/docs/artifact-lifecycle.md` (committable vs ephemeral matrix for every `.aperant/` subpath).

## 0.7 Pre-PR Branch-Current Audit + Rebase Gate

Before any team-sync push (§5) or PR creation (§6), audit whether the task branch has drifted behind `origin/<base_branch>`. A PR opened against a stale base can silently no-op GitHub Actions when path-filter matrices evaluate against the merge-base — the recent Source Control panel pipeline lost ~20 min to this defect class before the operator noticed CI never engaged.

This section runs BEFORE Section 5 (Sync Team Bookkeeping). Order is load-bearing: if §0.7 runs after §5, the team-sync push races the post-§0.7 rebase, the captured `remote_tip_sha` is stale by the time §6 leases against it, and the lease fails on a commit `apt:ship` itself just pushed. The TD-02 structural test locks the §0.7 → §5 ordering.

Resolve the base branch (same `task get` envelope §6 uses):

```bash
task_info=$(node packages/framework/bin/apt-tools.mjs task get . --id {task-id})
base_branch=$(echo "$task_info" | jq -r '.task.base_branch // "main"')
worktree_path=$(echo "$task_info" | jq -r '.task.worktree_path // empty')
ship_cwd=${worktree_path:-.}
```

Run the audit from the worktree (or the project root when no worktree is recorded):

```bash
audit_env=$(node packages/framework/bin/apt-tools.mjs audit branch-current "$ship_cwd" --base "$base_branch")
behind=$(echo "$audit_env" | jq -r '.behind // 0')
ahead=$(echo "$audit_env" | jq -r '.ahead // 0')
mergeable=$(echo "$audit_env" | jq -r '.mergeable')
audit_warning=$(echo "$audit_env" | jq -r '.audit_warning // empty')
remote_tip_sha_pre_rebase=$(echo "$audit_env" | jq -r '.remote_tip_sha // empty')
```

Capture `remote_tip_sha_pre_rebase` now — §6's lease uses it.

**Branch on the envelope:**

- **`behind === 0`** → no rebase needed. The branch is current with `origin/<base_branch>` (or strictly ahead). Skip the rebase and proceed to §1. §6's push will use the plain `git push -u origin <branch>` form, not the lease form.

- **`behind > 0` AND `mergeable === true`** → run a clean rebase:
  ```bash
  git -C "$ship_cwd" rebase "origin/$base_branch"
  ```
  Proceed to §1. §6's push will use `--force-with-lease=refs/heads/<branch>:$remote_tip_sha_pre_rebase` because history was rewritten.

- **`behind > 0` AND `mergeable === false`** → STOP. Emit a structured error with the conflict preview verbatim:
  ```
  [APT:ship] Cannot ship — task branch is behind origin/{base_branch} and the rebase would conflict.
  Conflicts preview:
    - {path1}
    - {path2}
  Resolve manually (rebase or merge in the worktree), then re-run /apt:ship.
  ```
  Exit the skill — do NOT auto-resolve, do NOT proceed to §5 or §6.

- **`mergeable === null` AND `audit_warning === "git-version-too-old"`** → STOP with a friendlier message:
  ```
  [APT:ship] Cannot ship — local git is too old to probe mergeability (need git ≥ 2.38 for 'merge-tree --write-tree').
  Upgrade git, rebase manually, or re-run /apt:ship after upgrading.
  ```
  Exit the skill.

- **`mergeable === null` AND `audit_warning === "fetch-failed"` or `"no-remote-base-ref"`** → STOP. The audit could not verify the remote tip, so we cannot lease safely. Emit:
  ```
  [APT:ship] Cannot ship — could not fetch origin/{base_branch} ({audit_warning}). Check network + remote, then re-run /apt:ship.
  ```
  Exit the skill.

**Autonomy gating.** Read the active task's autonomy level from `.aperant/state.json` (`active_tasks[{task-id}].autonomy`, falling back to `autonomy.default`):

- **Autonomy 0–1 (step / guided)** — print the audit envelope to the operator and ask `Rebase now? [Y/n]` before executing the `git rebase` line. On `N`, exit the skill (user can rebase manually and re-invoke).
- **Autonomy 2 (auto-chain)** — proceed silently on the clean-rebase branch; STOP on conflict regardless.
- **Autonomy 3 (YOLO)** — same as 2: silent on clean, STOP on conflict. Auto-resolution of conflicts is explicitly out of scope (see spec §Out of Scope).

The captured `remote_tip_sha_pre_rebase` and `behind` integer are referenced verbatim in §6's push command. Treat them as read-only state for the remainder of the skill.

## 1. Gather Context

**Profile guard:** Sections 1, 2, and 2.6 apply ONLY to `profile === "full-pipeline"`. When the envelope carried `profile === "lite"` (QUICK / DEBUG), SKIP Sections 1, 2, 2.6 and jump straight to Section 3 (`3.lite` branch).

Read all available context for building the PR:

```bash
# Active task info
cat .aperant/state.json

# Plan spec (active task, if any) — resolve via state first, then fall back to most-recent task dir
#   Prefer: .aperant/tasks/$(jq -r '.active_tasks | to_entries[0].key' .aperant/state.json)/implementation_plan.json

# QA scores (if review was run)
cat review.json 2>/dev/null

# Decision context files
ls .aperant/context/notes/ 2>/dev/null

# Commit log for this branch
git log main..HEAD --oneline
```

Extract:
- **Task description** from `state.json` active task
- **Subtask list** from the plan spec
- **QA scores** from `review.json` (scores object)
- **Review findings** from `review.json` (stats object)
- **Decisions** from `.aperant/context/notes/` files (gray areas resolved, discuss outcomes)
- **Commit list** from git log

## 2. Build Commit-to-Subtask Map

Parse commit messages for subtask references. Commits from `apt:execute` follow the pattern:
```
feat({scope}): {message}
fix({scope}): {message}
refactor({scope}): {message}
```

Map each commit to the subtask it implements by matching scope and message content against the plan's subtask list.

Format as a markdown table:

| Commit | Subtask | Description |
|--------|---------|-------------|
| `abc1234` | #1 | Implement auth middleware |
| `def5678` | #2 | Add token refresh logic |

If no plan exists (ad-hoc task), list commits without subtask mapping.

## 2.6 Release-note auto-draft (persona-aware release-notes opt-in)

This section runs ONLY when `.aperant/config.json` has `changelog.release_notes.enabled === true`. When the toggle is `false` (the default for the 9-of-10 internal projects that never publish user-facing notes), skip the section entirely — do NOT compute the feature diff and do NOT touch the PR body draft.

Read the toggle:

```bash
rn_enabled=$(jq -r '.changelog.release_notes.enabled // false' .aperant/config.json)
```

If `rn_enabled !== "true"`, jump to Section 3.

### 2.6.1 Compute the feature-registry diff

The auto-draft needs to know which features the PR's commits touched. Use the same `features-audit` surface that `apt:execute` uses post-step:

```bash
base=$(node packages/framework/bin/apt-tools.mjs task get . --id {task-id} | jq -r '.base_branch // "main"')
feature_diff=$(node packages/framework/bin/apt-tools.mjs features-audit . --diff-since "$base" --json)
```

The envelope shape (see `packages/framework/src/cli/commands/features-audit.mjs`) returns the list of touched features with their declared `personas: []` field (when the registry's feature row carries one).

### 2.6.2 Decide draft vs block via the helper

Hand the diff + the project's personas envelope + the filter mode to `ship-autodraft.mjs`. The CLI returns one of three envelopes:

- `{status: 'draft', line, persona, matchedFeatures}` — append `line` to the PR body draft built in Section 3.
- `{status: 'block', reason, message}` — fire the block path below.
- `{status: 'warn', reason, message}` — log the warning and proceed without the line.

The block path triggers when `release_notes.require_pr_field === true` (default) AND auto-draft failed (no feature match OR no persona signal). The exact error string is locked by AC7:

```
[APT] No release-note signal found — author manual note in PR body or label PR no-notes
```

When the block fires, STOP — do NOT proceed to `gh pr create`. The user must either (a) author a manual `Release note: …` line in the PR body via `git commit --amend` (or by editing the PR body after open), or (b) label the PR `no-notes` via `gh pr edit --add-label no-notes` and re-run `/apt:ship`. The label-based escape hatch keeps the gate from blocking infra-only PRs that genuinely have no user-facing impact.

### 2.6.3 Inject the line into the PR body draft

When the helper returns `status: 'draft'`, append the `line` value verbatim to the PR body draft assembled in Section 3, immediately under the `## Summary` heading. The line is one sentence, persona-voice-tuned, suitable for a user-facing surface.

### 2.6.4 Helper contract (for testability)

The helper lives at `packages/framework/src/cli/release-notes/ship-autodraft.mjs`. It is a pure function — given the feature-diff envelope + personas envelope + filter mode + the `require_pr_field` toggle, it returns the decision. The `decideAutodraft` export is unit-tested via TD-4; the SKILL.md body wires it without re-deriving the decision logic.

## 3. Build PR Body

**Title:** Derived from task description. Keep it short (<70 characters).

### 3.lite — `profile === "lite"` (QUICK and DEBUG)

When the gate envelope carries `profile === "lite"`, emit the lite PR body. QUICK and DEBUG do not produce `qa_signoff.json` or `review.json`, so the lite body sources verification evidence from `task.json.verification` (QUICK: `.aperant/quick/{task-id}/task.json`) or, when no per-track task.json exists (DEBUG fallback), from the active-task record in `state.active_tasks[task_id]`.

```bash
# QUICK: per-track task.json carries the verification block
task_json=$(cat .aperant/quick/{task-id}/task.json 2>/dev/null)
# DEBUG fallback: no per-track task.json — verification is best-effort, omit the block if absent
```

Lite body shape:

```markdown
## Summary

{one-liner from task.description in state.active_tasks[task-id]}

## Verification

- Typecheck: {task.json.verification.typecheck}
- Lint: {task.json.verification.lint}
- Tests: {task.json.verification.test}
- Visual: {task.json.verification.visual}{when task.json.verification.evidence_path present, append: ` ({evidence_path})`}

{IF track === "DEBUG" AND .aperant/debug/{task-id}/DEBUG.md exists, append the RCA section distilled from the file's `## Conclusion` heading. The distillation is best-effort — quote the conclusion paragraph verbatim, keeping it under ~10 lines:}

## RCA

{distilled from .aperant/debug/{task-id}/DEBUG.md `## Conclusion` section}

## Commits

| Commit | Description |
|--------|-------------|
| `{sha}` | {message} |

---

Built with [Aperant Framework](https://github.com/Mikalsen-AI/aperant-cloud-desktop/tree/main/packages/framework)
```

The lite body omits `## QA Scores`, `## Review Findings`, `## Decision Traceability`, and the per-subtask column in `## Commits` — none of those signals exist on lite-profile tasks. Once the body is assembled, skip the rest of Section 3 and jump to Section 4.

### 3.full — `profile === "full-pipeline"` (STANDARD and DEEP)

The body structure below is the full-pipeline template — unchanged from the pre-fix behavior. Render it when `profile === "full-pipeline"`.

**Body structure:**

```markdown
## Summary

{1-3 sentence description of what was built and why}

## QA Scores

| Dimension | Score |
|-----------|-------|
| Completeness | {N}/10 |
| Correctness | {N}/10 |
| Code Quality | {N}/10 |
| Coverage | {N}/10 |

{If review was run, add review scores:}

| Review Dimension | Score |
|------------------|-------|
| Logic | {N}/10 |
| Security | {N}/10 |
| Quality | {N}/10 |
| Architecture | {N}/10 |
| Performance | {N}/10 |

## Decision Traceability

| Decision | Description | Outcome |
|----------|-------------|---------|
| {id} | {what was decided} | {chosen option and rationale} |

{If no decisions were recorded: "No gray areas encountered -- implementation followed plan directly."}

## Commits

| Commit | Subtask | Description |
|--------|---------|-------------|
| `{sha}` | #{n} | {description} |

## Review Findings

{If review was run:}
- Critical: {n}
- Warnings: {n}
- Suggestions: {n}
- Nitpicks: {n}
- Auto-fixed: {n}

{If no review: "No code review was run for this task."}

---

Built with [Aperant Framework](https://github.com/Mikalsen-AI/aperant-cloud-desktop/tree/main/packages/framework)
```

## 4. Determine Ship Mode

Read `.aperant/config.json` for `project_type`.

**If `project_type` is `library` or `cli-tool`:**
- Ask the user: "This is a {project_type}. Create a PR, publish to npm, or both?"
- **If publish:**
  1. Check `package.json` for current version
  2. Suggest version bump: patch (bug fixes), minor (new features), major (breaking changes)
  3. Run `npm version {bump}` (or `pnpm version {bump}`)
  4. Run `npm publish` (or `pnpm publish`)
  5. Report: "Published {package}@{version} to npm"
- **If PR:** Proceed to Step 5
- **If both:** Publish first, then create PR

**If any other `project_type`:** Proceed directly to Step 5.

## 5. Sync Team Bookkeeping (before PR)

Before creating the PR, commit any uncommitted team bookkeeping files so teammates stay in sync. This runs **regardless of whether a PR is being created** and only touches an allowlisted set of files under `.aperant/` (never source code).

```bash
node packages/framework/bin/apt-tools.mjs team sync . --push
```

Behaviour:
- If team mode is disabled, this is a no-op (`status: "skipped"`).
- If nothing needs syncing, this is a no-op (`committed: false`).
- Otherwise it stages `.aperant/tasks/INDEX.md`, `.aperant/team.json`, and any dirty `.aperant/roadmap/*.md|*.json`, commits with `chore(aperant): sync team bookkeeping` + an `Aperant Framework` co-author trailer, and pushes. Per-agent `.aperant/team-status/{agent-id}.json` records are gitignored and intentionally not synced.
- The push is best-effort — if it fails (e.g. protected branch), the result includes `push_error` but the commit still exists locally.

Do **not** skip this step when publishing a package without a PR; the commit is the point, not the PR.

## 6. Create PR

Check prerequisites:
```bash
# Verify gh is installed
which gh || echo "ERROR: GitHub CLI (gh) is required. Install: https://cli.github.com/"

# Verify authentication
gh auth status || echo "ERROR: Run 'gh auth login' first"
```

Handle branch state. First resolve the active task's worktree/branch metadata:

```bash
node packages/framework/bin/apt-tools.mjs task get . --id {task_id}
```

Parse `worktree_path`, `branch`, and `base_branch` from the result.

- **If the task record has `branch` and `worktree_path` (task-level worktree isolation active):**
  ```bash
  if [ "$behind" = "0" ]; then
    git -C {worktree_path} push -u origin {branch}
  else
    # §0.7 rebased — lease against the pre-rebase remote tip so a concurrent
    # advance of origin/{branch} between §0.7 and §6 trips the lease instead
    # of silently overwriting the racing party's commit.
    git -C {worktree_path} push --force-with-lease=refs/heads/{branch}:$remote_tip_sha_pre_rebase origin {branch} || {
      echo '[APT:ship] Cannot ship — remote-advanced: origin/{branch} moved between §0.7 audit and §6 push. Re-run /apt:ship.' >&2
      exit 1
    }
  fi
  gh pr create --title "{title}" --body "{body}" --base {base_branch} --head {branch}
  ```
  The PR targets the recorded `base_branch` (the branch the task was forked from), NOT `main`. This is the correct target when the user is shipping an intermediate milestone onto a longer-lived feature branch. The lease form is used only when §0.7 reported `behind > 0` (history was rewritten); on `behind === 0` a plain push is correct.

- **If the task record has `branch` but no `worktree_path`:** Push the branch from the current cwd and create the PR against `base_branch`:
  ```bash
  if [ "$behind" = "0" ]; then
    git push -u origin {branch}
  else
    git push --force-with-lease=refs/heads/{branch}:$remote_tip_sha_pre_rebase origin {branch} || {
      echo '[APT:ship] Cannot ship — remote-advanced: origin/{branch} moved between §0.7 audit and §6 push. Re-run /apt:ship.' >&2
      exit 1
    }
  fi
  gh pr create --title "{title}" --body "{body}" --base {base_branch} --head {branch}
  ```

- **If no task branch is recorded and we're on main/master:** Legacy behaviour — create a feature branch first. §0.7 only ran when a task record exists; this path is reached only on ad-hoc shipping and the rebase gate is necessarily skipped, so a plain push is correct (no rewritten history to lease against):
  ```bash
  git checkout -b {task-slug}
  git push -u origin {task-slug}
  gh pr create --title "{title}" --body "{body}"
  ```

- **If no task branch is recorded and we're already on a feature branch:** Legacy behaviour — push HEAD and open a PR against `main`. When §0.7 ran (task record present), the lease form applies when `behind > 0`; on the ad-hoc / no-task-record path the gate is skipped and a plain push is correct:
  ```bash
  if [ -n "${remote_tip_sha_pre_rebase:-}" ] && [ "$behind" != "0" ]; then
    git push --force-with-lease=refs/heads/$(git rev-parse --abbrev-ref HEAD):$remote_tip_sha_pre_rebase origin HEAD || {
      echo '[APT:ship] Cannot ship — remote-advanced: branch tip moved between §0.7 audit and §6 push. Re-run /apt:ship.' >&2
      exit 1
    }
  else
    git push -u origin HEAD
  fi
  gh pr create --title "{title}" --body "{body}"
  ```

Capture the PR URL from the output into `pr_url` (for example, the last line of `gh pr create` stdout). Section 7.1 references `$pr_url` when parsing the PR number for the post-ship review hint, so binding the name here at capture time keeps the two sections coherent.

**Record `pr_url` on the task record (C56 A2).** `/apt:close-task` and the auto-close-phase path in `task close` both resolve the PR number from this URL to poll `gh pr view` for merge state — without it, the closer side cannot run:

```bash
ship_update=$(node packages/framework/bin/apt-tools.mjs task update . --id {task-id} --pr-url {PR_URL})
```

This same call **atomically flips `lifecycle_phase` from `reviewing` to `shipped-pending-merge`** when the task is in `reviewing` and no explicit `--lifecycle-phase` flag is passed. Section 6 is now the single load-bearing step that both records the PR and transitions the lifecycle; Section 8 below documents the semantics but no longer issues a separate command.

It also evaluates `ci_watch.after_ship` and may arm the post-ship CI watcher. Preserve the JSON envelope in `ship_update`; Section 7.2 reads `ci_watch_armed` and `ci_watch_skip_reason` from it instead of issuing a second auto-start command.

Display the resolution so users see which branch/base was used:

```
[APT:ship] task branch: {branch}
[APT:ship] base branch:  {base_branch}
[APT:ship] worktree:     {worktree_path or "(none)"}
[APT:ship] pr_url:       {PR_URL}
```

## 7. Report

Print the ship summary:

```
Shipped: {PR URL}
Title: {title}
QA: completeness={n} correctness={n} quality={n} coverage={n}
Review: logic={n} security={n} quality={n} architecture={n} performance={n}
Commits: {count}
Review findings: {count by severity or "none"}
Decisions traced: {count}
```

### 7.1 Post-ship /apt:pr-review hint

After printing the ship summary, conditionally emit a tip suggesting `/apt:pr-review` for a deep multi-agent review of the freshly-opened PR. The hint is controlled by `pr_review.suggest_after_ship` in `.aperant/config.json` (tri-state: `always` | `ask` | `never`; default when absent: `always`).

**Skip entirely** when no PR URL was captured in Section 6 — the library `npm publish`-only path (Section 4) ships to npm without opening a PR, so there is no PR to review.

**Parse the PR number** from the captured PR URL using the regex `/pull/(\d+)` (Bash: `pr_number=$(echo "$pr_url" | sed -nE 's|.*/pull/([0-9]+).*|\1|p')`). If the regex fails to match (malformed URL, capture missed), skip the hint and proceed to Section 8.

**Read the mode from config:**
```bash
suggest_mode=$(jq -r '.pr_review.suggest_after_ship // "always"' .aperant/config.json)
```

**Autonomy interaction** (apply BEFORE branching on `suggest_mode`): read the task's autonomy level from `.aperant/state.json` (`active_tasks[{task-id}].autonomy`, falling back to `autonomy.default`). When `autonomy === 3` (YOLO), degrade `ask` → `always` — one fewer prompt in unattended auto-chains. Autonomy 0-2 preserve `ask` unchanged (0-1 stay supervised/guided; 2 was opted into non-destructive auto-chaining but one last stop before a deep review is reasonable).

**Branch on the effective `suggest_mode`:**

- **`always`** — emit two banner lines matching the `:261-263` shape:
  ```
  [APT:ship] Tip: run a deep multi-agent review with: /apt:pr-review --pr {pr_number}
  [APT:ship] (disable or change to ask/never via /apt:setup → "PR Review Auto-Suggest")
  ```

- **`ask`** — emit the structured question:
  ```
  [APT:ship] Run /apt:pr-review --pr {pr_number} now for deep multi-agent review? [Y/n]
  ```
  On `Y` (or empty enter), invoke `/apt:pr-review --pr {pr_number}` inline — the task has already been flipped to `shipped-pending-merge` atomically by the `--pr-url` recording in Section 6, so the deep-review hint does NOT need to block on Section 8. On `N` or any other response, proceed silently to Section 8.

- **`never`** — silent. Proceed directly to Section 8.

`suggest_after_ship` is purely additive — it does not affect `authorship_override`, `authorship_overrides`, or `max_rounds` in the same `pr_review` block.

### 7.2 Post-ship /apt:watch-ci hook

After Section 7.1's hint resolves (whether it ran the deep review or skipped), report or interactively arm the post-ship CI watcher. The hook is controlled by `ci_watch.after_ship` in `.aperant/config.json` (tri-state: `auto` | `ask` | `never`). The default depends on host capability — `auto` on cron-capable hosts (Claude Code, Pi), `never` elsewhere.

**Skip entirely** when no PR URL was captured in Section 6 (library `npm publish`-only path — same skip rule as 7.1).

**Read the Section 6 task-update envelope first:**

```bash
ci_watch_armed=$(echo "$ship_update" | jq -r '.ci_watch_armed // false')
ci_watch_skip_reason=$(echo "$ship_update" | jq -r '.ci_watch_skip_reason // empty')
ci_watch_host=$(echo "$ship_update" | jq -r '.ci_watch.host // "unknown"')
ci_watch_deadline=$(echo "$ship_update" | jq -r '.ci_watch.deadline // empty')
ci_watch_scheduler_handle=$(echo "$ship_update" | jq -r '.scheduler_handle // empty')
host_cron=$(node packages/framework/bin/apt-tools.mjs host-detect | jq -r '.host.capabilities.cron')
```

**Autonomy interaction** (apply BEFORE branching, same rule as 7.1): when `autonomy === 3` (YOLO), degrade `ask` → `auto`.

**Branch on the task-update envelope** (FRAMEWORK-BUG-018 — Track 1: distinguish state-recording from wake-registration):

- **`ci_watch_armed=true` AND `host_cron=true` AND `ci_watch_scheduler_handle` non-empty** — emit:

  ```
  [APT:ship] CI watcher armed (host cron: {ci_watch_host}). Deadline: {ci_watch_deadline}. Stop: /apt:watch-ci --stop --pr {N}.
  ```

  This is the only honest "armed" case — a scheduler handle proves the wake is registered.

- **`ci_watch_armed=true` AND `host_cron=true` AND `ci_watch_scheduler_handle` empty** — emit:

  ```
  [APT:ship] CI watcher state created (host: {ci_watch_host}). Register a wake-up to drive ticks: CronCreate every ~3min running '/apt:watch-ci --tick --pr {N}'. Deadline: {ci_watch_deadline}. Stop: /apt:watch-ci --stop --pr {N}.
  ```

  State is recorded but no scheduler is registered — the orchestrator MUST register a wake or ticks will never fire. Track 2 (auto-wake on arm) will populate `scheduler_handle` and route into the first branch instead.

- **`ci_watch_armed=true` AND `host_cron=false`** — emit:

  ```
  [APT:ship] CI watcher state created (host has no cron; advance manually: /apt:watch-ci --tick --pr {N}).
  ```

  This is the honest-gap surface per D-03 — we never lie about cron-capability.

- **`ci_watch_skip_reason=ask-deferred`** — emit:

  ```
  [APT:ship] Arm CI watcher for PR {N} now? [Y/n]
  ```

  On `Y` (or empty enter), call:

  ```bash
  arm_envelope=$(node packages/framework/bin/apt-tools.mjs ci-watch start . --pr {pr_number} --task-id {task-id} --host {cli})
  ci_watch_scheduler_handle=$(echo "$arm_envelope" | jq -r '.scheduler_handle // empty')
  ```

  Then route into the same four-way logic above — branch 1 (armed with handle), branch 2 (state-created, needs wake), or branch 3 (host has no cron). On `N` or any other response, proceed silently to Section 8.

- **`ci_watch_skip_reason=disabled`** — silent. Proceed directly to Section 8.

- **Any other skip reason** — emit:
  ```
  [APT:ship] CI watcher not armed: {ci_watch_skip_reason}
  ```

Section 7.2 is purely additive — it does not affect Section 7.1's `pr_review.suggest_after_ship` policy or any other config block.

## 8. Transition lifecycle to `shipped-pending-merge` (C56 B5)

Ship is NOT the final closer. The PR has just been opened — it won't merge for minutes, hours, or days, and no agent will be present on merge. The task lifecycle is now `shipped-pending-merge` — the intermediate state between `reviewing` and `completed`. `/apt:close-task` runs post-merge (either manually via `/apt:close-task --all` or by the user when they return to the repo) and performs the actual closeout: phase flip, documentation narration, worktree GC, per-PR config strip, task archive.

Section 6 already performed this transition atomically as part of `task update --pr-url`. This section documents the semantics; no separate command is required.

The task stays in `state.active_tasks[*]` with `lifecycle_phase: "shipped-pending-merge"`. Do NOT call `task close` here — that would mark it `completed` before the PR is actually merged, producing the same artifact drift C56 was built to eliminate.

The linked phase (if any) stays `in-progress` — it only flips to `completed` when `/apt:close-task` confirms via `gh pr view` that the PR's state is `MERGED`.

**Documentation narration moved.** Any ROADMAP.md "Recent activity" prose updates run from `/apt:close-task` on confirmed merge, not from ship. The "shipped" moment is not the "landed" moment — and a landed-work claim belongs with the closer, not the opener.

</process>

<autonomy_interaction>
## Autonomy Levels

| Level | Behavior |
|-------|----------|
| 0-1 | Show full PR body preview. Ask for approval before creating the PR. Allow edits to title and body. |
| 2 | Create the PR automatically. Show the result (URL, title, scores). |
| 3 | Create the PR silently. Show only the PR URL. |

For library/cli-tool publish flow: always ask regardless of autonomy level (publishing to npm is an elevated-risk action per design principle 1.5 -- risk class overrides autonomy).
</autonomy_interaction>
