# Valven delivery rules across the phases

<!-- toc -->
- [Phase 1](#phase-1)
- [Phase 2](#phase-2)
- [Phase 3](#phase-3)
- [Phase 4](#phase-4)
- [Development Behaviors](#development-behaviors)
- [Old work](#old-work)
<!-- /toc -->

The `valven-rules` skill holds the rules in `references/rules.yml`; `valven-gate.mjs` measures them and `valven-split.mjs` turns an oversized branch into stacked pull requests. This file is the per-phase contract the phase docs point at.

The skill ships in `ai-common-toolkit`, the always-on plugin, so the rules reach everyone who installs the marketplace, with or without the pipeline. Claude Code loads it from the plugin (`ai-common-toolkit:valven-rules`); the installer copies it into the skills tree on Copilot CLI and Codex. The gate reads the registry installed with it, so its evaluators and its rules come from one release: `multi-agent-refs/registries/valven-rules/` on Claude Code (laid down by the installer outside the skills dir, so the host does not load the skill twice), the skills tree on Copilot CLI and Codex, the source in a checkout, and the plugin copies only when none of those exists. `skill-conformance.mjs` finds the judgement rules in the plugin on Claude Code and falls back to the same registries dir. The gate prints the path it used as `rulesPath`; when none is found it exits 2, which every step below treats as "could not measure", never as a pass.

## Phase 1

Work is built on one branch and shipped as small pull requests. Each task also gets an estimated line count (added + removed, tests included, generated output excluded), and the plan groups tasks into **PR slices** that each stay under the `VLV-SIZE-01` budget (`prefs.global.valven.thresholds`, default 250 lines and 25 files). Order the slices the way `valven-split.mjs` will cut them: renames and moves first, then data (models, services, repositories), then domain (view models, use cases), then UI, each slice carrying its own tests. Show the slice table in the plan (`slice`, `tasks`, `est. lines`) so the user approves the stack together with the plan. A plan whose total estimate exceeds the budget and shows no slices is incomplete. The estimate is advisory; Phase 4 Step 2.95 measures the real diff and splits by it.

## Phase 2

Gate 6 of the exit gate, after the secret scan:

```bash
node $HOME/.claude/scripts/valven-gate.mjs --mode dev --repo "$WORKTREE" --base "origin/$BASE_BRANCH" --worktree \
  --out "$WORKTREE/.pipeline/valven-dev.json" >/dev/null
```

**Gate 6** (valven-rules, dev mode). Exit 2 blocks the phase: the rules could not be read or git failed. Exit 1: `VLV-TEST-01` (source changed, no test changed) and `VLV-TRACE-01` (no issue key in the branch) are fixed here before Review. `VLV-SIZE-01`, `VLV-SIZE-02` and `VLV-SCOPE-01` are not fixed by editing: keep building on this branch, set `state.valven.splitRequired = true`, and Phase 4 Step 2.95 splits it into stacked PRs. `VLV-REWORK-01` is reported to the user with the ratio and continues. Contract: the `valven-rules` skill.

## Phase 3

Step 1.762, after the owned-path gate. The PR body and the pull request do not exist yet, so `VLV-PRDESC-01`, `VLV-CYCLE-01`, `VLV-CYCLE-02`, `VLV-CYCLE-03` and `VLV-PICKUP-01` report `not-measured`; Phase 4 and `/multi-agent:review` measure them.

```bash
VG_FILE="$WORKTREE/.pipeline/valven-gate.json"
node $HOME/.claude/scripts/valven-gate.mjs --mode review --repo "$WORKTREE" --base "origin/$BASE_BRANCH" \
  --out "$VG_FILE" >/dev/null
VG_RC=$?
[ "$VG_RC" = "2" ] && HALT "valven gate could not run  -  see stderr"
$HOME/.claude/scripts/log-metric.sh "$TASK_ID" 3 review.valven \
  findings="$(jq -r '.findings | length' "$VG_FILE")" split="$(jq -r '.splitRequired // false' "$VG_FILE")"
```

Exit 1 is findings, not an error: they merge at Step 3.0 with `tag: valven`, and `review-decision-gate.mjs` counts them as gate-backed, so a single-source one is not downgraded. Triage accepts a `criteriaSource: "valven-rules"` finding unless the task is exempt, never rejects it as style, and keeps its `line: 0`, which `post-pr-review.sh` posts as a top-level comment. When `splitRequired` is true the fix is never "edit the code"; Phase 4 Step 2.95 splits the branch. With `prefs.global.valven.enabled = false` the file still lands with `enabled: false` and no findings.

## Phase 4

```bash
VG_COMMIT="$WORKTREE/.pipeline/valven-commit.json"
node $HOME/.claude/scripts/valven-gate.mjs --mode commit --repo "$WORKTREE" --base "origin/$BASE_BRANCH" --out "$VG_COMMIT" >/dev/null
VG_RC=$?
```

- Exit 2 halts: the gate could not measure, and pushing on an unmeasured branch is the silent pass this step exists to prevent.
- Exit 1 without `splitRequired`: fix in place. `VLV-COMMIT-01` and `VLV-TRACE-02` are squash and reword of this branch's own commits (`git rebase -i` is not available; use `git reset --soft "$(git merge-base HEAD origin/$BASE_BRANCH)"` and re-commit with the step 6 convention). `VLV-TEST-01` and `VLV-TRACE-01` return to Phase 2. Re-run the gate.
- `splitRequired: true`: plan the stack and show it.

  ```bash
  node $HOME/.claude/scripts/valven-split.mjs --repo "$WORKTREE" --base "origin/$BASE_BRANCH" --out "$WORKTREE/.pipeline/valven-split.json"
  ```

  Render one row per slice (branch, lines, layers, files, `oversized`) and ask (`question` / labels in `outputLanguage`, `header` English `"Split PR"`): option 1 "Split into N stacked PRs" (Recommended), option 2 "Ship as one PR" (the review will mark it NEEDS WORK), option 3 "Pause". Autopilot takes option 1. On option 1 re-run with `--apply`; exit 1 (the stack's top differs from the branch) halts with `drift[]` shown. Then build every slice branch with the Phase 2 Gate 1 command, in order; a slice that fails is folded into the next one: delete the `<branch>-p*` branches (`--apply` refuses to overwrite them), re-run with `--budget` raised to the two slices' combined lines, and build again. The original branch is never modified, so every re-run starts from the same tree. An `oversized` slice is one file larger than the budget: report it, it cannot be split by file.

  After a split, steps 7 and 8 run once per slice in order: push `<branch>-pK`, open its PR with base `$BASE_BRANCH` for p1 and `<branch>-p(K-1)` for the rest, title suffix `(K/N)`, and a `## Stack` section in each body listing every slice PR with the current one marked. The original branch stays local. Record `state.valven.split = {sliceCount, branches[], prs[]}`.

Log: `Phase 4 Step 2.95: valven {pass|fixed|split N|shipped-whole}`

The commit-mode report also scores `VLV-CYCLE-01`, from the oldest author date among the commits about to be pushed to now. Phase 4 commits right before the push, so this is what Valven will read from the pushed history, and it is a `suggestion` that never blocks the push. The rule carries information on a pull request whose commits were pushed as they were made, which `/multi-agent:review` measures.

### Opening the pull request

Every gate has passed before the push, so the pull request opens ready for review with the default reviewers attached, and the draft/ready prompt recommends READY. A draft delays the reviewers' notification, and `VLV-PICKUP-01` flags a draft older than 3 hours on the next review.

### PR body

**Valven body check (required, every PR).** After the body is written to `/tmp/pr-body-$TASK_ID.md` and before the PR is created or updated:

```bash
node $HOME/.claude/scripts/valven-gate.mjs --mode review --repo "$WORKTREE" --base "origin/$BASE_BRANCH" \
  --pr-body-file "/tmp/pr-body-$TASK_ID.md" --out "$WORKTREE/.pipeline/valven-body.json" >/dev/null
jq -r '.findings[] | select(.ruleId == "VLV-PRDESC-01") | .issue' "$WORKTREE/.pipeline/valven-body.json"
```

Any `VLV-PRDESC-01` line blocks creation: add the missing why / how-to-verify text and re-run. Other findings here were already handled at Step 2.95.

## Development Behaviors

`VLV-CYCLE-01` (coding: first commit to the pull request opening), `VLV-CYCLE-02` (pickup: ready for review to the first review) and `VLV-CYCLE-03` (review: first review to approval or merge) are scored into Valven's bands, reported as `results[].score` and collected in `scores`; `VLV-REWORK-01` adds the churn score, `100 - churn %`. A duration in the 50 or 0 band is a `suggestion`, because the time has passed and the pull request cannot change it. `VLV-PICKUP-01` is `important` when the pull request has no reviewer, which the author fixes now; a draft older than 3 hours is a `suggestion` (`draft_severity`), because an unattended run opens a draft on purpose and a person decides when it is ready.

The durations need the pull request's timeline, passed with `--pr-meta-file`. On GitHub, `pr-timeline.mjs --repo <owner/repo> --pr <N> --out <file>` writes it: `gh pr view --json` (head and base branch, body, commits, author, open, close and merge times, draft state, review requests, reviews) plus two things only GitHub's GraphQL timeline carries, the time a draft was marked ready (`readyAt`) and each review author's type, so a bot such as Copilot's reviewer is not counted as a person. When that query fails the script keeps the `gh pr view` output and says on stderr that pickup then counts from the opening and bots are not recognised; the summary repeats it.

A Bitbucket caller writes `{createdAt, readyAt, isDraft, reviewerCount, firstReviewAt, approvedAt, mergedAt, closedAt}` in ISO 8601: `createdDate`, `draft` and the close time of a declined pull request from the pull request, `reviewerCount` the length of `reviewers[]`, `firstReviewAt` the earliest `COMMENTED`, `REVIEWED` or `APPROVED` activity by someone other than the author, `approvedAt` the earliest `APPROVED`; a field it cannot read is null. Bitbucket records no ready-for-review time, so its `readyAt` is null.

The gate leaves out reviews by the author, by bots and pending reviews, and a review left before the pull request was ready: pickup and review time start at ready. An unknown reviewer count stays unknown (`VLV-PICKUP-01` reports it `not-measured`, never "no reviewer"), a draft is not asked for a reviewer, and on a merged or closed pull request the clocks stop there and `VLV-PICKUP-01` does not apply. A file that does not parse, or has no valid `createdAt`, is reported on stderr and the timeline rules report `not-measured`; every other rule is still measured.

## Old work

`VLV-AGE-01` (a commit older than 14 days, by author date so a rebase does not hide it), `VLV-BEHIND-01` (more than 50 commits behind the base, or a merge-base older than 14 days) and `VLV-TRACE-03` (commits carrying another issue's key) mean the branch carries old or foreign work. The answer is never a rebase of the old history: the change moves.

```bash
node $HOME/.claude/scripts/valven-port.mjs --repo "$WORKTREE" --base "origin/$BASE_BRANCH"            # plan
node $HOME/.claude/scripts/valven-port.mjs --repo "$WORKTREE" --base "origin/$BASE_BRANCH" --apply    # move
```

`--apply` cuts `<branch>-v2` (next free `-vN`) from the current base tip, applies the net diff with `git apply --3way` as one commit, and returns to the original branch, which it never modifies. Exit 1 is a conflict: the new branch is deleted, `conflicts[]` names the paths, and they are resolved by hand on a fresh branch. `sharedWithBase[]` lists the paths the base also changed; those are the ones to re-test.

- **Phase 0.** A task that resolves to an existing branch runs `valven-gate.mjs --mode dev` against it first. An old-work finding means work starts on the ported branch, not on the old one.
- **Phase 4 Step 2.95.** Old-work findings are handled before the size check: port, re-run the gate on the new branch, then split if a size rule still fails. A `VLV-TRACE-03` commit is removed from the port (`--head` at the commit before it, or a cherry-pick of the rest) and gets its own branch.
- **Review.** The finding's `fix` is concrete: the `valven-port.mjs` plan (new branch name, commits dropped, `sharedWithBase[]`), then "open the new PR from `<to>`, close this one with a link to it". For `VLV-TRACE-03` it lists the foreign commits. Accepted findings make the review NEEDS WORK.
