The Job Board
Everything you built becomes a table of jobs and one terminal to lend out.
What you're wiring up
Job control is the payoff. The process groups from Stage 5 become jobs the shell tracks in a table. The terminal becomes a token the shell lends to exactly one job at a time. SIGCHLD becomes the news feed that keeps the table honest about who is still alive.
The state you have not met yet is stopped. SIGTSTP parks a process mid-instruction and SIGCONT resumes it, and it is a real kernel state, not a shell fiction: ps will show the process as T. Once you have implemented fg, the difference between a stopped process, a background process, and a zombie is knowledge you own for every debugging session for the rest of your career.
One rule constrains the design. You cannot print from a signal handler; it is undefined behavior. So the handler only records what happened, and the Done notices are printed at prompt time, which is also why real shells never interrupt a half-typed line to tell you a background job finished.
Assembly steps
hint
Background jobs never get the terminal, so no tcsetpgrp for them.
hint
In the handler, only set a flag or loop waitpid(-1, ..., WNOHANG|WUNTRACED). Do all printing at prompt time.
hint
You need WUNTRACED in the foreground waitpid, or a stop looks to your shell like nothing happened at all.
hint
Signal the whole group with kill(-pgid, SIGCONT). The minus sign is the entire difference.
hint
One boolean warned flag, reset by any other command. That is exactly what bash does.
Go deeper (after it passes)
The schematic is fully inked. Victory lap: run your shell as your login shell for an hour and notice everything it cannot do yet. The job lifecycle of Running, Stopped and Done is a genuine state machine, so compare your enum and switch against HFDP M9 — The Gumball State Machine. Then take the process model somewhere new: build-your-own-redis spawns and pipes clients exactly the way you just learned.