{"record":{"id":"234e67a4f2cd83bb","repo":"pbakaus/impeccable","slug":"build-phase-finish-disposition-ship-refused","errorCode":null,"errorMessage":"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","messagePattern":"build-phase: finish --disposition ship refused: (.+?) (.+?) not closed \\(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","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/comp-verbs/src/build_phase.rs","lineNumber":2016,"sourceCode":"                io.err(\"build-phase: finish --disposition ship|fix|rebuild|recapture\\n\");\n                return 1;\n            }\n            let disposition = disposition.unwrap();\n            let open_before: Vec<String> = PHASES\n                .iter()\n                .filter(|&&ph| {\n                    ph != \"review\"\n                        && state\n                            .pointer(&format!(\"/phases/{ph}/status\"))\n                            .and_then(Value::as_str)\n                            .map(|st| st != \"closed\" && st != \"skipped\")\n                            .unwrap_or(false)\n                })\n                .map(|s| s.to_string())\n                .collect();\n            if disposition == \"ship\" && !open_before.is_empty() {\n                let phase = state.get(\"phase\").and_then(Value::as_str).unwrap_or(\"\");\n                io.err(&format!(\n                    \"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\",\n                    open_before.join(\", \"),\n                    if open_before.len() == 1 { \"is\" } else { \"are\" }\n                ));\n                return 2;\n            }\n            let phase = state.get(\"phase\").and_then(Value::as_str).unwrap_or(\"\").to_string();\n            state.as_object_mut().unwrap().insert(\"finish\".into(), json!({ \"disposition\": disposition, \"at\": now(), \"phaseAtFinish\": phase }));\n            if phase == \"review\" {\n                if let Some(rev) = state.pointer_mut(\"/phases/review\").and_then(|v| v.as_object_mut()) {\n                    rev.insert(\"status\".into(), json!(\"closed\"));\n                    rev.insert(\"closedAt\".into(), json!(now()));\n                }\n            }\n            save_state(io, &state);\n            io.out(&format!(\"{}\\n\", render_status(io, &state)));\n            0\n        }","sourceCodeStart":1998,"sourceCodeEnd":2034,"githubUrl":"https://github.com/pbakaus/impeccable/blob/2bc2879276c1f321a53c4ca99d3371e411329b52/crates/comp-verbs/src/build_phase.rs#L1998-L2034","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Run the build-phase status output to list the open items named in the error.","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`.","If the open items are stale/obsolete, close them explicitly rather than deleting state, then ship.","Re-run `build-phase finish --disposition ship` once `open_before` is empty."],"exampleFix":"// before\n$ impeccable build-phase finish --disposition ship\nbuild-phase: finish --disposition ship refused: hero is not closed (phase build). ...\n// after\n$ impeccable build-phase record hero --outcome fix\n$ impeccable build-phase finish --disposition ship","handlingStrategy":"validation","validationCode":"const status = JSON.parse(runSync('impeccable build-phase status').stdout);\nif (status.open && status.open.length > 0) {\n  throw new Error(`Cannot ship: open phases: ${status.open.join(', ')}. Close them first.`);\n}","typeGuard":"function canShip(state) {\n  return Array.isArray(state?.open) && state.open.length === 0;\n}","tryCatchPattern":"try {\n  runSync('impeccable build-phase finish --disposition ship');\n} catch (e) {\n  if (e.stderr?.includes('not closed')) {\n    console.error('Open phases remain; record fix/rebuild for each item before shipping.');\n    process.exit(2);\n  }\n  throw e;\n}","preventionTips":["Always run build-phase status immediately before a ship disposition.","Automate close-out: record fix/rebuild outcomes as part of the fix workflow, not at the end.","Never hand-edit the phase state file; use the CLI so open items stay consistent.","Treat exit code 2 from finish as 'gate blocked', and stop the pipeline."],"tags":["build-phase","cli","state-gate","workflow"],"backgroundTag":"invalid-state-transition","analyzedSha":"2bc2879276c1f321a53c4ca99d3371e411329b52","analyzedAt":"2026-09-08T04:51:14.109Z","contentChangedAt":"2026-09-08T04:51:14.109Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}