Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ PHOTO_BUILDER_SIMULATE_IMAGE_PROMPT_GENERATION=0
PHOTO_BUILDER_SIMULATE_IMAGE_GENERATION=0
###< sitebuilder/llm-wire-log ###

CURSOR_AGENT_API_KEY=your-key-here

###> sitebuilder/docker-execution ###
# Host path for Docker-in-Docker volume mounts.
# IMPORTANT: This must be set to the absolute path of the project on the host.
Expand Down
19 changes: 19 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ parameters:
docker.host_base_path_default: "/var/www"
docker.host_base_path: "%env(default:docker.host_base_path_default:HOST_PROJECT_PATH)%"

# Docker image that contains the Cursor Agent CLI.
# Set via CURSOR_AGENT_IMAGE env var (docker-compose sets this to the app image).
cursor_agent.image_default: "node:22-slim"
cursor_agent.image: "%env(default:cursor_agent.image_default:CURSOR_AGENT_IMAGE)%"

services:
# default configuration for services in *this* file
_defaults:
Expand Down Expand Up @@ -133,6 +138,20 @@ services:
App\ChatBasedContentEditor\Facade\ChatBasedContentEditorFacadeInterface:
class: App\ChatBasedContentEditor\Facade\ChatBasedContentEditorFacade

App\LlmContentEditor\Infrastructure\LlmContentEditorAdapter: ~

App\CursorAgentContentEditor\Infrastructure\CursorAgentContentEditorAdapter:
arguments:
$cursorAgentImage: "%cursor_agent.image%"

App\AgenticContentEditor\Facade\AgenticContentEditorFacadeInterface:
class: App\AgenticContentEditor\Facade\AgenticContentEditorFacade
arguments:
- [
'@App\LlmContentEditor\Infrastructure\LlmContentEditorAdapter',
'@App\CursorAgentContentEditor\Infrastructure\CursorAgentContentEditorAdapter',
]

# Domain service bindings
App\WorkspaceMgmt\Domain\Service\WorkspaceStatusGuardInterface:
class: App\WorkspaceMgmt\Domain\Service\WorkspaceStatusGuard
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
services:
app:
image: etfs_${ETFS_PROJECT_NAME}_app
build:
context: .
dockerfile: docker/app/Dockerfile
container_name: etfs_${ETFS_PROJECT_NAME}_app
volumes:
- .:/var/www
- mise_data:/opt/mise
- /var/run/docker.sock:/var/run/docker.sock
environment:
# Redirect cache and config directories to /tmp to avoid polluting the mounted project directory
HOME: /tmp/container-home
Expand All @@ -19,13 +21,16 @@ services:
MISE_DATA_DIR: /opt/mise/data
MISE_CACHE_DIR: /opt/mise/cache
MISE_STATE_DIR: /opt/mise/state
# Use the app image (includes Cursor Agent CLI) for agent runs
CURSOR_AGENT_IMAGE: etfs_${ETFS_PROJECT_NAME}_app
networks:
- default
depends_on:
- mariadb
restart: unless-stopped

messenger:
image: etfs_${ETFS_PROJECT_NAME}_app
build:
context: .
dockerfile: docker/app/Dockerfile
Expand All @@ -50,6 +55,8 @@ services:
# The messenger runs Docker commands via the host socket, so paths must be host paths
# Uses PWD as default, which is set by docker-compose to the project directory
HOST_PROJECT_PATH: ${HOST_PROJECT_PATH:-${PWD}}
# Use the app image (includes Cursor Agent CLI) for agent runs
CURSOR_AGENT_IMAGE: etfs_${ETFS_PROJECT_NAME}_app
networks:
- default
depends_on:
Expand Down
14 changes: 14 additions & 0 deletions docker/app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ RUN install -m 0755 -d /etc/apt/keyrings && \
apt-get update -y && \
apt-get install -y docker-ce-cli

# Install Cursor CLI for agent execution
RUN curl -fsS https://cursor.com/install | bash

# Clean up to reduce image size
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

Expand Down Expand Up @@ -74,6 +77,17 @@ RUN mkdir -p /opt/mise/data /opt/mise/cache /opt/mise/state && \
chmod -R 777 /opt/mise && \
mise install node@24

# Create a custom entrypoint that adds the mise node bin directory to PATH.
# This makes node/npm/npx available in the default PATH for all invocations:
# docker-compose services, docker run commands, and non-interactive sh -c calls.
# The resolved path is baked at build time so there is no runtime lookup overhead.
RUN NODE_BIN="$(mise where node)/bin" && \
printf '#!/bin/sh\nset -e\nexport PATH="%s:$PATH"\nexec docker-php-entrypoint "$@"\n' "$NODE_BIN" \
> /usr/local/bin/app-entrypoint && \
chmod +x /usr/local/bin/app-entrypoint

ENTRYPOINT ["app-entrypoint"]

# Shell activation for interactive use (debugging)
RUN echo 'eval "$(/usr/bin/env mise activate bash)"' >> ~/.bashrc
RUN echo 'eval "$(/usr/bin/env mise activate zsh)"' >> ~/.zshrc
Expand Down
34 changes: 30 additions & 4 deletions docs/vertical-wiring.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Vertical Facade Wiring

This diagram shows **which verticals call which other verticals facade interface methods** — the wiring between verticals only. Internal calls within a vertical are omitted. See [archbook.md](archbook.md) for the overall facade/vertical architecture.
This diagram shows **which verticals call which other verticals' facade interface methods** — the wiring between verticals only. Internal calls within a vertical are omitted. See [archbook.md](archbook.md) for the overall facade/vertical architecture.

```mermaid
flowchart LR
subgraph callers["Callers"]
direction TB
CBCE["ChatBasedContentEditor"]
ACE["AgenticContentEditor"]
LLM["LlmContentEditor"]
CAC["CursorAgentContentEditor"]
PB["PhotoBuilder"]
WSM["WorkspaceMgmt"]
WST["WorkspaceTooling"]
Expand All @@ -22,6 +24,7 @@ flowchart LR
ACC[(Account)]
PRJF[(ProjectMgmt)]
WSMF[(WorkspaceMgmt)]
ACEF[(AgenticContentEditor)]
LLMF[(LlmContentEditor)]
WSTF[(WorkspaceTooling)]
RCAF[(RemoteContentAssets)]
Expand All @@ -33,11 +36,16 @@ flowchart LR
CBCE -->|account lookup| ACC
CBCE -->|getProjectInfo| PRJF
CBCE -->|workspace lifecycle, commit, PR| WSMF
CBCE -->|streamEdit, context dump| LLMF
CBCE -->|streamEdit, context dump, model info| ACEF

ACE -.->|dispatches to adapters in| LLM
ACE -.->|dispatches to adapters in| CAC

LLM -->|tools: build, preview, assets, rules| WSTF
LLM -->|getAgentConfigTemplate| PRJF

CAC -->|tools: build, rules| WSTF

PB -->|getAccountInfoByEmail| ACC
PB -->|getProjectInfo| PRJF
PB -->|readWorkspaceFile, getWorkspaceById| WSMF
Expand Down Expand Up @@ -71,8 +79,10 @@ Method details are in the summary table below.

| Caller vertical | Calls into (facade) | Main methods |
|---------------------------|----------------------------|--------------|
| **ChatBasedContentEditor** | Account, ProjectMgmt, WorkspaceMgmt, LlmContentEditor | Workspace lifecycle, commitAndPush, streamEditWithHistory, buildAgentContextDump, account resolution |
| **ChatBasedContentEditor** | Account, ProjectMgmt, WorkspaceMgmt, AgenticContentEditor | Workspace lifecycle, commitAndPush, streamEditWithHistory, buildAgentContextDump, getBackendModelInfo, account resolution |
| **AgenticContentEditor** | *(dispatches to adapters)* | Facade dispatches to `LlmContentEditorAdapter` and `CursorAgentContentEditorAdapter` via SPI |
| **LlmContentEditor** | WorkspaceTooling, ProjectMgmt | runQualityChecks, runTests, runBuild, suggestCommitMessage, getPreviewUrl, list/search remote assets, getWorkspaceRules; getAgentConfigTemplate (EditContentCommand) |
| **CursorAgentContentEditor** | WorkspaceTooling | runBuildInWorkspace, runShellCommandAsync, getWorkspaceRules |
| **PhotoBuilder** | Account, ProjectMgmt, WorkspaceMgmt, RemoteContentAssets | getAccountInfoByEmail; getProjectInfo (API key, S3 config); readWorkspaceFile (page HTML), getWorkspaceById; uploadAsset (S3) |
| **WorkspaceMgmt** | ProjectMgmt, ChatBasedContentEditor | getProjectInfo (setup, git, review); getLatestConversationId (reviewer UI) |
| **WorkspaceTooling** | RemoteContentAssets | fetchAndMergeAssetUrls, getRemoteAssetInfo |
Expand All @@ -81,11 +91,27 @@ Method details are in the summary table below.
| **RemoteContentAssets** (UI) | ProjectMgmt | getProjectInfo (for manifest URLs) |
| **Common** (voter) | Account, Organization | getAccountCoreIdByEmail; userCanReviewWorkspaces |

## Architecture: Agentic Content Editor

The **AgenticContentEditor** vertical implements a hexagonal port/adapter pattern:

- **Port** (`AgenticContentEditorFacadeInterface`): what consumers call (e.g. `ChatBasedContentEditor`).
- **SPI** (`AgenticContentEditorAdapterInterface`): what backend adapters implement.
- **Facade** (`AgenticContentEditorFacade`): dispatcher that resolves the correct adapter by backend type.

Adapters live in their respective backend verticals:
- `LlmContentEditor/Infrastructure/LlmContentEditorAdapter` — delegates to `LlmContentEditorFacade`
- `CursorAgentContentEditor/Infrastructure/CursorAgentContentEditorAdapter` — runs the Cursor CLI agent directly

Canonical DTOs and enums (`EditStreamChunkDto`, `AgentConfigDto`, `AgenticContentEditorBackend`, etc.) live in `AgenticContentEditor/Facade/` and are shared by all participants.

## Notes

- **ChatBasedContentEditor** is the main consumer of **WorkspaceMgmt** and **LlmContentEditor** (conversation flow, edit sessions, commit/push).
- **ChatBasedContentEditor** calls **AgenticContentEditor** for all edit operations. It never imports from LlmContentEditor or CursorAgentContentEditor directly.
- **LlmContentEditor** (ContentEditorAgent) uses **WorkspaceTooling** for all tool implementations (quality checks, build, preview, remote assets, rules).
- **CursorAgentContentEditor** uses **WorkspaceTooling** for build and shell execution.
- **WorkspaceTooling** delegates remote asset listing/info to **RemoteContentAssets**.
- **Organization** onboarding (AccountCoreCreatedSymfonyEventSubscriber) wires **Prefab → ProjectMgmt → WorkspaceMgmt** to create projects and dispatch setup.
- **ProjectMgmt** presentation layer coordinates **ChatBasedContentEditor**, **WorkspaceMgmt**, **LlmContentEditor**, and **RemoteContentAssets** for project/workspace/conversation and validation flows.
- **ProjectMgmt** still calls **LlmContentEditor** directly for `verifyApiKey()` — this is intentional as API key verification is LLM-specific and part of project configuration.
- **PhotoBuilder** reads workspace page HTML via **WorkspaceMgmt**, fetches API keys and S3 config via **ProjectMgmt**, uploads generated images via **RemoteContentAssets**, and validates user access via **Account**.
33 changes: 33 additions & 0 deletions migrations/Version20260127145023.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260127145023 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE conversations ADD content_editor_backend VARCHAR(32) DEFAULT \'llm\' NOT NULL, ADD cursor_agent_session_id VARCHAR(64) DEFAULT NULL');
$this->addSql('ALTER TABLE projects ADD content_editor_backend VARCHAR(32) DEFAULT \'llm\' NOT NULL');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE conversations DROP content_editor_backend, DROP cursor_agent_session_id');
$this->addSql('ALTER TABLE projects DROP content_editor_backend');
}
}
30 changes: 30 additions & 0 deletions migrations/Version20260211120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Rename cursor_agent_session_id to backend_session_state on conversations.
* Opaque session state is backend-agnostic (e.g. Cursor session ID for Cursor backend).
*/
final class Version20260211120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Rename conversations.cursor_agent_session_id to backend_session_state';
}

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE conversations RENAME COLUMN cursor_agent_session_id TO backend_session_state');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE conversations RENAME COLUMN backend_session_state TO cursor_agent_session_id');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace App\AgenticContentEditor\Facade;

use App\AgenticContentEditor\Facade\Dto\AgentConfigDto;
use App\AgenticContentEditor\Facade\Dto\BackendModelInfoDto;
use App\AgenticContentEditor\Facade\Dto\ConversationMessageDto;
use App\AgenticContentEditor\Facade\Dto\EditStreamChunkDto;
use App\AgenticContentEditor\Facade\Enum\AgenticContentEditorBackend;
use Generator;

/**
* SPI: what backends (LlmContentEditor, CursorAgentContentEditor) implement.
* Adapters yield Done chunks with optional backendSessionState for resumable backends.
*/
interface AgenticContentEditorAdapterInterface
{
public function supports(AgenticContentEditorBackend $backend): bool;

/**
* @param list<ConversationMessageDto> $previousMessages
*
* @return Generator<EditStreamChunkDto>
*/
public function streamEdit(
string $workspacePath,
string $instruction,
array $previousMessages,
string $apiKey,
AgentConfigDto $agentConfig,
?string $backendSessionState = null,
string $locale = 'en',
): Generator;

/**
* Build a human-readable dump of the full agent context for debugging.
* Each backend formats this according to how it actually sends context to its agent.
*
* @param list<ConversationMessageDto> $previousMessages
*/
public function buildAgentContextDump(
string $instruction,
array $previousMessages,
AgentConfigDto $agentConfig
): string;

/**
* Return model information for this backend (name, context limit, cost rates).
* Used for context usage bars and cost estimation in the UI.
*/
public function getBackendModelInfo(): BackendModelInfoDto;
}
Loading