pbakaus/impeccable · error

build-phase: finish --disposition ship refused: {} {} not cl

Error message

build-phase: finish --disposition ship refused: {} {} not closed (phase {phase}). Record fix or rebuild, or close the phases first; a page shipped over an open hero is a page shipped against its own gate.

What it means

The `build-phase finish --disposition ship` command refuses to mark a build phase as shipped while there are still open (unclosed) phase items. The build-phase subsystem acts as a gate: shipping a page over an open hero/phase would mean shipping against the project's own quality gate. The error names each open item, the singular/plural verb, and the current phase so the developer knows exactly what to close first.

Source

Thrown at crates/comp-verbs/src/build_phase.rs:2016

                io.err("build-phase: finish --disposition ship|fix|rebuild|recapture\n");
                return 1;
            }
            let disposition = disposition.unwrap();
            let open_before: Vec<String> = PHASES
                .iter()
                .filter(|&&ph| {
                    ph != "review"
                        && state
                            .pointer(&format!("/phases/{ph}/status"))
                            .and_then(Value::as_str)
                            .map(|st| st != "closed" && st != "skipped")
                            .unwrap_or(false)
                })
                .map(|s| s.to_string())
                .collect();
            if disposition == "ship" && !open_before.is_empty() {
                let phase = state.get("phase").and_then(Value::as_str).unwrap_or("");
                io.err(&format!(
                    "build-phase: finish --disposition ship refused: {} {} not closed (phase {phase}). Record fix or rebuild, or close the phases first; a page shipped over an open hero is a page shipped against its own gate.\n",
                    open_before.join(", "),
                    if open_before.len() == 1 { "is" } else { "are" }
                ));
                return 2;
            }
            let phase = state.get("phase").and_then(Value::as_str).unwrap_or("").to_string();
            state.as_object_mut().unwrap().insert("finish".into(), json!({ "disposition": disposition, "at": now(), "phaseAtFinish": phase }));
            if phase == "review" {
                if let Some(rev) = state.pointer_mut("/phases/review").and_then(|v| v.as_object_mut()) {
                    rev.insert("status".into(), json!("closed"));
                    rev.insert("closedAt".into(), json!(now()));
                }
            }
            save_state(io, &state);
            io.out(&format!("{}\n", render_status(io, &state)));
            0
        }

View on GitHub (pinned to 2bc2879276)

Solutions

  1. Run the build-phase status output to list the open items named in the error.
  2. Close each open phase by recording its fix or rebuild outcome (e.g. `build-phase finish --disposition fix` / `rebuild` per item) before re-running with `--disposition ship`.
  3. If the open items are stale/obsolete, close them explicitly rather than deleting state, then ship.
  4. Re-run `build-phase finish --disposition ship` once `open_before` is empty.

Example fix

// before
$ impeccable build-phase finish --disposition ship
build-phase: finish --disposition ship refused: hero is not closed (phase build). ...
// after
$ impeccable build-phase record hero --outcome fix
$ impeccable build-phase finish --disposition ship
Defensive patterns

Strategy: validation

Validate before calling

const status = JSON.parse(runSync('impeccable build-phase status').stdout);
if (status.open && status.open.length > 0) {
  throw new Error(`Cannot ship: open phases: ${status.open.join(', ')}. Close them first.`);
}

Type guard

function canShip(state) {
  return Array.isArray(state?.open) && state.open.length === 0;
}

Try / catch

try {
  runSync('impeccable build-phase finish --disposition ship');
} catch (e) {
  if (e.stderr?.includes('not closed')) {
    console.error('Open phases remain; record fix/rebuild for each item before shipping.');
    process.exit(2);
  }
  throw e;
}

Prevention

When it happens

Trigger: Running `impeccable build-phase finish --disposition ship` while `state.open` (open_before) is non-empty; i.e. phase items recorded as 'fix' or 'rebuild' have not been closed via a subsequent finish/record operation before attempting ship.

Common situations: Developers finishing a build session forget to record fixes or rebuild outcomes for flagged items; multiple phases were opened but only some were closed; the phase state file (.impeccable build-phase state) was edited or restored from an older snapshot leaving stale open items.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of pbakaus/impeccable@2bc2879276 (2026-09-08). Data as JSON: /api/errors/234e67a4f2cd83bb. Report an issue: GitHub.