# Refine

> Refine a ticket interactively until it is ready for planning.

- Skill: `matteobortolazzo/refine` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add matteobortolazzo/refine`
- Raw SKILL.md: https://api.skillmd.com/api/skills/matteobortolazzo/refine/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: matteobortolazzo (https://skillmd.com/u/matteobortolazzo)
- Updated: 2026-08-19
- Page: https://skillmd.com/skills/matteobortolazzo/refine

---


> **Client dispatch**: In Codex, read `codex-runtime` and `refine/codex.md`, execute that native procedure, and do not continue into the Claude procedure below.

> **Interaction rule**: Every question, confirmation, or approval directed at the user — anywhere in this skill, including error recovery — MUST be asked with the `AskUserQuestion` tool. Never ask in plain text. If an instruction says "ask the user" or "confirm", that means `AskUserQuestion`.

## Context

Read `project-core` and resolve neutral configuration before continuing.

Use the config returned by `project-core`; if none exists, stop with its client-appropriate setup guidance.

**Parse `$ARGUMENTS`:**
The first token is the ticket ID. Everything after it is optional **user context** (additional instructions or focus areas).

Split `$ARGUMENTS` into:
- **Ticket ID**: the first whitespace-delimited token, with any leading `#` prefix stripped.
  For example: `#1 focus on API` → ID `1`, `7` → ID `7`.
- **User context**: everything after the first token (may be empty).
  For example: `42 focus on the API layer` → context is `focus on the API layer`.

**Ticket ID validation**: Validate the parsed ticket ID against `^[0-9]+$`. If it does not match, **stop immediately** — do not run `gh`, do not write any temp file — and tell the user:
"The ticket ID must be numeric (digits only) — got `<value>`. Re-run `/cenci:refine <ticket-id> [additional context]` with the ticket's numeric ID (e.g. `/cenci:refine 350`)."

**Shell rules**: Read the `shell-rules` skill before running any `gh` commands (covers heredoc temp-file pattern).

**Fetch the ticket:**
Extract owner/repo from `git remote get-url origin` (e.g. `git@github.com:owner/repo.git` → `owner/repo`).

**owner/repo validation**: Trim surrounding whitespace from the derived value (e.g. a trailing newline from command output), then validate it against `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$` (exactly one `/`, each segment limited to letters, digits, `.`, `_`, `-`), additionally rejecting either segment being exactly `.` or `..`, or ending in `.git` (GitHub never allows a repo name to end in `.git`, so a trailing `.git` here always indicates a botched extraction, not a real repo name). If it does not match, **stop immediately** — do not run `gh`, do not write any temp file. Before echoing the value below, redact anything up through the last `@` (keep only the text after it, or the whole value if no `@` is present) so a malformed remote URL with embedded userinfo/credentials is never echoed back — then tell the user:
"The derived `owner/repo` value (`<value>`) does not look like a valid `owner/repo`. Check `git remote get-url origin` and verify the remote URL is correctly configured."

Once validated, run:
```bash
gh issue view <number> --repo <owner>/<repo> --json number,title,body,labels,state,assignees,milestone,comments,author
```
Each entry in `comments` now also carries its own `author.login` and `authorAssociation` — the bundle written in step 4 below passes these through per comment so the refiner's Design Coverage Check can restrict which comments' ticket-carried screen node references count toward coverage.

The ticket's **own** author association needs a second read: `gh issue view --json` exposes no top-level `authorAssociation` field (it exists only per comment, inside `comments`), so requesting it there makes the whole fetch above exit non-zero with `Unknown JSON field: "authorAssociation"`. The REST issue endpoint exposes it as `author_association`:
```bash
gh api repos/<owner>/<repo>/issues/<number> --jq '.author_association'
```
If this call fails, treat the ticket's own association as unknown — it is then not one of the accepted values, so a ticket-body screen node reference does not count toward coverage. Never substitute a per-comment `authorAssociation` for it.

**Split-child provenance detection:** Determine whether this ticket is itself a child of an earlier split — mirrors `agents/context-gatherer.md`'s parent-child detection. Primary source is the native sub-issue link:
```bash
gh issue view <number> --repo <owner>/<repo> --json parent --jq '.parent.number // empty'
```
If that returns a number, record `isSplitChild: true` and `parentNumber: <that number>`. **Fallback** (older tickets linked only by convention, or the primary command failing non-zero): if the first non-empty line of the fetched body matches `Related to #<number>`, record `isSplitChild: true` with that number as `parentNumber`. If neither yields a parent, record `isSplitChild: false`. A split child is presumed sized by its parent's refinement — split depth is one (`docs/ticket-sizing.md`) — so this flag drives the bundle's resolved flags (step 4), the **Split-depth guard** in step 9, and the **Oversize split child escalation** at the `## Confirmation Gate`.

## Attachments

Read the `attachments` reference skill and follow its 4-step procedure to discover, present, download, and load ticket attachments. If no attachments are found or the user selects none, proceed to Pre-flight Checks.

Store each image summary alongside its file reference for use during refinement.

## Pre-flight Checks

After fetching the ticket, inspect its current state before proceeding:

Check the issue's `labels` array and `state` field.

If any of these conditions are true, warn the user and ask for confirmation using `AskUserQuestion`:

| Condition | Message |
|-----------|---------|
| Ticket is closed/resolved | "This ticket is closed/resolved. Do you still want to refine it?" |
| Has "Refined" label/tag | "This ticket is already marked as Refined. Do you want to re-refine it?" |
| Has "Planned" label/tag | "This ticket already has a saved plan (Planned). Do you want to re-refine it?" |
| Has "Working" label/tag | "This ticket is currently being worked on. Do you want to re-refine it?" |
| Has "In Review" label/tag | "This ticket has an open PR (In Review). Do you want to re-refine it?" |
| Has "Implemented" label/tag | "This ticket is already marked as Implemented (its PR merged). Do you want to re-refine it?" |

If the user says no → stop. If yes → proceed normally.

## Ownership Inspection (read-only)

Read the `ticket-ownership` reference skill's `## Inspect ownership (read-only)`
section only, and follow it using the assignees from the ticket fetch above — this
performs zero GitHub writes. Record the result as `ownershipState`
(`unowned` | `owned-by-caller` | `conflict`) for the `## Confirmation Gate`
manifest below. The claim itself is deferred to the write phase (`## Update Ticket`
below, `#### Ownership claim (first write)`) — it must not run before the gate's
confirmation. Never replace an existing assignee.

## Your Role

You orchestrate backlog refinement. The judgment-heavy analysis — ambiguity hunting, question drafting, sizing, and the refined ticket proposal — is delegated to the **refiner** agent (spawned via the `Task` tool), whose `model: opus` frontmatter pin holds for its entire run; a skill-level pin only lasts the invoking turn, so it would silently degrade every follow-up turn of the Q&A loop to the session model. You run the interaction and the writes: relay the refiner's questions to the user, feed answers back, and perform every GitHub mutation yourself, all of them after the `## Confirmation Gate`. Never perform the refiner's analysis inline, and never delegate a GitHub write or an `AskUserQuestion` to a subagent (see the `subagent-safety` skill).

## Process

1. **Summarize** the fetched ticket for the user in 2-3 lines (title, current scope, state). Do not analyze it — that is the refiner's job.

2. **Classify ticket type**: Read the `frontend-classification` reference skill and apply its rule to determine if this ticket involves frontend/UI work. Record the result as `isFrontend` for the bundle and the labeling steps.

   **Design-only classification** (if `isFrontend` AND `pencil.enabled` is `true` in `.cenci/config.json`): determine whether the ticket's *deliverable* is the design itself — a `.pen` file plus `DESIGN.md` spec, with no production code change (e.g., "Design the settings page", "Create mockups for the onboarding flow"). If the signals point that way, confirm via `AskUserQuestion`:

   > "This reads as a design-only ticket — the deliverable would be a design spec (`.pen` + `DESIGN.md`) produced by `/cenci:design`, with no code change. Is that right?"

   Options: "Yes — design-only", "No — includes implementation"

   If confirmed, set `isDesignTicket = true`. Design-only tickets are routed to `/cenci:design`, not `/cenci:implement`: they skip the browser question (step 8) and the `ui:visual-check` label (step 12), and receive the `Design` label in step 11. The refiner focuses its analysis accordingly via the bundle's `isDesignTicket` flag — design questions (visual direction, screens, states, design-system fit) instead of implementation-only items.

   The **Design Coverage Check** (`.pen`/`DESIGN.md` evaluation and the `designNeeded` determination) is performed by the refiner agent, not here — its result arrives in the proposal's `### Design Coverage` section (step 7); a conventions-only DESIGN.md is not itself a coverage gap, so `designNeeded` only goes true when there is genuinely no design to work from. **Design always happens on a dedicated design ticket, never on the implementation ticket itself** — when `designNeeded` is true, the design ticket is created later, either as the first child of a split (see **Design-first splits** in step 9) or as a companion ticket (see **Companion design ticket** in the Update Ticket section).

3. **Create the per-run temp-file token.** Run `mktemp -u ${TMPDIR:-/tmp}/cenci/issue-<number>-XXXXXX` once and capture the trailing random segment as `<token>` (the token is the random suffix only, e.g. `a1b2c3` — not the full mktemp basename). As with `<ticket-id-or-slug>` in the implement phases, carry the literal `<token>` value forward as text into every temp-file path for the rest of this run — do NOT re-derive it per Bash call, and do not use `$$`/shell state (it does not persist across separate Bash tool invocations). `-u` is a dry-run name generator — it only produces a unique-ish suffix, not an atomically-created file — which is why the `Write` tool is what actually creates each temp file in this run. The `<token>` is a **collision-avoidance mechanism only** — it reduces the chance of two concurrent runs picking the same temp-file basename — and is explicitly **not** an atomic reservation (a second run could theoretically generate the same suffix before either run's `Write` call lands) and **not** a security boundary (it provides no protection against a malicious or adversarial process targeting the same path).

4. **Write the context bundle.** Use the `Write` tool to create `${TMPDIR:-/tmp}/cenci/issue-<number>-<token>-bundle.md` containing, in order:
   - The **verbatim** ticket title, body, labels, state, and comments from the fetch above — full text, never a digest or paraphrase (the refiner's decisions require source fidelity; see `docs/skill-authoring.md`). Include the ticket's own `author.login` (from `gh issue view`) paired with its `author_association` (from the REST issue-endpoint read) alongside the body, and each comment's `author.login`/`authorAssociation` alongside its body, per the two fetches above.
   - Each attachment's summary alongside its downloaded file path (from the Attachments step).
   - The user context parsed from `$ARGUMENTS`, verbatim (or `None`).
   - The resolved flags: `isFrontend`, `isDesignTicket`, `pencil.enabled`, `pencil.designPath` (from the resolved config), and `isSplitChild` with its `parentNumber` (from the **Split-child provenance detection** in the Context section above; `parentNumber` is omitted when `isSplitChild` is false).

   If the `Write` fails, retry once; if it still fails, STOP and report the error — the refiner must never run without the bundle.

5. **Delegate to the refiner agent** (`Task` tool, agent `refiner`). The first invocation's prompt contains only: the bundle path, a note that this is round 1, and the instruction to read the bundle file in full before analyzing. Later rounds additionally carry the Q&A history (step 6) — do not re-paste ticket or bundle content into any prompt; the bundle path is the context.

6. **Q&A relay loop.** Parse the refiner's `## Questions` section:
   - If it is `None.` → the same output contains the `## Refined Ticket Proposal`; continue to step 7.
   - Otherwise, present the questions to the user **in the refiner's order**. Ask exactly ONE question per `AskUserQuestion` call, and wait for the answer before the next call. Never combine multiple refiner questions into a single `AskUserQuestion` call, never merge them into one composite question, and never ask them as plain text. When the refiner supplied answer options for a question, map them onto the `AskUserQuestion` options using the refiner's wording (the user can always answer freeform via "Other"); otherwise offer sensible options. When the refiner marked one option recommended, map it onto the first `AskUserQuestion` option, appending `(Recommended)` to that option's label and carrying the rationale into its description; this is advisory: a question with no marked recommendation is relayed exactly as today and never triggers a re-invocation of the refiner.
   - When every question in the round is answered, re-invoke the refiner with the bundle path and the **complete** accumulated Q&A history — all rounds, as `Q:`/`A:` pairs using the refiner's original question wording; do not re-paste ticket or bundle content. Route the new output through this step again. The refiner asks at most 4 questions per round and rounds continue until it returns `None.`.

7. **Proposal received.** When the refiner returns `None.`, its output contains the `## Refined Ticket Proposal` — the summary content adopted in step 9 and persisted in steps 10-12. Read `designNeeded` from its `### Design Coverage` section (treat it as false when the section or field is absent).

8. **Before adopting the proposal**, ask one final infrastructure question — but only when it can plausibly apply. **Skip for design-only tickets** (`isDesignTicket` is true) — they never reach the implement pipeline; set `browserRequired: false`. Ask it if the ticket was classified frontend/UI in step 2, **or** the ticket/answers mention web scraping, browser automation, or manual browser testing. For pure backend/infrastructure/data tickets with none of those signals, skip the question and set `browserRequired: false`.

   Using `AskUserQuestion`:
   "Does this story need interactive browser access during implementation? (e.g., for visual verification, form testing, or web scraping). If yes, the implementer should ensure `playwright-cli` is installed (`npm i -g @playwright/cli`)."
   - If **yes** → note `browserRequired: true` for the labeling step
   - If **no** → proceed normally

9. **When refined**, adopt the refiner's `## Refined Ticket Proposal` **verbatim** as the summary content — its sections (`### Updated Title` (optional), `### Updated Description`, `### Acceptance Criteria`, `### Assumptions (auto-adopted)`, `### Decisions`, `### Technical Notes`, `### Design Coverage`, `### Design Direction`, `### Automation`, `### Size Estimate`, `### Suggested Split` with `#### Execution Order`) map 1:1 onto the persistence steps below; the section formats themselves are specified in `agents/refiner.md`. `### Assumptions (auto-adopted)` and `### Decisions` persist into the ticket body in step 10, same as `### Acceptance Criteria`; `### Automation` drives the `## Confirmation Gate`'s per-ticket `automerge:ok` verdict computation (parent and every child) and is never written into the body. Do not rewrite, summarize, or reorder the proposal's content. The proposal itself is rendered to the user at the `## Confirmation Gate` below, before any write — it is not held back until the final message; steps 10-13 persist the ticket update, any split or companion design ticket, and the labels only after that gate's Confirm branch (see the **Final Message** note at the end of the Update Ticket section).

   A `### Suggested Split` in the proposal means each child becomes its own numbered ticket and PR, linked to the parent as a native GitHub sub-issue, with dependency ordering captured in the child bodies (Pass 1/Pass 2 below).

   **Split-depth guard (fail closed).** When `isSplitChild` is true and the adopted proposal nonetheless contains a `### Suggested Split`, STOP immediately — render no Confirmation Gate manifest, ask no confirmation, perform zero GitHub writes — and report that the refiner violated the no-resplit contract for split child #`<number>` (child of #`<parentNumber>`; see `agents/refiner.md`'s **Split children are never split again** rule), and that re-running `/cenci:refine <number>` is how to retry. Split depth is one and grandchild tickets are never created (`docs/ticket-sizing.md`); an L-sized split child is handled by the **Oversize split child escalation** at the `## Confirmation Gate` below, never by another split.

   **Design-first splits** (if frontend feature AND `pencil.enabled` is `true` AND `designNeeded` is true): the proposal's split makes the first child a **design-only ticket** (e.g., "Design <feature> screens") that every UI implementation child depends on. It gets the `Design` label in Pass 1, its body includes the `### Design Direction` section from the proposal (that's where `/cenci:design` reads it from), it is executed via `/cenci:design`, and it produces a committed design spec rather than a PR (the one exception to "1 ticket = 1 PR"). When `/cenci:design` completes it, the `Designed` label is propagated to the implementation children that depend on it, satisfying implement's Design gate.

## Confirmation Gate

Placed here — between Process step 9 (the proposal is adopted) and `## Update Ticket` below — this gate performs **zero** GitHub writes; it only classifies, asks, computes, and renders. **No GitHub write of any kind occurs before** its single confirmation — the ownership claim, the `Working` label, and every ticket/label/sub-issue mutation all wait for it. Steps 10-13 below then consume everything this gate computes; they never recompute a verdict or a label set. A split proposal must also clear the Coverage gate precondition immediately below before this gate renders anything.

#### Coverage gate — verify the split's acceptance-criteria partition

Before rendering a manifest for a proposal carrying a `### Suggested Split`, run the structural completeness check first, the acceptance-criteria partition check second — both within this same gate, both gating the same pre-render stop point.

**Structural completeness check.** For every child block in the adopted `### Suggested Split`, verify all five subsections are present: `### Goal`, `### Decisions`, `### Assumptions (auto-adopted)`, `### Acceptance criteria`, and `### Dependencies`. Each present subsection must also satisfy its emptiness rule: `### Goal` must contain non-empty prose — never a placeholder or blank section; `### Dependencies` must be non-empty ("None." is a valid value when the child truly has no dependencies); `### Decisions` and `### Assumptions (auto-adopted)` must each be non-empty or exactly "None."; `### Acceptance criteria` may be empty only for a child the partition assigned zero criteria (e.g. a design-only first child). If any child is missing a subsection or violates its emptiness rule, STOP before any child creation — render no manifest, ask no confirmation, create no tickets, run no Pass 2 — and report the violating child's `(K/N)` title and the missing or empty section(s) so the split can be corrected in another refinement round. This check only confirms presence/absence and does not itself judge whether an empty `### Acceptance criteria` section is legitimate — the acceptance-criteria partition check below is the sole verifier of correct assignment, and a child wrongly left empty will surface there as an unassigned criterion.

**Acceptance-criteria partition check.** Only after the structural completeness check passes for every child, verify the proposal partitions the parent's acceptance criteria per `agents/refiner.md`'s **Acceptance-criteria partition** rule: every `- [ ]` item in the proposal's `### Acceptance Criteria` must appear in exactly one child's `### Acceptance criteria` checklist (scoped rewording is fine as long as the mapping is evident), and no criterion may appear under two children. If any criterion is unassigned or duplicated, STOP — render no manifest, ask no confirmation, create no tickets, run no Pass 2 — and report the violating criteria to the user so the split can be corrected in another refinement round. Nothing has been written at this point, so stopping here is free; proceeding would mint children whose closure can no longer prove the parent's criteria (#661), or, per the structural check above, whose `Refined` label is not truthful (#872).

**Oversize split child escalation (before rendering).** When `isSplitChild` is true and the adopted proposal's `### Size Estimate` is L, surface the refiner's parent re-partition recommendation before rendering the manifest and ask, via `AskUserQuestion`:
   "This ticket is a split child of #`<parentNumber>` and still sizes L (budget risk). Split depth is one, so it will not be split again — proceed with it as-is, or decline so parent #`<parentNumber>`'s partition can be redone?"
   Options: "Proceed — keep the oversize child as-is" / "Decline — redo the parent's partition". **Proceed** ⇒ continue into the numbered steps below with the proposal unchanged (the L reasoning stays in the persisted body's `### Technical Notes`-adjacent sections exactly as the refiner wrote it). **Decline** ⇒ zero GitHub writes have occurred and none will occur — jump to step 13's declined-cleanup branch, and report that re-running `/cenci:refine <parentNumber>` is how to re-partition the parent. Either way this escalation only asks — it never modifies the proposal, and it never splits: this is the human hand-off for an oversize child, replacing any automatic grandchild creation.

1. **Per-child classification** — only when the adopted proposal carries a `### Suggested Split`. For each proposed child K, apply the `frontend-classification` reference skill to **that child's own block text** (its title, `### Goal`, and `### Acceptance criteria`) — never an inlined keyword list; that skill is the single source of truth. Record `childIsFrontend(K)` and `childVisualCheck(K)` (the visual-check signal subset) per child.

2. **Per-child browser question** — for each child where `childIsFrontend(K)` is true, or whose block mentions web scraping, browser automation, or manual browser testing, ask step 8's question once, scoped to that child, as its own `AskUserQuestion` call:
   "Does child (K/N) `<title>` need interactive browser access during implementation? (e.g., for visual verification, form testing, or web scraping). If yes, the implementer should ensure `playwright-cli` is installed (`npm i -g @playwright/cli`)."
   Ask them in child order (1/N … N/N); never combine two children into one call — mirrors step 6's one-question-per-call rule. **Skip entirely for a design-only child** — design children never reach the implement pipeline, exactly as step 8 skips for `isDesignTicket`, and `childBrowserRequired(K)` is set `false`. A child with no frontend/browser signal is not asked at all and gets `childBrowserRequired(K) = false`.

3. **The parent's step-8 question is independent of every child.** Step 8 stays where it is and is unchanged; its answer applies to the parent ticket only and **is never propagated to any child** — on a split the parent is a tracking epic, so a `browserRequired: true` parent answer must not force `Browser` or a withhold onto a child, and a `false` parent answer must not clear a child's own `true`.

4. **Compute effective verdicts**, per ticket T in {parent, child 1…N}:
   `effective grant(T) = ### Automation verdict for T is exactly "grant" AND NOT isDesignTicket AND NOT browserRequired AND NOT visual-check-signals-match` (each override evaluated for T itself — the parent's own `isDesignTicket`/`browserRequired`/visual-check match for the parent; for a child K, `isDesignTicket(K)` is true only when K is the design-only child produced by a Design-first split (the child seeded with `["Refined","Design"]` in Pass 1) — never derived from or equal to `childIsFrontend(K)` — plus that child's own `childBrowserRequired(K)` and `childVisualCheck(K)`).
   **Fail-closed default preserved**: an absent/empty `### Automation` entry for T, or any value other than exactly `grant`, is `withhold`.

5. **Compute the label set per ticket**: the parent's set is computed per steps 11-12 below (unchanged in shape; steps 11-12 consume the gate-computed values rather than recomputing them); each child's set = inherited non-excluded parent labels + `Refined` [+ `Design` for the design child] [+ `Browser` when `childBrowserRequired(K)`] [+ `ui:visual-check` when `childVisualCheck(K)` and the child is not the design child] [+ `automerge:ok` when its effective grant holds]. **Record the gate-time parent snapshot** — the parent's current `labels` and `milestone`, as fetched for this gate — as the basis for "inherited non-excluded parent labels" above; this is a preview, not a promise. It is reconfirmed at write time by `## Update Ticket`'s **Manifest revalidation** sub-step (run before the first write), which re-fetches the parent and diffs it against this gate-time snapshot. That diff is split into two kinds: **authorization-sensitive drift** — a change in `automerge:ok`, `Browser`, or `ui:visual-check` on the parent between the gate and the write — versus **cosmetic drift** — everything else (milestone, `area:*`, priority, team, `Design`, or any other label). The gate's own `automerge:ok`/`Browser`/`ui:visual-check` computation above is authoritative for what this run computed and does not itself change; it is the parent's *current remote state* that may have moved, which is exactly what the revalidation sub-step exists to catch.

6. **Render** the complete adopted proposal, followed by a manifest: one row per ticket to be updated or created, with its title, final label set, its milestone, its intended hierarchy/dependencies (`Related to` / `Depends on` / `Parallel with` lines and native sub-issue links), and `grant`/`withhold` plus a one-line rationale. Include a row for the parent's own pending mutations: the ownership claim (or `already owned by you` when `ownershipState` is `owned-by-caller`) and the `Working` label transition. If the parent already carries sub-issues from a prior run, state in the manifest that those existing children will not be modified — and note that a child created before this gate existed may still carry a legacy `automerge:ok`/`Browser`/`ui:visual-check` grant inherited under the old behavior; this run does not audit or revoke it, since the unmodified child is untouched by design.

7. **One confirmation.** Ask, via `AskUserQuestion`:
   "Apply this refinement as shown?"
   Options: "Confirm — apply as shown" / "Decline — make no changes". No adjust loop — a decline requires a fresh `/cenci:refine <id>` run to change anything.

8. **Decline** ⇒ zero GitHub writes have occurred and none will occur — no ownership claim, no `Working` label, no ticket/label/sub-issue mutation of any kind — so no cleanup mutation is needed: title, body, labels, assignees, milestone, and native sub-issues are state-for-state exactly as they were when this run started. Jump straight to step 13's declined-cleanup branch below, and report that re-running `/cenci:refine <id>` is how to adjust the proposal.

**Confirm** ⇒ continue into `## Update Ticket` below, where steps 10-13 apply every effective verdict and label set exactly as rendered in the manifest.

## Update Ticket

> **CRITICAL**: This section is mandatory after refinement. Do NOT skip it.
> **No GitHub write of any kind occurs before** the human confirms at the `## Confirmation Gate` above — that gate, not the Q&A loop (steps 6-8), is the pre-write authorization boundary. The Q&A loop only shapes the proposal's content; steps 10-13 below execute only after the gate's Confirm branch, and a Decline stops before any of them run (see step 13's declined branch).

> **Write-failure protocol**: Every *edit* in this section (ticket body/title updates, parent tracking updates, label add/remove) MUST be verified by re-fetching the resource with `gh issue view ... --json ...` and confirming the expected change is actually present — a command exiting 0 is not sufficient proof. Ticket *creation* is the one exception: `--jq .number` on the `gh api repos/...` response returns the new issue number directly — a numeric value is the proof; empty output, non-numeric output, or a non-zero exit is a failed create, so no separate re-fetch is required there. A malformed JSON payload surfaces as an API 4xx parse error and is itself a failed write — handle it with the single documented retry below, not a hand-patch loop. This retry also covers the local `Write` tool call that authors the raw title/body files and the `jq -n --rawfile` invocation that composes the JSON payload from them (see the `shell-rules` skill's canonical snippet): if a raw-file `Write` call fails, or the subsequent `jq` invocation exits non-zero, or the payload file is missing/empty/stale when the `gh api repos/... --input` command is about to read it, retry the failed step (`Write` or `jq`) once before (re-)invoking that `gh api repos/...` command — do not assume a local Write or jq failure is instead an API-side rejection. If the write or the verification fails:
> 1. Report the error to the user.
> 2. Retry the write once, then verify again.
> 3. If it still fails, **STOP** — do not proceed to the next step — and emit a partial-state report: what succeeded so far (with concrete issue/label numbers or names), what failed, and what the user needs to do manually to reconcile it. Each write point below states what belongs in that report.

**Per-run temp-file token**: The `<token>` used in every temp-file path of this section is the one created in Process step 3 — carry that same literal value forward; never mint a second token mid-run (two tokens in one run would orphan the earlier files from step 13's cleanup list).

### Write order

The canonical, machine-verified order of every GitHub write this skill performs, reached only via the gate's Confirm branch — nothing before this point ever writes:

1. `claim` — the ownership claim, first write (`#### Ownership claim (first write)` below).
2. `working` — the `Working` label ensure + add (`#### Label "Working"` below).
3. `parent-body` — the parent ticket's title/body update (step 10).
4. `child-create:1` immediately followed by its own `child-link:1`, then `child-create:2` immediately followed by its own `child-link:2`, immediately followed by its own `child-blockers:2` when child 2 has at least one blocking sibling (omitted entirely for a child with none), and so on for every split child in dependency order (Pass 1) — every child's create+link(+blockers) group completes before the next step. The blockers op is per-child optional, and the **first** child never carries one: children are created in dependency order, so child 1 has no already-created sibling to be blocked by. That is why this canonical illustration shows a blockers op on child 2 only — a blockers op on child 1 is not a shape this procedure can produce. (Stated without its op token here on purpose: this section is machine-extracted, and every backtick-quoted token inside it is read as part of the canonical sequence.)
5. `parent-exec-order` — Pass 2's Execution Order note on the parent body when the split has real ordering (step 10 Pass 2(b)); when there is no split, this same slot instead covers the companion design ticket's create, its native `--add-blocked-by` link plus `blockedBy` verification, and the supplementary `Depends on #<D> (design)` prose-line body write.
6. `refined` — step 11's `Refined` label add / `Working` label removal.
7. `visual-check` — step 12's `ui:visual-check` label add (skipped when `isDesignTicket`).

#### Manifest revalidation (read-only — before the first write)

Reached immediately after the gate's Confirm, before the ownership claim, before `Working`, before any other write. Fetch the parent's current milestone and labels **to a file**, as its own Bash call (a fetch failure then surfaces distinctly, before any write), mirroring the same pattern in `implement/phases/phase-9-pr.md`. This fetch is now **unconditional** — it runs on every Confirm, whether or not this run creates split children or a companion design ticket, because it also guards the parent's own label write (steps 11-12) against write-time drift, not only child/design-ticket inheritance. Do not reuse the `labels`/`milestone` already in context from the Context section's fetch or from the gate's own snapshot: both are stale by the time this runs, and externally-sourced label names must never be interpolated into a command line (`docs/skill-authoring.md`) — the file is consumed mechanically by `jq --slurpfile` below, never read back into shell text. The trailing `|| rm -f` is load-bearing: it removes a partially-written file so the presence gate below can never mistake one for a good fetch.
```bash
gh issue view <number> --repo <owner>/<repo> --json milestone,labels > ${TMPDIR:-/tmp}/cenci/issue-<number>-<token>-parent-meta.json || rm -f ${TMPDIR:-/tmp}/cenci/issue-<number>-<token>-parent-meta.json
```

**Presence gate — fail closed, zero writes (D1).** Read the file back, then validate its content is actually usable — an exit-0 `cat` alone is not proof the fetch produced real data:
```bash
cat ${TMPDIR:-/tmp}/cenci/issue-<number>-<token>-parent-meta.json
```
```bash
jq -e 'has("labels")' ${TMPDIR:-/tmp}/cenci/issue-<number>-<token>-parent-meta.json > /dev/null
```
If the `cat` command errors (non-zero exit, e.g. `No such file or directory`), OR the file exists but the `jq -e 'has("labels")'` check fails (empty file, malformed JSON, or valid JSON missing the expected `labels` shape) — treat a present-but-empty-or-malformed file exactly the same as a missing one, never as a good fetch — retry the fetch once. If it errors again by either check — the parent cannot be read after one retry — **STOP**: zero writes have occurred anywhere in this run, so stopping here is free (D1; converges with the fail-closed precedent for the identical fetch at `implement/phases/phase-9-pr.md:263` — that fetch's graceful-degrade rationale no longer applies here now that this fetch precedes every write instead of following one). Report that ticket #`<number>`'s metadata could not be fetched and that re-running `/cenci:refine <number>` is how to retry. Do not claim ownership, do not add `Working`, do not update the parent body, do not create children.

**Drift classification — fail closed on authorization-sensitive drift (D2).** If the fetch succeeds, diff the freshly fetched `labels` against the gate-time parent snapshot recorded at step 5 of the `## Confirmation Gate` above:
- **Authorization-sensitive drift** — the parent's `automerge:ok`, `Browser`, or `ui:visual-check` labels changed since the gate rendered its manifest: **STOP** — zero writes anywhere in this run — stop and ask for a fresh confirmation: report exactly what changed and tell the user to re-run `/cenci:refine <number>` from scratch. No in-session re-gate: the gate's single confirmation authorizes exactly the manifest it rendered, and re-rendering here would violate "steps 10-13 never recompute a verdict or a label set."
- **Cosmetic drift** — any other label or milestone change (e.g. `area:*`, priority, team, `Design`, milestone): proceed with the freshly fetched snapshot (not the gate-time one) for every inheritance decision below, and disclose the cosmetic label drift in the **Final Message**.
- **No drift**: proceed with the freshly fetched snapshot as normal.

#### Ownership claim (first write)

The first GitHub write of the run. Read the `ticket-ownership` reference skill's `## Claim ownership (mutating)` section and follow it — re-verify exclusive ownership as the first action after the gate's Confirm, using a fresh fetch: the `ownershipState` recorded in `## Ownership Inspection (read-only)` above may be stale by the time Confirm is reached, since time has passed for the Q&A loop and the gate's questions. If a conflict is found (a different assignee, or multiple assignees since the inspection), **STOP**: a conflict stops here with zero other writes — no `Working`, no parent body edit, no child creation — and report the observed state exactly as `ticket-ownership` specifies. Otherwise the claim proceeds and this run is now the ticket's sole assignee.

#### Label "Working"

Immediately after the ownership claim succeeds, add the "Working" label to signal that the ticket is actively being worked on. `gh issue edit --add-label` fails when the label does not exist in the repository, so ensure it exists first — run each as its own Bash call (`|| true` swallows only the "already exists" error):
```bash
gh label create "Working" --repo <owner>/<repo> --color "FBCA04" --description "Actively being refined, designed, or implemented" 2>/dev/null || true
```
```bash
gh issue edit <number> --repo <owner>/<repo> --add-label "Working"
```
Apply the same ensure-then-add pattern to every label this skill applies later (`Refined`, `Design`, `Browser`, `ui:visual-check`, `automerge:ok`, …): before the first `--add-label <name>` of a label, run its `gh label create <name> … || true` with the color/description from the lifecycle table in `/cenci:configure`.

**Ensure gate-decided labels exist, before Pass 1.** Step 10's Pass 1 below may seed `automerge:ok`, `Browser`, and/or `ui:visual-check` directly into a child's create payload, and step 11 may add `automerge:ok` to the parent — `gh issue edit --add-label` fails when the label does not exist in the repository, so ensure all three exist now, before any child is created (the ensure-then-add rule already exists in `#### Label "Working"` above; this only relocates it ahead of the writes that need it, using the same colors/descriptions as the canonical table at `configure/SKILL.md:715`). Run each ensure as its own Bash call — idempotent (`|| true`), so running one this run doesn't need is harmless:
```bash
gh label create "automerge:ok" --repo <owner>/<repo> --color "006B75" --description "Human granted hands-off merge at refinement — babysit may merge this PR without review" 2>/dev/null || true
```
```bash
gh label create "Browser" --repo <owner>/<repo> --color "BFD4F2" --description "Implementation needs interactive browser access (Playwright CLI)" 2>/dev/null || true
```
```bash
gh label create "ui:visual-check" --repo <owner>/<repo> --color "FEF2C0" --description "Visual/layout change — verify in a browser before merge" 2>/dev/null || true
```

10. **Update the ticket description in the remote system.**

   > **IMPORTANT**: Writing a temp file is NOT updating the ticket. You MUST execute the update command after writing the file. Never stop between writing the temp file and running the update command.

   **When the proposal adopted in step 9 includes an `### Updated Title`**, use the `Write` tool to create the raw title and body as plain text — `${TMPDIR:-/tmp}/cenci/issue-<number>-<token>-edit-title.txt` (the updated title) and `${TMPDIR:-/tmp}/cenci/issue-<number>-<token>-edit-body.md` (the updated description) — never a hand-escaped JSON literal (the title is free text and must never be interpolated directly into a command line; a title c

…(truncated)
