open-design/docs/architecture.md
INFINITY 988fd6db5e
feat: import existing local folder as project (#597) (#624)
* feat(contracts): types for folder-import endpoint

Add ImportFolderRequest, ImportFolderResponse to the public contract
surface. Extend ProjectMetadata with a baseDir field — when set, the
project's files live at this absolute path instead of .od/projects/<id>/.
Stored as the realpath() result so symlinks cannot redirect later writes.

Refs nexu-io/open-design#597

* feat(daemon): support metadata.baseDir for folder-rooted projects

Add resolveProjectDir() and metadata-aware variants of listFiles,
readProjectFile, writeProjectFile, ensureProject so a project's files
can live under metadata.baseDir (the user's chosen folder) instead of
.od/projects/<id>/. metadata.baseDir is opt-in — projects without it
keep the existing .od/projects/<id>/ behavior unchanged.

When listFiles walks a baseDir-rooted project, it skips conventional
build / install dirs (node_modules, .git, dist, build, .next, .nuxt,
.turbo, .cache, .output, out, coverage, __pycache__, .venv, vendor,
target, .od, .tmp) so the file panel stays focused on design content
instead of being dominated by lockfiles and node_modules.

Add detectEntryFile() — best-effort lookup for index.html or any
.html at the folder root, used by the import endpoint to seed the
initial active tab.

Refs nexu-io/open-design#597

* feat(daemon): add POST /api/import/folder endpoint

Creates a project rooted at the submitted local folder. metadata.baseDir
points at that folder and OD reads / writes there directly — no copy,
no shadow tree, mirroring how Cursor / Claude Code / Aider behave. The
user owns the workspace and is responsible for their own version
control.

Safety:
- baseDir is canonicalized via fs.promises.realpath() at import time so
  user-controlled symlinks can't redirect later writes. resolveSafe
  enforces the bounds check against the literal stored path; without
  realpath, a symlink (e.g. ~/sneaky → /etc) would let writeProjectFile
  escape the project tree at every later call because the OS follows
  the symlink at open() time.
- Post-realpath lstat ensures the canonical target is itself a real
  directory (defense-in-depth).
- The data directory (RUNTIME_DATA_DIR) and its descendants are
  refused after symlink resolution so a redirect into the daemon's
  own state can't masquerade as a project import.

The web client wires this through state/projects.ts → App.tsx,
landing the user on the auto-detected entry file when present.

Refs nexu-io/open-design#597

* feat(desktop): expose native folder picker to renderer

Adds an Electron preload script that exposes window.electronAPI.pickFolder
via contextBridge. Wires dialog.showOpenDialog through ipcMain so the
web UI can open a native folder selector for project import. Browser-only
users fall back to a text input for the absolute path (handled in the
web layer); the picker stays an optional convenience on the desktop
binary.

ipcMain.handle() registers handlers in an internal map that is not
exposed via eventNames(), so the natural-looking guard
  if (!ipcMain.eventNames().includes('dialog:pick-folder')) ipcMain.handle(...)
is always true. On a second createDesktopRuntime() call (dev hot-reload,
packaged-vs-electron mode swap) the body re-runs and ipcMain.handle()
throws 'Attempted to register a second handler'. Use removeHandler()
+ handle() unconditionally — removeHandler() is a documented no-op
when nothing is registered, making the pair idempotent.

Includes *.cts in the apps/desktop tsconfig so the preload script is
typechecked.

Refs nexu-io/open-design#597

* feat(web): add 'From existing folder' option to New Project

UI surface for the import flow:
- A new 'Open folder' affordance in NewProjectPanel that uses the
  native picker on Electron (window.electronAPI.pickFolder) and falls
  back to an absolute-path text input in the browser.
- importFolderProject() in state/projects.ts: typed wrapper around
  POST /api/import/folder using @open-design/contracts types.
- App.tsx wires the response: prepend the new project to the list,
  navigate to it, and select the auto-detected entry file as the
  active tab.

Skill / design-system pickers from the existing prototype tab are
reused — folder import is a project-creation flow, not a separate
project type.

Refs nexu-io/open-design#597

* docs(architecture): document folder-import endpoint

Adds POST /api/import/folder to the daemon API table and a 'Folder
import' section explaining the single-mode design (direct read/write
in metadata.baseDir, mirroring Cursor / Claude Code / Aider), the
realpath() canonicalization, the RUNTIME_DATA_DIR refusal, and the
SKIP_DIRS list applied to listFiles for baseDir-rooted projects.

Refs nexu-io/open-design#597

* test(daemon): unit + integration tests for folder import

Two new files:

apps/daemon/tests/folder-import-projects.test.ts (13 unit tests):
- resolveProjectDir behavior under all metadata combinations,
  including the fallback when baseDir is relative and the
  isSafeId-bypass when baseDir is set
- detectEntryFile: index.html priority, .html fallback, null when
  no html, no descent into subdirs
- listFiles with metadata.baseDir: walk, SKIP_DIRS hides node_modules
  / .git / dist, back-compat for projects without baseDir

apps/daemon/tests/folder-import-route.test.ts (10 integration tests):
- Happy path: baseDir stored in metadata, importedFrom='folder',
  conversation created, entry file detected
- Error paths: missing baseDir, empty, relative, non-existent,
  pointing at a file
- Security: realpath canonicalization (the symlink test was the one
  that surfaced the original /var vs /private/var mismatch in
  RUNTIME_DATA_DIR comparison on macOS)
- Security: a symlink that resolves into RUNTIME_DATA_DIR is rejected
  after realpath, not before

Refs nexu-io/open-design#597

* fix(daemon): wire baseDir metadata into chat + deploy reads

Two bugs caught in Codex automated review of #624:

1. chat-route was passing the metadata object directly as the listFiles
   opts argument: `listFiles(PROJECTS_DIR, projectId, chatMeta)`. The
   listFiles contract reads opts.metadata, not opts itself, so this
   silently fell back to .od/projects/<id>/ instead of the imported
   folder. existingProjectFiles was empty for baseDir-rooted projects.
   Wrap as `{ metadata: chatMeta }`.

2. deploy.ts read project files via readProjectFile without the
   metadata third argument, so for baseDir-rooted projects the deploy
   and preflight endpoints would look in .od/projects/<id>/ and fail
   with file-not-found instead of reading the imported folder. Thread
   options.metadata through buildDeployFilePlan → readProjectFile and
   pass project?.metadata at the two server.ts callsites
   (`POST /api/projects/:id/deploy` and the preflight endpoint).

Add a regression test that locks the listFiles contract: passing a
bare metadata object as opts must NOT scan baseDir — it must fall back
to the standard project dir, otherwise callers can leak the wrong
folder by mistake.

Refs nexu-io/open-design#597, #624 (Codex review)

* fix(daemon): ensure correct metadata handling in folder import

Addressed issues with metadata handling in folder import functionality. Updated the listFiles and readProjectFile methods to correctly utilize the metadata.baseDir, ensuring that project files are read from the intended directory. Added regression tests to verify that passing a bare metadata object does not inadvertently scan the baseDir, maintaining the integrity of project file access.

Refs nexu-io/open-design#597

* fix(daemon): security hardening from Codex review of #624

P1 findings from automated review:

1. POST /api/projects + PATCH /api/projects/:id rejected
   client-supplied metadata.baseDir. baseDir is privileged: it lets a
   project root inside the user's filesystem, and the realpath() +
   RUNTIME_DATA_DIR reentry checks live only on /api/import/folder.
   Allowing it on the generic create/patch path lets an attacker
   smuggle e.g. /etc through and bypass every import-time guard.
   Both endpoints now refuse a baseDir field with 400.

2. resolveSafeReal() helper: realpath()s each candidate path (or its
   longest existing prefix for write paths) and re-validates against
   realpath(projectRoot). The original resolveSafe() only did a
   string-prefix check, which was fooled by symlinks *inside* a
   baseDir-rooted project. A repo containing 'assets -> /Users/me/.ssh'
   passed the literal prefix check but readFile() followed the link
   at open() time. resolveSafeReal() is now used by readProjectFile,
   writeProjectFile, and deleteProjectFile.

3. Multer chat-upload destination now resolves to metadata.baseDir for
   imported folder projects via a module-level lookup wired to db at
   startServer() boot. Previously attachments landed in
   .od/projects/<id>/ even for baseDir projects, so the agent (which
   runs with cwd=baseDir) couldn't open them.

P2 findings:

4. searchProjectFiles threads metadata through listFiles +
   resolveProjectDir so /api/projects/:id/search hits the right tree.
5. buildProjectArchive + buildBatchArchive now accept metadata so
   'Download .zip' works for imported folder projects.
6. Watcher subscribe() resolves to baseDir for imported projects so
   live-reload SSE actually fires when the user edits files in their
   own folder. Registry stays keyed by the canonical directory.
7. Template snapshotting reads source-project files with metadata
   so a template can be saved from a baseDir-rooted source.

Tests:

- Regression: POST /api/projects with metadata.baseDir → 400.
- Regression: descendant symlink (assets/leak.txt -> /etc/hosts) is
  refused on the raw read endpoint.

Refs nexu-io/open-design#597, #624 (Codex P1+P2 review)

* fix(daemon): close two regressions found in #624 review round 2

@mrcfps caught two more correctness gaps:

1. Archive root symlink escape — buildProjectArchive accepts an optional
   ?root=<subdir> param to scope the zip to a subdirectory. The path was
   resolved with the string-only resolveSafe(), so a directory symlink
   inside an imported folder (docs -> /Users/me/.ssh) passed the prefix
   check and collectArchiveEntries() then walked outside the project
   tree. Switch to the symlink-aware resolveSafeReal() — the same one
   that already protects raw read/write/delete paths. The walker itself
   already skips dirent symlinks via !isDirectory && !isFile, so
   canonicalizing the root is the only missing piece.

2. PATCH metadata wiped baseDir — updateProject() replaces metadata
   wholesale. The previous guard only blocked an explicit baseDir
   change, but a normal patch that *omits* baseDir (a UI editing
   linkedDirs only sends { metadata: { kind, linkedDirs } }) silently
   detached imported projects from their folder root. Subsequent
   reads/writes/watch/deploy fell back to .od/projects/<id>.

   Re-stamp the immutable folder-import fields (baseDir, importedFrom='folder')
   from the existing project record onto the incoming patch when the
   project is imported. A patch that supplies a *different* baseDir
   still gets rejected as before; a patch that supplies the *same*
   baseDir is accepted as a no-op. A patch on a non-imported project
   that tries to set baseDir is also still rejected (preserves the
   POST /api/projects guard from the previous round).

Tests:

- archive endpoint: ?root=<symlink-to-/etc> → 400.
- patch endpoint: PATCH that omits baseDir on an imported project keeps
  baseDir intact (project still resolves to the user's folder after).

Refs nexu-io/open-design#597, #624 (Codex P1 round 2)

* fix(web): add Indonesian deploy provider copy

---------

Co-authored-by: INFINITY <valentyn.sotov@trendarena.app>
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-05-07 20:43:31 +08:00

18 KiB

Architecture

Parent: spec.md · Siblings: skills-protocol.md · agent-adapters.md · modes.md

This doc describes the system topology, runtime modes, data flow, and file layout. Design rationale lives in spec.md; protocol details for skills and agent adapters live in their own docs.


1. Three deployment topologies

OD is a web app plus a local daemon. The split means the same UI can run in three shapes:

Topology A — Fully local (the default)

┌────────────────── user's machine ──────────────────┐
│                                                    │
│   browser ──► Next.js dev server (localhost:3000)  │
│                       │                            │
│                       │ http://localhost:7456      │
│                       ▼                            │
│            od daemon (Node, long-running)         │
│                       │                            │
│                       ▼                            │
│            spawns: claude / codex / cursor / …     │
└────────────────────────────────────────────────────┘

One pnpm tools-dev run web starts both the Next.js app and the daemon. pnpm tools-dev adds the desktop shell. Zero config. No accounts.

Topology B — Web on Vercel + daemon on user's machine

browser ──► od.yourdomain.com (Vercel)
              │
              │ ws(s):// user-provided URL (e.g. cloudflared tunnel)
              ▼
        od daemon on user's laptop
              │
              ▼
        spawns: claude / codex / …

The user runs od daemon --expose which prints a tunnel URL; they paste the URL into the deployed web app's "Connect daemon" screen. Daemon holds secrets; Vercel holds nothing sensitive.

Topology C — Web on Vercel + direct API (no daemon)

browser ──► od.yourdomain.com (Vercel serverless)
                       │
                       ▼
              Anthropic Messages API (BYOK stored in browser)

No local CLI, no daemon. Degraded experience — no Claude Code skills, no filesystem artifacts (stored in IndexedDB), no PPTX export. But it's the "just try it" path. Keys stored localStorage with explicit warning.

The three topologies share the same web bundle; the difference is which transports are enabled.

2. Component diagram (logical)

┌─────────────────────────────── Web App ─────────────────────────────┐
│                                                                     │
│  ┌──────────┐  ┌─────────────┐  ┌───────────┐  ┌────────────────┐  │
│  │ chat pane│  │ artifact    │  │ preview   │  │ comment /      │  │
│  │          │  │ tree        │  │ iframe    │  │ slider overlay │  │
│  └────┬─────┘  └──────┬──────┘  └─────┬─────┘  └────────┬───────┘  │
│       │               │               │                  │           │
│       └─────────── session bus (in-memory) ──────────────┘           │
│                        │                                             │
│                        ▼                                             │
│              Transport layer (daemon SSE | api-direct | browser)      │
└─────────────────────────┬───────────────────────────────────────────┘
                          │
  ┌───────────────────────┴────────────────────────────────┐
  │                                                        │
  ▼ (topology A/B)                                         ▼ (topology C)
┌─────────────────────── Daemon ───────────────────────┐  ┌────────────┐
│                                                      │  │ browser-   │
│  session manager      skill registry                 │  │ only       │
│  agent adapter pool   design-system resolver         │  │ runtime    │
│  artifact store       preview compile pipeline       │  │ (limited)  │
│  export pipeline      detection service              │  └────────────┘
│                                                      │
└─┬────────────────────────────────────────────────┬───┘
  │                                                │
  ▼                                                ▼
┌─ agent CLIs ─┐                           ┌─ filesystem ─┐
│ claude       │                           │ ./.od/      │
│ codex        │                           │ ~/.od/      │
│ cursor-agent │                           │ skills/      │
│ gemini       │                           │ DESIGN.md    │
│ opencode     │                           └──────────────┘
│ qwen         │
└──────────────┘

3. Key components

3.1 Web app (Next.js 16, App Router)

  • Why Next.js, not Vite SPA? We want SSR for the marketing landing page + serverless routes for Topology C's direct-API path + Vercel deployment as a first-class citizen. An SPA would need a separate server for any of that.
  • State: React/browser state for UI config, with projects/conversations/files hydrated from the daemon APIs.
  • Iframe preview: Vendored React 18 + Babel standalone for JSX artifacts, following Open CoDesign's approach. HTML artifacts load raw. See §5.
  • Comment mode: Click captures [data-od-id] on preview DOM, opens a popover, sends {artifact_id, element_id, note} to daemon → agent gets a surgical edit instruction.
  • Slider UI: When an agent emits a "tweak parameter" tool call (see skills-protocol.md §4.2), the web app renders a live-update control that re-sends parameterized prompts without round-tripping the chat.

3.2 Local daemon (od daemon)

Single binary via pkg or a thin Node script distributed over npm. Responsibilities:

  • Listen on http://localhost:7456 by default. Accept REST/SSE routes under /api/*.
  • Maintain a session per web tab. Sessions hold: active agent, active skill, active artifact, in-flight tool calls, design-system reference.
  • Operate the agent adapter pool: one detected CLI = one adapter instance, reused across sessions.
  • Scan and index skills from ~/.claude/skills/, ./skills/, ./.claude/skills/ on startup and on FS-watch events.
  • Own the artifact store — writes files to disk, never in memory.
  • Run the preview compile pipeline (Babel transform for JSX, CSS inliner for HTML exports).
  • Provide export hooks for HTML/PDF/ZIP and skill-defined deck outputs.

3.3 Agent adapter pool

See agent-adapters.md for the full interface. Each adapter:

  1. Detects its target CLI (PATH lookup + config-dir probe).
  2. Spawns the CLI with a standardized wrapper prompt + skill context + design-system context + CWD set to the project's artifact root.
  3. Streams stdout/stderr as structured events (JSON Lines if the CLI supports it; line-based parser otherwise).
  4. Reports capabilities — does it support multi-turn? Surgical edits? Native skill loading? Tool use?

3.4 Skill registry

See skills-protocol.md. Scans three locations and merges:

Source Priority Purpose
./.claude/skills/ highest project-private skills
./skills/ medium project-declared skills
~/.claude/skills/ lowest user-global skills

Conflicts resolve by priority (higher wins). Each skill parsed once; watched for changes in dev.

3.5 Design-system resolver

  • Looks for ./DESIGN.md first, then ./design-system/DESIGN.md, then user-configured path.
  • Parses the 9-section format (see awesome-claude-design schema).
  • Injects as a prepended system message on every agent run, plus as a {{ design_system }} template variable skills can reference.
  • Hot-reloads on file change in dev.

3.6 Artifact store

Plain files on disk. Conventional layout per project:

./.od/
├── config.json                  # project-level daemon config
├── artifacts/
│   ├── 2026-04-24T10-03-12-landing/
│   │   ├── artifact.json        # metadata (skill, mode, prompt, parent)
│   │   ├── index.html           # primary output (or .jsx, .md, .pptx.json)
│   │   └── assets/              # skill-generated images, fonts, etc.
│   └── …
├── history.jsonl                # append-only action log (generations, edits, comments)
└── sessions/
    └── <session-id>.json        # transient; garbage-collected after 24h

Rationale:

  • Plain files → users can git add ./.od/artifacts/ and review designs in PRs.
  • artifact.json metadata → OD can reconstruct the artifact tree without a DB.
  • history.jsonl not SQLite → append-only, git-friendly, greppable. Open CoDesign uses SQLite; we deliberately don't.
  • Sessions separate from artifacts → sessions are ephemeral UI state; artifacts are durable.

3.7 Export pipeline

Format How
HTML (self-contained) Inline all CSS, rewrite asset URLs to data: URIs
PDF puppeteerpage.pdf() on the rendered HTML
PPTX deck-skill outputs a JSON intermediate (slides.json); pptxgenjs generates the .pptx
ZIP archiver over artifacts/<id>/
Markdown direct copy if artifact is .md, otherwise skill-defined render

4. Data flow — a typical "generate prototype" turn

1. User types prompt in web chat.
2. Web sends { method: "session.generate", params: {
        sessionId, prompt, modeHint: "prototype"
   }} to daemon via WS.

3. Daemon:
     a. picks active skill (prototype-skill)
     b. loads design-system (DESIGN.md)
     c. materializes a new artifact dir under ./.od/artifacts/<slug>/
     d. invokes agent adapter with:
          - system: skill's SKILL.md contents + DESIGN.md
          - user: original prompt
          - cwd: the new artifact dir
     e. streams agent events back to web as they arrive:
          - "tool_call" (edit file, write file, read file)
          - "text_delta"
          - "thinking" (if supported)

4. Web shows:
     - running tool-call feed in the side panel
     - artifact tree updates as files materialize
     - preview iframe loads the primary output file when agent signals "done"
     - slider/comment overlay activates once preview loads

5. On completion, daemon appends:
     { ts, sessionId, artifactId, action: "generate", skill, promptHash }
   to history.jsonl.

6. User comments on an element → web sends { method: "session.refine", params: {
        sessionId, artifactId, elementId, note }}

7. Daemon re-invokes agent with surgical-edit instruction + the note.
   Adapter translates based on capabilities:
     - Claude Code → native tool loop, edits that region only
     - Codex → regenerates the file with "only change element X" constraint
     - API fallback → same as Codex path

5. Preview renderer

Constraints:

  • Must isolate artifact code from the host app (no access to window, cookies, parent DOM).
  • Must hot-reload as the agent streams writes.
  • Must support both static HTML and JSX artifacts.

Design:

  • Always an <iframe sandbox="allow-scripts"> — no allow-same-origin.
  • Static HTML: srcdoc load of the inlined artifact.
  • JSX: inject a small bootstrap that imports vendored React 18 + Babel standalone, then dynamically evals the JSX as Babel-transformed code. (This is what Open CoDesign does, and it works; no reason to reinvent.)
  • Agent writes trigger a debounced rebuild + iframe srcdoc replace. Full reload each time — React state loss is acceptable at this scope.

6. Config files

File Purpose
~/.open-design/config.toml daemon-global: default agent preference, keys (optional, BYOK), telemetry opt-in (default off)
~/.open-design/agents.json cached agent detection results
./.od/config.json project-local: active design system, preferred skills, preferred mode
./skills/<skill>/SKILL.md skill manifest (standard Claude Code format)
./DESIGN.md active design system (awesome-claude-design format)

All config is plain text / TOML / JSON — no binary formats, no sqlite. Reviewable in PRs.

7. Protocol between web and daemon

The shipped daemon uses HTTP routes plus Server-Sent Events for streaming chat output. This keeps the browser on the same /api/* surface in dev and production while still allowing incremental agent output.

Representative API surface:

GET  /api/health
GET  /api/agents
GET  /api/skills
GET  /api/design-systems
GET  /api/projects
POST /api/projects
POST /api/import/folder                    # see Folder import
GET  /api/projects/:id/files
POST /api/projects/:id/upload
POST /api/chat              -> text/event-stream
POST /api/artifacts/save

Folder import

POST /api/import/folder creates a project rooted at an existing local folder instead of the default .od/projects/<id>/. The submitted baseDir is stored on metadata.baseDir and OD reads / writes directly inside it — there is no copy or shadow tree. The user owns the workspace and is responsible for their own version control (git, time machine, etc.), mirroring how Cursor / Claude Code / Aider behave.

Safety:

  • The submitted baseDir is canonicalized via realpath() before storage, so user-controlled symlinks cannot redirect later writes.
  • Standard resolveSafe / sanitizePath checks apply on every write — metadata.baseDir only changes the project root, not the bounds check.
  • Imports inside RUNTIME_DATA_DIR (the daemon's own data directory) are refused after symlink resolution.
  • The file panel hides the conventional build / install dirs (node_modules .git dist build .next .nuxt .turbo .cache .output out coverage __pycache__ .venv vendor target .od .tmp) so the listing stays focused on design content.

Request / response types: ImportFolderRequest, ImportFolderResponse in @open-design/contracts.

Full schema in schemas/protocol.md (TODO: write).

8. Deployment

Local

pnpm install
pnpm tools-dev run web       # starts daemon + web foreground loop

When a reverse proxy sits in front of the daemon, /api/* includes SSE streams and must stay unbuffered. The daemon sends Cache-Control: no-cache, no-transform and X-Accel-Buffering: no, and also emits SSE comment keepalives, but nginx can still break chunked streams if gzip is enabled. For nginx, set proxy_buffering off;, gzip off;, and long proxy_read_timeout / proxy_send_timeout values on the API location. Otherwise browsers can report net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK) on long generations.

Docker

# docker-compose.yml
services:
  daemon:
    image: openclaudedesign/daemon
    volumes: [ "~/.open-design:/root/.open-design", "./:/workspace" ]
    ports: ["7456:7456"]
  web:
    image: openclaudedesign/web
    ports: ["3000:3000"]
    environment: [ "OD_DAEMON_URL=http://daemon:7456" ]

Vercel + local daemon (Topology B)

vercel deploy                     # web only
od daemon --expose               # user runs locally; prints tunnel URL
# user pastes URL into /connect UI

Vercel direct (Topology C)

vercel deploy                     # same bundle
# flip VERCEL env flag OD_MODE=direct to hide daemon-connect UI

9. Security model

Surface Threat Mitigation
Daemon HTTP/SSE API Arbitrary local process talks to daemon Bind to localhost by default; add auth/tunnel hardening before exposing beyond the machine
Artifact code in preview XSS/cookie theft from host <iframe sandbox="allow-scripts">, no allow-same-origin
Agent running on user's machine Agent reads/writes outside project Adapter sets cwd to artifact dir; relies on agent's own permission system (Claude Code's --allowed-tools etc.)
User secrets Leak to cloud BYOK stored only in daemon's config.toml (mode 0600) or browser localStorage in Topology C, never sent to OD's own servers (we don't have any)
Skill from untrusted source Malicious skill in ~/.claude/skills/ Install-time warning; skills run under the agent's permission model, not ours
Vercel web bundle Compromised build Standard Vercel integrity; bundle has zero secrets

We inherit the agent's permission model on purpose — we don't invent our own sandbox, because Claude Code's --permission-mode / Codex's sandboxing / Cursor's containment already exist and are maintained.

10. Performance notes

  • Daemon startup: < 500 ms (lazy adapter init).
  • Agent detection: < 200 ms (parallel PATH probes).
  • First generation latency: dominated by agent model time; OD overhead should be < 50 ms.
  • Preview reload: debounced 100 ms on artifact file writes.
  • Skill index: cold scan < 100 ms for ~50 skills; watched with chokidar.

11. What's explicitly out of scope for MVP

  • Multi-user / RBAC / orgs
  • Hosted skill marketplace (git URLs only in v1)
  • Figma export (post-1.0, same as Open CoDesign)
  • Collaborative editing
  • Mobile web support (desktop only in MVP)
  • Offline mode (beyond "the agent is local" — we don't cache model responses)