# Feature Spec: Runtime-Selectable Ready-to-Merge Branch Handoff (AOP)

> **Purpose of this document**: Define how an operator can choose, at `ready_to_merge` time, between merging into the configured base branch or publishing the feature worktree to an operator-selected branch from the CLI or dashboard.

**Version:** 1.0  
**Date:** 2026-03-19  
**Status:** Draft  
**Roadmap Mapping:** M53

---

## 0. Scope and Standards

### 0.1 Required Standards

All implementation derived from this spec MUST preserve:

- the existing lifecycle invariant that `ready_to_merge` is the final pre-merge state and only merge-specific logic may act on it
- deterministic gate enforcement and normalized `{ ok, data/error }` envelopes
- lock/index/state atomicity
- explicit merge control; no silent auto-merge to base branch
- backward compatibility for existing `aop merge` and dashboard merge review flows when branch handoff is not used

### 0.2 Required Inputs

Implementing agents MUST read:

- `apps/control-plane/src/application/services/merge-service.ts`
- `apps/control-plane/src/application/services/repo-operations-service.ts`
- `apps/control-plane/src/application/services/feature-lifecycle-service.ts`
- `apps/control-plane/src/application/kernel-tool-wiring.ts`
- `apps/control-plane/src/core/kernel.ts`
- `apps/control-plane/src/core/constants.ts`
- `apps/control-plane/src/core/error-codes.ts`
- `apps/control-plane/src/cli/types.ts`
- `apps/control-plane/src/cli/cli-argument-parser.ts`
- `apps/control-plane/src/cli/command-catalog.ts`
- `apps/control-plane/src/interfaces/cli/bootstrap.ts`
- `agentic/orchestrator/tools/catalog.json`
- `agentic/orchestrator/tools/schemas/input/feature.ready_to_merge.input.schema.json`
- `agentic/orchestrator/tools/schemas/output/feature.ready_to_merge.output.schema.json`
- `agentic/orchestrator/schemas/state.schema.json`
- `packages/web-dashboard/src/lib/orchestrator-tools.ts`
- `packages/web-dashboard/src/app/api/actions/route.ts`
- `packages/web-dashboard/src/app/api/features/[id]/review/route.ts`
- `packages/web-dashboard/src/app/api/features/[id]/triage/route.ts`
- `packages/web-dashboard/src/components/focus/FocusLayout.tsx`
- `packages/web-dashboard/src/components/focus/tabs/ReviewTab.tsx`
- `packages/web-dashboard/src/components/dashboard/triage-drawer.tsx`
- `packages/web-dashboard/src/lib/aop-client.ts`
- `packages/web-dashboard/src/lib/focus-detail-derivations.ts`
- `packages/web-dashboard/src/lib/types.ts`

### 0.3 Out of Scope

This spec does **not** implement:

- automatic PR creation after branch publish
- automatic transition to `merged` when a remote PR merges outside AOP
- branch naming templates at feature-init time
- a new lifecycle state such as `handed_off` or `published`
- remote-only merge queues or hosted SCM governance

### 0.4 Current Repo State Audit

As of 2026-03-19, the current implementation behaves as follows:

1. Feature worktrees are created on a branch named exactly after `feature_id`.
2. `feature.ready_to_merge` is hardcoded to:
   - commit the feature worktree
   - checkout the configured base branch
   - merge the feature branch into the base branch
3. Dashboard review only exposes merge-gate execution plus approve/deny/request-changes; it has no destination selector or branch-publish action.
4. Dashboard and run-monitoring surfaces already prefer `worktree_branch` over `branch` if present, which is a useful existing seam for representing an operator-selected branch.

This means the system already has isolated per-feature branches, but it does **not** have a first-class concept of “publish this ready-to-merge result to an operator branch and stop before merging into base.”

---

## 1. Problem Statement

The requested workflow is:

1. A feature reaches `ready_to_merge`.
2. The operator reviews the result.
3. Instead of forcing the worktree back into local `main`, the operator may choose:
   - **Merge to base branch** using the current deterministic merge path, or
   - **Publish to branch** using a branch name they choose at runtime.

The existing system has no clean way to represent that decision:

- `aop merge` is semantically and technically base-branch merge only.
- `feature.ready_to_merge` returns `status: merged`, which is wrong for a published-but-not-merged feature.
- The dashboard Review tab only models merge approval, not delivery target selection.
- Dependency scheduling only unblocks on real `merged`, so a published branch must not be treated as equivalent to merged.

---

## 2. Design Summary

### 2.1 Core Decision

Introduce a **new delivery action** rather than overloading merge:

- new MCP tool: `feature.publish_branch`
- new CLI command: `aop publish`
- new dashboard review action: `review.publish_branch`

`aop merge` and `feature.ready_to_merge` remain base-branch merge operations.

### 2.2 Runtime Selection Model

Runtime selection is exposed as:

- **CLI**: operator chooses `aop merge` or `aop publish`
- **Dashboard**: operator chooses a delivery target in the Review tab:
  - `Merge to base branch`
  - `Publish to branch`

This keeps command semantics honest while still making the choice runtime-selectable.

### 2.3 Lifecycle Decision

Publishing to a branch does **not** change the feature to `merged`.

Normative behavior:

- `ready_to_merge + publish_branch` leaves the feature in `ready_to_merge`
- downstream dependency promotion remains gated on actual `merged`
- auto-cleanup remains tied to actual merge only

### 2.4 Source Branch Decision

After a branch publish, later merge operations MUST use the branch resolved from feature state rather than hardcoding `feature_id`.

Canonical resolution order:

1. `frontMatter.worktree_branch`
2. `frontMatter.branch`
3. `featureId`

This same resolution order SHOULD be shared anywhere the repo needs the operator-facing branch identity.

### 2.5 Rejected Alternatives

#### A. Extend `aop merge --destination branch`

Rejected for v1 because:

- it makes `merge` mean “sometimes not merge”
- `feature.ready_to_merge` output/status contract no longer fits the action
- dashboard/API strings become misleading

#### B. Add a new terminal lifecycle state such as `handed_off`

Rejected for v1 because:

- dependency scheduling currently promotes only from `merged`
- issue tracker/reporting/board semantics would all need broader state-machine work
- the simplest safe behavior is to keep the feature in `ready_to_merge`

---

## 3. Functional Requirements

### FR-1 Branch Publish Must Be Operator-Selectable at Runtime

Acceptance criteria:

1. CLI exposes a dedicated `publish` command for `ready_to_merge` features.
2. Dashboard Review UI exposes a delivery target selector when `phase === 'ready_to_merge'`.
3. The operator can choose between base-branch merge and branch publish without editing policy files.

### FR-2 Branch Publish Must Enforce the Same Review Readiness Bar as Merge

Acceptance criteria:

1. The feature must be in `ready_to_merge`.
2. Required gate modes must be `pass`.
3. `merge` gate must be `pass` before publish succeeds.
4. If policy requires user approval, publish also requires the same approval token field used by merge review.
5. Branch publish may reuse the same reconciliation preflight (`repo.reconcile_mainline`) and `remote_sync_strategy` behavior as merge.

### FR-3 Publish Must Not Pretend the Feature Is Merged

Acceptance criteria:

1. `feature.publish_branch` does not write `status: merged`.
2. Index membership is unchanged: feature stays active/ready for review, not moved to `merged`.
3. Cleanup of worktree or feature artifacts is not scheduled as part of publish.

### FR-4 Later Merge Must Use the Published Branch

Acceptance criteria:

1. If a feature has `worktree_branch` or an updated `branch`, `aop merge` uses that branch as the merge source.
2. Ahead-count verification, merge commands, and cleanup use the resolved branch name rather than literal `featureId`.
3. A later merge from a published branch behaves exactly like merge from the original feature branch.

### FR-5 Published Metadata Must Be Invalidated When Review Readiness Is Lost

Acceptance criteria:

1. If `ready_to_merge` is denied, rerouted to QA, or otherwise moved out of review readiness, publish metadata is cleared or explicitly marked stale.
2. The dashboard must not continue to imply the currently reviewed head is still the published branch once fresh edits begin.
3. Reaching `ready_to_merge` again after further work requires an explicit new publish action.

### FR-6 Dashboard Must Surface Published-Branch State

Acceptance criteria:

1. Review tab shows current delivery target, branch name, and publish status.
2. Feature summary/detail surfaces can show that a ready-to-merge feature has already been published to a branch.
3. Recommended-action copy reflects branch-published state (for example, “Open PR or merge externally” rather than “make the merge decision”).

### FR-7 New Action Must Be Authorized Separately

Acceptance criteria:

1. Dashboard authorization can distinguish `dashboard.review.publish_branch` from `dashboard.review.approve`.
2. Existing allow-all adapter remains backward-compatible.
3. Triage/action-state APIs include `publish_branch` capability metadata.

---

## 4. Runtime Contracts

## 4.1 New MCP Tool

Add a new mutating MCP tool:

- `feature.publish_branch`

Catalog requirements:

- `mutating: true`
- `requires_operation_id: true`
- supported roles: `orchestrator`, `system`

### 4.1.1 Input Schema

Add `agentic/orchestrator/tools/schemas/input/feature.publish_branch.input.schema.json`:

```json
{
  "type": "object",
  "required": ["operation_id", "feature_id", "commit_message", "target_branch"],
  "properties": {
    "operation_id": { "type": "string", "minLength": 8, "maxLength": 256 },
    "feature_id": { "type": "string", "minLength": 1 },
    "commit_message": { "type": "string", "minLength": 1 },
    "target_branch": { "type": "string", "minLength": 1 },
    "user_approval_token": { "type": "string" },
    "remote_sync_strategy": {
      "type": "string",
      "enum": ["auto", "skip"]
    },
    "push_remote": { "type": "boolean" },
    "remote_name": { "type": "string", "minLength": 1 }
  },
  "additionalProperties": false
}
```

### 4.1.2 Output Schema

Add `agentic/orchestrator/tools/schemas/output/feature.publish_branch.output.schema.json`:

```json
{
  "type": "object",
  "required": ["feature_id", "status", "published_branch", "head_sha", "pushed_remote"],
  "properties": {
    "feature_id": { "type": "string", "minLength": 1 },
    "status": { "type": "string", "enum": ["ready_to_merge"] },
    "published_branch": { "type": "string", "minLength": 1 },
    "previous_branch": { "type": "string", "minLength": 1 },
    "head_sha": { "type": "string", "minLength": 1 },
    "pushed_remote": { "type": "boolean" },
    "remote_name": { "type": ["string", "null"] }
  },
  "additionalProperties": false
}
```

## 4.2 State Schema Extension

Extend `agentic/orchestrator/schemas/state.schema.json` with an optional `delivery` object:

```json
{
  "delivery": {
    "type": "object",
    "required": [
      "mode",
      "published_branch",
      "head_sha",
      "pushed_remote",
      "remote_name",
      "published_at"
    ],
    "properties": {
      "mode": {
        "type": "string",
        "enum": ["branch_publish"]
      },
      "published_branch": { "type": "string", "minLength": 1 },
      "head_sha": { "type": "string", "minLength": 1 },
      "pushed_remote": { "type": "boolean" },
      "remote_name": { "type": ["string", "null"] },
      "published_at": { "type": "string", "format": "date-time" }
    },
    "additionalProperties": false
  }
}
```

Normative semantics:

- `delivery` is present only while the currently reviewed head is the currently published branch handoff.
- When review readiness is invalidated, `delivery` is removed.
- On actual merge, merge evidence remains authoritative; `delivery` may be removed or ignored after `status: merged`.

## 4.3 Frontmatter Example

```yaml
feature_id: feature_alpha
status: ready_to_merge
branch: eric/feature_alpha
worktree_branch: eric/feature_alpha
delivery:
  mode: branch_publish
  published_branch: eric/feature_alpha
  head_sha: abc123def456
  pushed_remote: true
  remote_name: origin
  published_at: 2026-03-19T23:15:00.000Z
```

---

## 5. Control-Plane Design

## 5.1 Kernel and Tool Wiring

Add:

- `TOOLS.FEATURE_PUBLISH_BRANCH`
- tool registration in `application/kernel-tool-wiring.ts`
- `AopKernel.featurePublishBranch(...)`

`featurePublishBranch(...)` should mirror the merge kernel preflight:

1. resolve base branch
2. honor `remote_sync_strategy`
3. run pre-merge reconciliation when configured for `ready_to_merge` / `merge`
4. delegate to the service method that performs publish

## 5.2 Service Design

The current `MergeService` mixes two concerns:

- preflight and commit preparation
- base-branch merge execution

Implementation SHOULD extract a shared helper inside `merge-service.ts`, for example:

- `prepareDeliveryCandidate(...)`

Shared preflight responsibilities:

1. validate allowed operator action
2. validate `ready_to_merge`
3. ensure required gates passed
4. ensure merge gate passed
5. stage and commit feature worktree if needed
6. resolve source branch from feature state

Then:

- `featureReadyToMerge(...)` continues into checkout-base + merge
- `featurePublishBranch(...)` performs branch rename/push + state update

### 5.2.1 Branch Publish Git Workflow

Normative branch publish workflow:

1. Resolve `currentBranch` from state and/or `git rev-parse --abbrev-ref HEAD`.
2. Validate `targetBranch`:
   - not empty
   - not equal to base branch
   - not detached (`HEAD`)
   - not already owned by another local branch unless it is the current branch
3. If `currentBranch !== targetBranch`, rename the checked-out worktree branch to `targetBranch`.
   - preferred operation: `git branch -m <targetBranch>` executed in the feature worktree
4. If `push_remote === true`, run `git push -u <remote_name> <targetBranch>`.
5. Read `HEAD` SHA.
6. Persist frontmatter/body updates.

### 5.2.2 Branch Validation Errors

Add branch-specific error codes:

- `INVALID_TARGET_BRANCH`
- `TARGET_BRANCH_ALREADY_EXISTS`

Low-level push/rename failures may continue to use `GIT_FAILURE` with structured details.

## 5.3 Merge Behavior After Publish

`feature.ready_to_merge` MUST be updated to resolve merge source branch from feature state.

Normative updates in merge flow:

- ahead-count comparison uses `${baseBranch}...${sourceBranch}`
- merge commands use `sourceBranch`
- cleanup deletes `sourceBranch`, not hardcoded `featureId`

If cleanup is running after a published-branch merge, it SHOULD best-effort delete:

1. resolved source branch
2. the legacy `featureId` branch if distinct and still present

## 5.4 Review-State Invalidation

Any operator action that moves the feature out of current review readiness MUST clear `delivery`.

Minimum required paths:

- dashboard deny flow
- dashboard request-changes / send-back-to-QA flow
- merge-gate repair reroutes
- any future explicit state patch from `ready_to_merge` to `blocked` or `qa`

---

## 6. CLI Contract

## 6.1 New Command

Add:

- `aop publish`

Usage:

```bash
aop publish \
  --feature-id <feature_id> \
  --branch <target_branch> \
  [--approval-token <token>] \
  [--commit-message <msg>] \
  [--push-remote] \
  [--remote <name>] \
  [--sync-remote auto|skip]
```

### 6.1.1 Semantics

- `aop publish` is valid only for `ready_to_merge` features
- it runs the same readiness/reconciliation/merge-gate preflight as merge
- it publishes the reviewed head to the requested branch
- it returns `status: ready_to_merge`

### 6.1.2 Output

CLI response SHOULD include:

```json
{
  "ok": true,
  "data": {
    "command": "publish",
    "feature_id": "feature_alpha",
    "merge_gate_run": true,
    "merge_gate_overall": "pass",
    "reconciliation_status": "up_to_date",
    "published_branch": "eric/feature_alpha",
    "status": "ready_to_merge",
    "pushed_remote": true,
    "remote_name": "origin"
  }
}
```

### 6.1.3 CLI File Updates

Implementation MUST update at least:

- `apps/control-plane/src/cli/types.ts`
- `apps/control-plane/src/cli/cli-argument-parser.ts`
- `apps/control-plane/src/cli/command-catalog.ts`
- `apps/control-plane/src/interfaces/cli/bootstrap.ts`
- `CLAUDE.md`
- `AGENTS.md`

### 6.1.4 Command-Handler Structure

Add `publish-command-handler.ts`.

Implementation SHOULD share preflight code with `merge-command-handler.ts` rather than duplicating readiness/reconciliation logic inline.

---

## 7. Dashboard Contract

## 7.1 Review Tab UX

Update `packages/web-dashboard/src/components/focus/tabs/ReviewTab.tsx` so the operator can choose a delivery target at runtime.

### 7.1.1 Delivery Target Controls

Add a new “Delivery Target” section with:

- radio: `Merge to base branch`
- radio: `Publish to branch`

When `Merge to base branch` is selected:

- show merge strategy select
- primary button label: `Approve & Merge`

When `Publish to branch` is selected:

- show `Target branch` input
- show `Push to remote` checkbox
- show `Remote name` input when push is enabled
- primary button label: `Publish Branch`
- helper copy: “This keeps the feature in ready_to_merge and does not mark it merged.”

### 7.1.2 Review Draft State

Extend `FocusLayout` review draft state with:

- `deliveryTarget: 'merge' | 'publish_branch'`
- `targetBranch: string`
- `pushRemote: boolean`
- `remoteName: string`

This state must be shared between the pinned review surface and the full Review tab in the same way `approvalToken` and `mergeStrategy` are today.

## 7.2 Review API

Update both dashboard review endpoints:

- `POST /api/actions`
- `POST /api/features/[id]/review`

New action/decision:

- `review.publish_branch`
- `decision: 'publish_branch'`

Request shape:

```json
{
  "action": "review.publish_branch",
  "feature_id": "feature_alpha",
  "approval_token": "approve_me",
  "target_branch": "eric/feature_alpha",
  "push_remote": true,
  "remote_name": "origin",
  "commit_message": "Publish feature_alpha to eric/feature_alpha",
  "message": "Reviewed and publishing for external PR flow"
}
```

## 7.3 Action-State and Authorization Surfaces

Extend dashboard action models with:

- `publish_branch`

Required changes:

- `DashboardFeatureActionKey`
- triage/action route action unions
- triage route action-state computation
- authorization action string `dashboard.review.publish_branch`

Enablement rule:

- `feature.phase === 'ready_to_merge'`
- `feature.gates.merge === 'pass'`
- authorized for `dashboard.review.publish_branch`

## 7.4 Published-Branch Status Display

The dashboard must surface published-branch state in the following places:

1. **Focus header / sidebar**
   - show `Published branch: <branch>`
2. **Review tab**
   - info banner when `feature.delivery.mode === 'branch_publish'`
   - include publish timestamp and remote name if present
3. **Board / feature card / list summary**
   - additive chip or badge such as `Published`
4. **Triage drawer**
   - show published branch metadata
   - no prompt-based branch-name collection in the drawer; the detailed publish flow belongs on the Review tab

## 7.5 Recommended-Action Copy

Update focus-detail derivations:

- if `phase === 'ready_to_merge'` and no publish metadata exists:
  - `Review the final evidence and choose merge or branch publish.`
- if `phase === 'ready_to_merge'` and publish metadata exists:
  - `Branch published; open a PR or merge externally when ready.`

## 7.6 Revision and Review History

Extend dashboard revision history types/builders to include:

- `branch_publish_event`

This event should appear in Review Decision History with:

- branch name
- timestamp
- remote if pushed

---

## 8. State and UI Compatibility Rules

### 8.1 Existing Features

Existing features with no `delivery` metadata continue to behave exactly as they do today.

### 8.2 Manually Renamed Branches

If a feature already has `worktree_branch` set, later merge logic should treat that as authoritative even if the new publish flow was not the mechanism that created it.

### 8.3 Publish Does Not Replace Merge

After branch publish:

- the operator may still later run `aop merge`
- dashboard may still later execute merge approval
- the merge source branch is simply the currently resolved branch from state

---

## 9. Implementation Plan

### Slice 1: Control-Plane Branch Publish Path

1. Add `feature.publish_branch` tool, schemas, constants, and kernel wiring.
2. Extend merge service with shared delivery preflight plus publish implementation.
3. Add branch validation and publish-state persistence.

### Slice 2: CLI Runtime Selection

1. Add `aop publish`.
2. Share readiness/reconciliation/merge-gate preflight with `aop merge`.
3. Teach merge path to resolve source branch from state.

### Slice 3: Dashboard Review and Published-State UX

1. Add Review-tab delivery target selector and draft state.
2. Add publish action routing and authorization.
3. Surface published-branch status in focus/board/triage summaries.

---

## 10. Test Plan

### 10.1 Control-Plane Tests

Add or update tests for:

- publish success to a new branch
- publish with `push_remote=true`
- reject publish when not `ready_to_merge`
- reject publish when merge gate is not `pass`
- reject target branch equal to base branch
- reject existing conflicting target branch
- merge after publish uses published branch
- cleanup after merge deletes the published branch
- deny/request-changes clears `delivery`

### 10.2 CLI Tests

Add or update tests for:

- parser support for `publish` flags
- command catalog/help output
- publish handler payload mapping
- merge handler/source-branch resolution after publish

### 10.3 Dashboard Tests

Add or update tests for:

- `/api/actions` dispatch for `review.publish_branch`
- `/api/features/[id]/review` decision `publish_branch`
- triage route action-state includes `publish_branch`
- ReviewTab render/validation for delivery target toggle
- `aop-client` parsing of `delivery`
- feature card / focus header / triage drawer published-state rendering
- recommended-action copy after publish

### 10.4 Validator Expectations

The implementation must continue to pass:

- `npm run validate:mcp-contracts`
- `npm run typecheck`
- relevant Vitest suites for control plane and web dashboard

---

## 11. Acceptance Criteria

This spec is complete when all of the following are true:

1. An operator can publish a `ready_to_merge` feature to a chosen branch from the CLI.
2. An operator can choose merge vs branch publish in the dashboard Review tab at runtime.
3. Publishing does not mark the feature merged or unblock dependent features.
4. Later merge operations correctly use the published branch.
5. Published-branch metadata is visible in the dashboard and is cleared when review readiness is lost.
6. Existing merge behavior remains unchanged when the new publish path is not used.
