diff --git a/fusion-plating/docs/superpowers/specs/2026-05-12-job-milestone-cascade-design.md b/fusion-plating/docs/superpowers/specs/2026-05-12-job-milestone-cascade-design.md
new file mode 100644
index 00000000..8338268d
--- /dev/null
+++ b/fusion-plating/docs/superpowers/specs/2026-05-12-job-milestone-cascade-design.md
@@ -0,0 +1,310 @@
+# Job Milestone Cascade — Design Spec
+
+**Date:** 2026-05-12
+**Status:** Approved for implementation (Phase 1)
+**Scope:** `fusion_plating`, `fusion_plating_jobs`, `fusion_plating_certificates`, `fusion_plating_logistics` (on entech)
+
+## Goal
+
+Replace the per-step "Finish & Next" button on the `fp.job` form header with a single context-aware milestone-advance button. When all steps are done, the button cycles the manager through the remaining post-step lifecycle:
+
+```
+Mark Job Done → Issue Certs → Schedule Delivery → Mark Shipped → (closed)
+```
+
+Each click runs the existing downstream method (no new business logic invented). The button is **one place** the manager looks; the system always tells them what's next.
+
+## Motivation (workflow gap audit)
+
+End-to-end audit found:
+
+- **G1.** `fp.job.state` and `fp.job.workflow_state_id` are two parallel state machines that drift.
+- **G2.** No auto-fire of `button_mark_done` when all steps complete. The cascade (delivery / cert / notification) hangs off a manual click that has no UI surface after Finish & Next becomes a no-op.
+- **G3.** Delivery + cert creation only happen via `button_mark_done`.
+- **G4.** Invoice timing is strategy-dependent; no `on_job_done` strategy.
+- **G5.** Certificate auto-creation is best-effort and only spawns CoC. Thickness Report cert is never auto-created even when the part / partner requires it.
+- **G6.** No "next action" surface on the job header.
+
+Phase 1 closes **G2 and G6 directly**, makes meaningful progress on **G5**, and lays groundwork for G3/G4. G1 is explicitly deferred.
+
+## Decisions
+
+| Decision | Choice | Rationale |
+|---|---|---|
+| Ship in recipe vs separate | **Separate (Option C — Hybrid)** | Recipes = manufacturing; deliveries = logistics. Surface "next" on the job header so manager doesn't have to navigate. Supports split shipments naturally. |
+| Cert gate strictness on Mark Shipped | **Hard block** (with manager bypass via context key) | AS9100 / Nadcap compliance — no shipping without paperwork. |
+| Per-cert vs bulk issuance | **Per-cert** | Each cert (CoC vs Thickness Report) needs its own compliance review. |
+| No-cert-required jobs | Skip Issue Certs, go straight to Schedule Delivery | Commercial customers don't need to click a button that has nothing to do. |
+| Migration of existing data | **None — dev stage** | No production jobs to preserve. Just rewrite the `Shipped` state seed XML; `-u` reloads it. |
+
+## Architecture
+
+### New compute fields on `fp.job`
+
+```python
+all_steps_terminal = fields.Boolean(
+ compute='_compute_all_steps_terminal', store=True,
+ help='True ⇔ at least one step exists AND every step is in '
+ 'done/skipped/cancelled.',
+)
+
+next_milestone_action = fields.Selection([
+ ('mark_done', 'Mark Job Done'),
+ ('issue_certs', 'Issue Certs'),
+ ('schedule_delivery', 'Schedule Delivery'),
+ ('mark_shipped', 'Mark Shipped'),
+ ('closed', 'Closed'),
+], compute='_compute_next_milestone_action')
+
+next_milestone_label = fields.Char(
+ compute='_compute_next_milestone_action',
+ help='Human label for the next-action button — read by the view.',
+)
+```
+
+`_compute_next_milestone_action` resolution order (top wins):
+
+```
+1. NOT all_steps_terminal → None (the existing Finish & Next stays)
+2. state != 'done' → mark_done
+3. ANY required cert in state='draft' → issue_certs
+4. NO delivery, OR delivery in state='draft' → schedule_delivery
+5. delivery.state in scheduled/in_transit → mark_shipped
+6. otherwise → closed
+```
+
+### Dispatcher action
+
+```python
+def action_advance_next_milestone(self):
+ """Single entry point — branches on next_milestone_action and
+ delegates to the existing method. Never invents new business logic."""
+ self.ensure_one()
+ handlers = {
+ 'mark_done': self.button_mark_done,
+ 'issue_certs': self._action_open_draft_certs,
+ 'schedule_delivery': self._action_open_draft_delivery,
+ 'mark_shipped': self._action_mark_active_delivery_delivered,
+ }
+ fn = handlers.get(self.next_milestone_action)
+ if fn:
+ return fn()
+ return True
+```
+
+**Helper methods** (each returns an Odoo action dict or calls the existing
+business-logic method):
+
+- `_action_open_draft_certs` → returns an `ir.actions.act_window` opening
+ the `fp.certificate` list view with domain
+ `[('x_fc_job_id', '=', self.id), ('state', '=', 'draft')]` and
+ `target='current'` so the manager works on the cert list, then uses the
+ breadcrumb to return.
+- `_action_open_draft_delivery` → finds the first delivery in
+ `state='draft'` for this job and returns an `ir.actions.act_window`
+ opening that record's form in `target='current'`. Falls back to the
+ delivery list view filtered to this job if no draft delivery exists.
+- `_action_mark_active_delivery_delivered` → finds the first delivery in
+ `state in ('scheduled', 'in_transit')`, calls `action_mark_delivered`
+ on it directly (no UI navigation — the cascade just *does* the thing).
+ Posts to job chatter on success.
+
+`target='current'` is chosen everywhere because the manager is working
+on the cascade as a multi-step process; a popup would lose breadcrumb
+context. The existing job-form breadcrumb survives, so they can navigate
+back when done.
+
+### New trigger on `fp.job.workflow.state`
+
+```python
+trigger_on_delivery_state = fields.Boolean(
+ string='Trigger on Delivery Delivered',
+ help='When True, this state passes once at least one '
+ 'fusion.plating.delivery linked to the job reaches '
+ 'state="delivered". Use for the Shipped milestone in '
+ 'lieu of recipe-side default_kind="ship" tagging.',
+)
+```
+
+`fp.job.workflow.state._fp_is_passed_for_job(job)` gains:
+
+```python
+if self.trigger_on_delivery_state:
+ return any(d.state == 'delivered' for d in job.delivery_ids)
+```
+
+`fp.job._compute_workflow_state_id`'s `@api.depends` extends to include `delivery_ids.state`.
+
+### Cert auto-create hardening
+
+Add to `fp.job`:
+
+```python
+def _resolve_required_cert_types(self):
+ """Return the set of cert types this job must produce.
+ Reads the part's certificate_requirement; falls back to the
+ customer's send_coc / send_thickness_report flags when the part
+ is set to 'inherit'."""
+ req = (self.part_catalog_id and
+ self.part_catalog_id.certificate_requirement) or 'inherit'
+ if req == 'inherit':
+ types = set()
+ if self.partner_id.x_fc_send_coc:
+ types.add('coc')
+ if self.partner_id.x_fc_send_thickness_report:
+ types.add('thickness_report')
+ return types
+ return {
+ 'none': set(),
+ 'coc': {'coc'},
+ 'coc_thickness': {'coc', 'thickness_report'},
+ }.get(req, {'coc'})
+```
+
+`_fp_create_certificates` is rewritten to loop over the resolved set and create one draft `fp.certificate` per type, idempotent per type (checks `x_fc_job_id` + `certificate_type` before creating).
+
+### Cert gate on Mark Shipped
+
+`fusion.plating.delivery.action_mark_delivered` gains a gate:
+
+```python
+def action_mark_delivered(self):
+ skip_cert = self.env.context.get('fp_skip_cert_gate')
+ for delivery in self:
+ if not skip_cert and delivery.job_ref:
+ job = self.env['fp.job'].search(
+ [('name', '=', delivery.job_ref)], limit=1)
+ if job:
+ draft_certs = self.env['fp.certificate'].search([
+ ('x_fc_job_id', '=', job.id),
+ ('state', '=', 'draft'),
+ ])
+ if draft_certs:
+ raise UserError(_(
+ 'Cannot mark delivery %(d)s shipped — '
+ 'job %(j)s still has %(n)d draft certificate(s). '
+ 'Issue them first, or override via '
+ 'fp_skip_cert_gate=True context key.'
+ ) % {
+ 'd': delivery.name,
+ 'j': job.name,
+ 'n': len(draft_certs),
+ })
+ return super().action_mark_delivered()
+```
+
+Lives in `fusion_plating_certificates/models/fp_delivery.py` (so the gate ships with the certs module — no coupling to logistics).
+
+### View changes
+
+In `fusion_plating_jobs/views/fp_job_form_inherit.xml`:
+
+1. **Hide existing Finish & Next** when `all_steps_terminal`:
+
+ ```xml
+
+ ```
+
+2. **Add four mutually-exclusive milestone buttons.** Each binds to `action_advance_next_milestone` but with a hardcoded label so users don't see a generic button. Visibility is gated on `next_milestone_action`:
+
+ ```xml
+
+
+
+
+ ```
+
+ `next_milestone_action == 'closed'` shows nothing (terminal).
+
+3. **Hide invisible field** — register `` and `` so the view can reference them in `invisible=` expressions.
+
+### Data change — Shipped workflow state seed
+
+In `fusion_plating_jobs/data/fp_workflow_state_data.xml`, replace the `Shipped` state record:
+
+```xml
+
+ Shipped
+ shipped
+ 60
+ success
+
+ Shipment confirmed (delivery marked delivered). Customer can be notified.
+
+```
+
+Keep `noupdate="1"` on the wrapping `` block since shops may further customise. In dev, `-u fusion_plating_jobs` re-applies it on fresh DBs.
+
+## State transition cascade (visual)
+
+```
+ ┌──────────────────────┐
+ │ Steps still running │ ← Finish & Next visible
+ └──────────┬───────────┘
+ ▼ last step done
+ ┌──────────────────────┐
+ │ Mark Job Done │ ← button cascade starts
+ └──────────┬───────────┘
+ ▼ button_mark_done (gates + create delivery + cert)
+ ┌────────────────────────────┴─────────────────────────────┐
+ │ │
+ any draft cert? no required certs
+ │ │
+ ▼ ▼
+ ┌────────────┐ (skip to next)
+ │ Issue Certs│
+ └─────┬──────┘
+ ▼ all certs issued
+ ┌─────────────────┐
+ │ Schedule Deliv. │
+ └─────┬───────────┘
+ ▼ delivery scheduled
+ ┌─────────────┐
+ │ Mark Shipped │ ← gates on issued certs (cert module)
+ └─────┬────────┘
+ ▼ delivery.action_mark_delivered
+ (workflow_state → Shipped via the new trigger;
+ invoice fires if strategy='on_delivery')
+ │
+ ▼
+ Closed
+```
+
+## Files touched
+
+| File | Change |
+|---|---|
+| `fusion_plating_jobs/models/fp_job.py` | Add `all_steps_terminal`, `next_milestone_action`, `next_milestone_label` compute fields. Add `action_advance_next_milestone` dispatcher + 3 helper methods. Add `_resolve_required_cert_types`. Rewrite `_fp_create_certificates` to loop over resolved types. Extend `@api.depends` on `_compute_workflow_state_id` to include `delivery_ids.state`. |
+| `fusion_plating_jobs/models/fp_job_workflow_state.py` | Add `trigger_on_delivery_state` Boolean. Extend `_fp_is_passed_for_job` with delivery-state branch. |
+| `fusion_plating_jobs/data/fp_workflow_state_data.xml` | Rewrite `Shipped` state seed: drop `trigger_default_kinds='ship'`, add `trigger_on_delivery_state=True`. |
+| `fusion_plating_jobs/views/fp_job_form_inherit.xml` | Hide `Finish & Next` when `all_steps_terminal`. Add 4 milestone buttons. Add invisible field declarations. |
+| `fusion_plating_certificates/models/fp_delivery.py` | Inherit `fusion.plating.delivery`; override `action_mark_delivered` to gate on draft certs. Manager bypass via `fp_skip_cert_gate=True`. |
+| `fusion_plating_certificates/__init__.py` / `models/__init__.py` | Register the new `fp_delivery.py` if needed. |
+
+Manifest versions to bump:
+- `fusion_plating_jobs`
+- `fusion_plating_certificates`
+
+## Out of scope (Phase 2+)
+
+- **Send Certs to Customer button** — wrap `action_send_to_customer` per cert into the cascade after Mark Shipped. Existing `fp_notification_trigger` hooks already handle ship-time customer email; needs integration design.
+- **`on_job_done` invoice strategy** — currently invoices fire at SO confirm or delivery delivered. A "fire at job done" option is desirable for cash-up-front shops; needs strategy-pattern extension in `fusion_plating_invoicing/models/sale_order.py`.
+- **`fp.job.state` ↔ `workflow_state_id` reconciliation (G1)** — pick one source of truth, drop or compute the other. Larger refactor; defer until Phase 1 lands and we see how the cascade affects state-machine readability.
+
+## Implementation notes / gotchas
+
+- `next_milestone_action` is **not stored** — recompute on every access. Cheap (4 boolean checks). Avoids dependency-tracking complexity when delivery state changes.
+- The cascade reads `delivery_ids` on `fp.job`. Confirm this field exists (related/computed) before relying on it. Fallback: search `fusion.plating.delivery` by `job_ref == self.name`.
+- The cert gate in `action_mark_delivered` lives in the certs module so logistics doesn't depend on certs (currently logistics is upstream of certs in the dependency graph — verify).
+- View buttons share the same `name="action_advance_next_milestone"` but Odoo distinguishes them by their `string=` attribute in the rendered DOM — this is the standard Odoo pattern for context-aware buttons (see `sale.order` action buttons).
+- All four buttons are inside the header; users won't see more than one at a time thanks to the `invisible=` filters.