diff --git a/bin/php-qml-init b/bin/php-qml-init
index 21cdb1c..64aafdf 100755
--- a/bin/php-qml-init
+++ b/bin/php-qml-init
@@ -151,6 +151,14 @@ sed -i \
-e "s|\$(BUILD_DIR)/skeleton|\$(BUILD_DIR)/$NAME|g" \
"$TARGET/Makefile"
+# .vscode/launch.json: binary path + config label both mention `skeleton`.
+if [ -f "$TARGET/.vscode/launch.json" ]; then
+ sed -i \
+ -e "s|build/qml/skeleton|build/qml/$NAME|g" \
+ -e "s|Run skeleton (Qt host)|Run $NAME (Qt host)|g" \
+ "$TARGET/.vscode/launch.json"
+fi
+
# ── Path-repo: absolute reference, or vendor a copy ──────────────────
COMPOSER_JSON="$TARGET/symfony/composer.json"
[ -f "$COMPOSER_JSON" ] || die "skeleton missing symfony/composer.json (corrupt copy?)"
diff --git a/examples/todo/.idea/.gitignore b/examples/todo/.idea/.gitignore
new file mode 100644
index 0000000..57d484e
--- /dev/null
+++ b/examples/todo/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Track only the shared run configs; ignore per-user IDE state.
+*
+!.gitignore
+!runConfigurations/
+!runConfigurations/*.xml
diff --git a/examples/todo/.idea/runConfigurations/make_appimage.xml b/examples/todo/.idea/runConfigurations/make_appimage.xml
new file mode 100644
index 0000000..eb46fd6
--- /dev/null
+++ b/examples/todo/.idea/runConfigurations/make_appimage.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/todo/.idea/runConfigurations/make_dev.xml b/examples/todo/.idea/runConfigurations/make_dev.xml
new file mode 100644
index 0000000..c732a41
--- /dev/null
+++ b/examples/todo/.idea/runConfigurations/make_dev.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/todo/.idea/runConfigurations/make_doctor.xml b/examples/todo/.idea/runConfigurations/make_doctor.xml
new file mode 100644
index 0000000..214305d
--- /dev/null
+++ b/examples/todo/.idea/runConfigurations/make_doctor.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/todo/.idea/runConfigurations/make_quality.xml b/examples/todo/.idea/runConfigurations/make_quality.xml
new file mode 100644
index 0000000..91dbe3b
--- /dev/null
+++ b/examples/todo/.idea/runConfigurations/make_quality.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/todo/.vscode/launch.json b/examples/todo/.vscode/launch.json
new file mode 100644
index 0000000..114f84e
--- /dev/null
+++ b/examples/todo/.vscode/launch.json
@@ -0,0 +1,41 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Listen for Xdebug (Symfony / FrankenPHP)",
+ "type": "php",
+ "request": "launch",
+ "port": 9003,
+ "pathMappings": {
+ "${workspaceFolder}/symfony": "${workspaceFolder}/symfony"
+ },
+ "log": false
+ },
+ {
+ "name": "Run todo (Qt host)",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/build/qml/todo",
+ "args": [],
+ "stopAtEntry": false,
+ "cwd": "${workspaceFolder}",
+ "environment": [
+ { "name": "BRIDGE_URL", "value": "http://127.0.0.1:8765" },
+ { "name": "BRIDGE_TOKEN", "value": "devtoken" }
+ ],
+ "preLaunchTask": "make build",
+ "MIMode": "gdb",
+ "linux": { "MIMode": "gdb" },
+ "osx": { "MIMode": "lldb" }
+ }
+ ],
+ "compounds": [
+ {
+ "name": "Dev: Xdebug + Qt host",
+ "configurations": [
+ "Listen for Xdebug (Symfony / FrankenPHP)",
+ "Run todo (Qt host)"
+ ]
+ }
+ ]
+}
diff --git a/examples/todo/.vscode/settings.json b/examples/todo/.vscode/settings.json
new file mode 100644
index 0000000..c7da623
--- /dev/null
+++ b/examples/todo/.vscode/settings.json
@@ -0,0 +1,23 @@
+{
+ "files.exclude": {
+ "**/build": true,
+ "**/.qt": true,
+ "**/.rcc": true,
+ "**/var/cache": true,
+ "**/var/log": true,
+ "**/vendor": true,
+ "**/packaging/linux/tools": true
+ },
+ "search.exclude": {
+ "**/build": true,
+ "**/vendor": true,
+ "**/.qt": true,
+ "**/.rcc": true,
+ "**/packaging/linux/tools": true
+ },
+ "[php]": { "editor.tabSize": 4 },
+ "[qml]": { "editor.tabSize": 4 },
+ "[cpp]": { "editor.tabSize": 4 },
+ "intelephense.environment.phpVersion": "8.4.0",
+ "qt-qml.qmlls.useQmlImportPathEnvVar": true
+}
diff --git a/examples/todo/.vscode/tasks.json b/examples/todo/.vscode/tasks.json
new file mode 100644
index 0000000..5109c57
--- /dev/null
+++ b/examples/todo/.vscode/tasks.json
@@ -0,0 +1,50 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "make build",
+ "type": "shell",
+ "command": "make",
+ "args": ["build"],
+ "group": { "kind": "build", "isDefault": true },
+ "problemMatcher": ["$gcc"]
+ },
+ {
+ "label": "make dev",
+ "type": "shell",
+ "command": "make",
+ "args": ["dev"],
+ "isBackground": true,
+ "presentation": { "reveal": "always", "panel": "dedicated" },
+ "problemMatcher": []
+ },
+ {
+ "label": "make doctor",
+ "type": "shell",
+ "command": "make",
+ "args": ["doctor"],
+ "problemMatcher": []
+ },
+ {
+ "label": "make quality",
+ "type": "shell",
+ "command": "make",
+ "args": ["quality"],
+ "problemMatcher": ["$gcc"]
+ },
+ {
+ "label": "make integration",
+ "type": "shell",
+ "command": "make",
+ "args": ["integration"],
+ "problemMatcher": []
+ },
+ {
+ "label": "make appimage",
+ "type": "shell",
+ "command": "make",
+ "args": ["appimage"],
+ "problemMatcher": ["$gcc"]
+ }
+ ]
+}
diff --git a/framework/skeleton/.idea/.gitignore b/framework/skeleton/.idea/.gitignore
new file mode 100644
index 0000000..57d484e
--- /dev/null
+++ b/framework/skeleton/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Track only the shared run configs; ignore per-user IDE state.
+*
+!.gitignore
+!runConfigurations/
+!runConfigurations/*.xml
diff --git a/framework/skeleton/.idea/runConfigurations/make_dev.xml b/framework/skeleton/.idea/runConfigurations/make_dev.xml
new file mode 100644
index 0000000..c732a41
--- /dev/null
+++ b/framework/skeleton/.idea/runConfigurations/make_dev.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/skeleton/.idea/runConfigurations/make_doctor.xml b/framework/skeleton/.idea/runConfigurations/make_doctor.xml
new file mode 100644
index 0000000..214305d
--- /dev/null
+++ b/framework/skeleton/.idea/runConfigurations/make_doctor.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/skeleton/.idea/runConfigurations/make_quality.xml b/framework/skeleton/.idea/runConfigurations/make_quality.xml
new file mode 100644
index 0000000..91dbe3b
--- /dev/null
+++ b/framework/skeleton/.idea/runConfigurations/make_quality.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/skeleton/.vscode/launch.json b/framework/skeleton/.vscode/launch.json
new file mode 100644
index 0000000..bbd8961
--- /dev/null
+++ b/framework/skeleton/.vscode/launch.json
@@ -0,0 +1,41 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Listen for Xdebug (Symfony / FrankenPHP)",
+ "type": "php",
+ "request": "launch",
+ "port": 9003,
+ "pathMappings": {
+ "${workspaceFolder}/symfony": "${workspaceFolder}/symfony"
+ },
+ "log": false
+ },
+ {
+ "name": "Run skeleton (Qt host)",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/build/qml/skeleton",
+ "args": [],
+ "stopAtEntry": false,
+ "cwd": "${workspaceFolder}",
+ "environment": [
+ { "name": "BRIDGE_URL", "value": "http://127.0.0.1:8765" },
+ { "name": "BRIDGE_TOKEN", "value": "devtoken" }
+ ],
+ "preLaunchTask": "make build",
+ "MIMode": "gdb",
+ "linux": { "MIMode": "gdb" },
+ "osx": { "MIMode": "lldb" }
+ }
+ ],
+ "compounds": [
+ {
+ "name": "Dev: Xdebug + Qt host",
+ "configurations": [
+ "Listen for Xdebug (Symfony / FrankenPHP)",
+ "Run skeleton (Qt host)"
+ ]
+ }
+ ]
+}
diff --git a/framework/skeleton/.vscode/settings.json b/framework/skeleton/.vscode/settings.json
new file mode 100644
index 0000000..fa8c7c4
--- /dev/null
+++ b/framework/skeleton/.vscode/settings.json
@@ -0,0 +1,22 @@
+{
+ "files.exclude": {
+ "**/build": true,
+ "**/.qt": true,
+ "**/.rcc": true,
+ "**/var/cache": true,
+ "**/var/log": true,
+ "**/vendor": true
+ },
+ "search.exclude": {
+ "**/build": true,
+ "**/vendor": true,
+ "**/.qt": true,
+ "**/.rcc": true
+ },
+ "[php]": { "editor.tabSize": 4 },
+ "[qml]": { "editor.tabSize": 4 },
+ "[cpp]": { "editor.tabSize": 4 },
+ "intelephense.environment.phpVersion": "8.4.0",
+ "intelephense.files.associations": ["*.php", "*.phtml"],
+ "qt-qml.qmlls.useQmlImportPathEnvVar": true
+}
diff --git a/framework/skeleton/.vscode/tasks.json b/framework/skeleton/.vscode/tasks.json
new file mode 100644
index 0000000..b6a63f4
--- /dev/null
+++ b/framework/skeleton/.vscode/tasks.json
@@ -0,0 +1,36 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "make build",
+ "type": "shell",
+ "command": "make",
+ "args": ["build"],
+ "group": { "kind": "build", "isDefault": true },
+ "problemMatcher": ["$gcc"]
+ },
+ {
+ "label": "make dev",
+ "type": "shell",
+ "command": "make",
+ "args": ["dev"],
+ "isBackground": true,
+ "presentation": { "reveal": "always", "panel": "dedicated" },
+ "problemMatcher": []
+ },
+ {
+ "label": "make doctor",
+ "type": "shell",
+ "command": "make",
+ "args": ["doctor"],
+ "problemMatcher": []
+ },
+ {
+ "label": "make quality",
+ "type": "shell",
+ "command": "make",
+ "args": ["quality"],
+ "problemMatcher": ["$gcc"]
+ }
+ ]
+}
diff --git a/framework/skeleton/README.md b/framework/skeleton/README.md
index e2525bf..5e063c7 100644
--- a/framework/skeleton/README.md
+++ b/framework/skeleton/README.md
@@ -6,7 +6,7 @@ The framework's reference application: a minimal Symfony backend plus a Qt/QML h
- Linux (other platforms land in Phase 4)
- Qt 6.5+ dev packages (`qt6-base-devel`, `qt6-declarative-devel`, `qt6-quickcontrols2-devel`, `qt6-tools-devel`), CMake, gcc-c++
-- PHP 8.3+
+- PHP 8.4+ (Symfony 8 enforces this)
- [FrankenPHP](https://frankenphp.dev/) on PATH (or set `FRANKENPHP=/path/to/frankenphp`)
- Composer
@@ -63,6 +63,50 @@ curl -X POST http://127.0.0.1:8765/api/todos \
The Mercure SSE stream receives a `correlationKey: my-key-1` envelope which the Qt host's `ReactiveListModel` matches against any in-flight optimistic mutation (PLAN.md §5).
+## Hot reload
+
+Both halves of the app reload without re-running `make dev`.
+
+### PHP-side (Symfony / FrankenPHP)
+
+`make dev` runs `frankenphp run --watch` (see `Caddyfile` and `scripts/dev.sh`). FrankenPHP rebuilds the worker on any change under `symfony/` — controllers, services, entities, templates, configuration. Just save the file; the next request through the Qt host hits the new code. There is no opcache to clear and no service to restart.
+
+If you change something Doctrine-mapped, run a fresh migration in another terminal:
+
+```bash
+cd symfony
+bin/console make:migration
+bin/console doctrine:migrations:migrate -n
+```
+
+The Qt host stays up across all of this.
+
+### QML-side
+
+The Qt host loads QML from a compiled-in resource bundle, so saving a `.qml` file does **not** flip the running window automatically. Three workflows that do:
+
+- **Qt Creator → File → Reload** (or `Ctrl+R` with focus on a QML file). Rebuilds the QML cache and reloads the window in place.
+- **`qmlls` live preview** — the QML language server bundled with Qt 6.5+ runs a live preview connected to your editor (VSCode + the Qt extension, neovim, helix). Edits show up instantly in the preview window without rebuilding.
+- **Run from source** — start the host with `QT_QUICK_CONTROLS_CONF=` and `QML_IMPORT_TRACE=1` set, and pass `-DQT_QML_DEBUG` so the running engine accepts a hot-reload connection from Qt Creator. PLAN.md §6 captures the long-term plan to gate this behind `BRIDGE_DEV=1`.
+
+For most edits, Qt Creator's *Reload* is the lowest-friction option. The `.qmlls.ini` file (auto-generated when `qmlls` first runs) configures completion + live preview against this project's QML import paths.
+
+### Editor configs
+
+Both `.vscode/` and `.idea/runConfigurations/` ship with the skeleton.
+
+VSCode (`.vscode/launch.json`):
+
+- **Listen for Xdebug** — attaches the debugger on port 9003 once you set `XDEBUG_MODE=debug` for the FrankenPHP child (e.g. `XDEBUG_MODE=debug make dev`).
+- **Run skeleton (Qt host)** — gdb-launches the built binary with `BRIDGE_URL=http://127.0.0.1:8765` so it talks to the dev mode FrankenPHP started elsewhere by `make dev`.
+- **Compound: Dev: Xdebug + Qt host** — runs both at once.
+
+PhpStorm (`.idea/runConfigurations/`): `make dev`, `make doctor`, `make quality` shell run configs. PHP debugging is via the toolbar's **Start Listening for PHP Debug Connections** toggle (PhpStorm's Xdebug listener is global, not per-project).
+
+### Dev console
+
+`Ctrl+`` toggles an in-window console showing the bundled FrankenPHP child's stdout + stderr (PLAN.md §8). It's a passive ring buffer (~500 lines) — opening it has no IPC cost. Use it when you don't have a separate terminal to read the dev log.
+
## Quality checks
```bash