diff --git a/framework/php/src/Maker/BridgeCommandMaker.php b/framework/php/src/Maker/BridgeCommandMaker.php
new file mode 100644
index 0000000..8044568
--- /dev/null
+++ b/framework/php/src/Maker/BridgeCommandMaker.php
@@ -0,0 +1,119 @@
+` — emits a non-CRUD HTTP endpoint that
+ * carries a domain action.
+ *
+ * For example, `make:bridge:command MarkAllDone` generates
+ * `src/Controller/MarkAllDoneController.php` mounted at
+ * `POST /api/mark-all-done`. The generated body is a TODO stub the
+ * developer fills in.
+ *
+ * Phase 3 keeps this synchronous — Messenger integration for async
+ * dispatch is a follow-up. Apps that need it can add Messenger
+ * separately and refactor the generated controller to dispatch via the
+ * bus.
+ */
+final class BridgeCommandMaker extends AbstractMaker
+{
+ public static function getCommandName(): string
+ {
+ return 'make:bridge:command';
+ }
+
+ public static function getCommandDescription(): string
+ {
+ return 'Generate a non-CRUD HTTP command endpoint (POST /api/{kebab-name}).';
+ }
+
+ public function configureCommand(Command $command, InputConfiguration $inputConfig): void
+ {
+ $command
+ ->addArgument(
+ 'name',
+ InputArgument::OPTIONAL,
+ 'Singular CamelCase name of the command (e.g. MarkAllDone).',
+ )
+ ->setHelp(
+ "Creates one file:\n\n"
+ ." • src/Controller/Controller.php — POST handler at /api/\n\n"
+ ."The body is a TODO stub. Fill in the domain logic and persist via the EntityManager.\n"
+ );
+ }
+
+ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
+ {
+ if (null === $input->getArgument('name')) {
+ $name = $io->ask('Command name (CamelCase)?', null, static function (?string $v): string {
+ if (null === $v || '' === trim($v)) {
+ throw new \RuntimeException('Command name cannot be empty.');
+ }
+
+ return ucfirst(trim($v));
+ });
+ $input->setArgument('name', $name);
+ }
+ }
+
+ public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
+ {
+ $rawName = (string) $input->getArgument('name');
+ $singular = ucfirst(Str::asCamelCase($rawName));
+ $kebab = strtolower(preg_replace('/(?createClassNameDetails(
+ $singular,
+ 'Controller\\',
+ 'Controller',
+ );
+
+ $vars = [
+ 'singular' => $singular,
+ 'route' => $route,
+ 'kebab' => $kebab,
+ 'controller_fqcn' => $controllerFqcn->getFullName(),
+ 'controller_short' => $controllerFqcn->getShortName(),
+ ];
+
+ $generator->generateFile(
+ 'src/Controller/'.$controllerFqcn->getShortName().'.php',
+ __DIR__.'/templates/Command.tpl.php',
+ $vars,
+ );
+
+ $generator->writeChanges();
+ $this->writeSuccessMessage($io);
+ $io->text([
+ 'Next:',
+ " • Fill in the body of {$controllerFqcn->getShortName()}::__invoke().",
+ " • From QML, call rest.post(\"{$route}\").",
+ ]);
+ }
+
+ public function configureDependencies(DependencyBuilder $dependencies): void
+ {
+ $dependencies->addClassDependency(Route::class, 'symfony/routing');
+ $dependencies->addClassDependency(JsonResponse::class, 'symfony/http-foundation');
+ $dependencies->addClassDependency(Request::class, 'symfony/http-foundation');
+ $dependencies->addClassDependency(EntityManagerInterface::class, 'doctrine/orm');
+ }
+}
diff --git a/framework/php/src/Maker/BridgeWindowMaker.php b/framework/php/src/Maker/BridgeWindowMaker.php
new file mode 100644
index 0000000..571c407
--- /dev/null
+++ b/framework/php/src/Maker/BridgeWindowMaker.php
@@ -0,0 +1,98 @@
+` — emits a top-level QML Window that
+ * wraps `AppShell` and a content slot. Application code opens it via
+ * `Qt.createComponent("Window.qml")` (or by importing it) for
+ * the first window and as many extra instances as it wants for the
+ * multi-window test from PLAN.md §9 / §13 Phase 3.
+ *
+ * Generated file goes to `qml_path` (default: `../qml/`).
+ */
+final class BridgeWindowMaker extends AbstractMaker
+{
+ public function __construct(
+ private readonly string $qmlPath = '../qml/',
+ ) {
+ }
+
+ public static function getCommandName(): string
+ {
+ return 'make:bridge:window';
+ }
+
+ public static function getCommandDescription(): string
+ {
+ return 'Generate a top-level QML Window scaffold (AppShell + content slot).';
+ }
+
+ public function configureCommand(Command $command, InputConfiguration $inputConfig): void
+ {
+ $command
+ ->addArgument(
+ 'name',
+ InputArgument::OPTIONAL,
+ 'Singular name of the window (e.g. Todo, Settings).',
+ )
+ ->setHelp(
+ "Creates one file:\n\n"
+ ." • {qml_path}/Window.qml — Window subclass with AppShell\n"
+ );
+ }
+
+ public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
+ {
+ if (null === $input->getArgument('name')) {
+ $name = $io->ask('Window name?', null, static function (?string $v): string {
+ if (null === $v || '' === trim($v)) {
+ throw new \RuntimeException('Window name cannot be empty.');
+ }
+
+ return ucfirst(trim($v));
+ });
+ $input->setArgument('name', $name);
+ }
+ }
+
+ public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
+ {
+ $rawName = (string) $input->getArgument('name');
+ $singular = ucfirst(Str::asCamelCase($rawName));
+ $resource = strtolower($singular);
+
+ $vars = [
+ 'singular' => $singular,
+ 'resource' => $resource,
+ ];
+
+ $target = rtrim($this->qmlPath, '/').'/'.$singular.'Window.qml';
+ $generator->generateFile(
+ $target,
+ __DIR__.'/templates/Window.tpl.php',
+ $vars,
+ );
+
+ $generator->writeChanges();
+ $this->writeSuccessMessage($io);
+ $io->text("Window scaffold at {$target}.");
+ }
+
+ public function configureDependencies(DependencyBuilder $dependencies): void
+ {
+ // Pure-QML output — no PHP runtime deps.
+ }
+}
diff --git a/framework/php/src/Maker/templates/Command.tpl.php b/framework/php/src/Maker/templates/Command.tpl.php
new file mode 100644
index 0000000..c267242
--- /dev/null
+++ b/framework/php/src/Maker/templates/Command.tpl.php
@@ -0,0 +1,34 @@
+= "
+
+declare(strict_types=1);
+
+namespace App\Controller;
+
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Attribute\Route;
+
+/**
+ * Auto-generated domain command endpoint. Fill in the body and persist
+ * via $this->em->flush(). Doctrine entities tagged with #[BridgeResource]
+ * publish their changes to Mercure automatically.
+ */
+final class = $controller_short ?>
+
+{
+ public function __construct(
+ private readonly EntityManagerInterface $em,
+ ) {
+ }
+
+ #[Route('= $route ?>', name: '= $kebab ?>', methods: ['POST'])]
+ public function __invoke(Request $request): JsonResponse
+ {
+ // TODO: implement = $singular ?>.
+ // Read inputs from $request->getPayload() / json_decode($request->getContent(), true).
+ // Mutate entities, then $this->em->flush().
+
+ return new JsonResponse(['ok' => true]);
+ }
+}
diff --git a/framework/php/src/Maker/templates/Window.tpl.php b/framework/php/src/Maker/templates/Window.tpl.php
new file mode 100644
index 0000000..f8f2e36
--- /dev/null
+++ b/framework/php/src/Maker/templates/Window.tpl.php
@@ -0,0 +1,36 @@
+// Auto-generated by `bin/console make:bridge:window = $singular ?>`.
+// Edit freely; re-running the maker won't overwrite this file.
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Window
+import PhpQml.Bridge
+
+ApplicationWindow {
+ id: = $resource ?>Window
+ width: 720
+ height: 520
+ visible: true
+ title: "= $singular ?>"
+
+ AppShell {
+ anchors.fill: parent
+
+ // Replace this with the window's content. The AppShell parent
+ // takes care of Reconnecting / Offline UI overlays.
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 16
+
+ Label {
+ text: "= $singular ?>"
+ font.pixelSize: 18
+ font.bold: true
+ }
+
+ // TODO: window content here.
+ Item { Layout.fillWidth: true; Layout.fillHeight: true }
+ }
+ }
+}