070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
12 KiB
12 KiB
Plugin-driven flow plan
Parent: spec.md · Siblings: architecture-boundaries.md · maintainability-roadmap.md · run.md
Purpose
Make plugins 1:1 drive a complete run, give bare query input a deterministic fallback, and align Home entry with the NewProject taxonomy plus migration shortcuts (Figma / folder / repo). The current shape lets users pick a plugin in the rail but the actual run still falls back to a generic agent loop because (a) plugin-local SKILL.md is never injected, (b) Home direct-query never pins a plugin, and (c) the daemon stage runner is still a stub.
Goals / non-goals
- Goals
- Every plugin reaches the agent prompt with its own SKILL.md body and pipeline contract intact, so the same
Use pluginaction yields the same artifact across runs. - "Naked" Home query input gets auto-bound to a default scenario plugin keyed off the project
kindso there is no zero-plugin path. - Home input card surfaces an intent rail (Prototype / Slide deck / Image / Video / Audio / From Figma / From folder / Template / Other) modelled on the Lovart entry pattern.
- Every plugin reaches the agent prompt with its own SKILL.md body and pipeline contract intact, so the same
- Non-goals
- Marketplace / third-party plugin distribution changes.
- Transport changes between web and daemon.
- Replacing the agent CLI loop. We keep the LLM as the primary worker; we only formalise stage orchestration around it.
Current gaps (referenced to code)
| # | Where | Symptom |
|---|---|---|
| G1 | apps/daemon/src/server.ts#firePipelineForRun |
runStage stub returns synthetic critique.score: 4; pipeline events are emitted but no real per-atom work runs. |
| G2 | apps/daemon/src/plugins/apply.ts#pickFirstSkillId |
./SKILL.md is treated as a global skill id and never matches the registry, so a plugin's own SKILL.md is silently dropped from composeSystemPrompt. |
| G3 | apps/web/src/components/HomeView.tsx#submit |
When no plugin is active, pluginId is null; resolvePluginSnapshot then returns null and the run goes through the legacy non-plugin code path. |
| G4 | apps/web/src/components/EntryShell.tsx#DEFAULT_SCENARIO_PLUGIN_BY_KIND |
Every kind maps to od-new-generation, so image/video/audio and migration kinds never reach their bundled scenarios (od-figma-migration, od-code-migration). |
| G5 | apps/web/src/components/HomeHero.tsx |
No category / model chips below the input card; first-touch users have to know which plugin they want. |
Target architecture
plugin manifest (od.pipeline.stages[])
└─ daemon stage runner (real workers; replaces stub in G1)
└─ atom executor registry (per-atom handler)
└─ agent CLI process (LLM + tool calls, prompt carries plugin skill + active stage)
- The agent stays the primary "worker"; the new layer is the daemon-side enforcement of stage order, atom-prompt fragments, and exit signals.
- Plugin-local SKILL.md flows into the same
## Active skillslot as global skills, just sourced fromplugin.fsPath/<path>instead ofSKILLS_DIR. - The default-scenario binding moves to a single resolver shared by
/api/projects,/api/runs, andEntryShellso the client and daemon never disagree.
Kind → default scenario plugin
metadata.kind / taskKind |
Bundled scenario |
|---|---|
prototype, other, template |
od-new-generation |
deck |
od-new-generation (until a deck-specialised scenario lands) |
image, video, audio |
od-media-generation (Stage C) |
figma-migration taskKind |
od-figma-migration |
code-migration taskKind |
od-code-migration |
tune-collab taskKind |
od-tune-collab |
Stages
| Stage | Status | Notes |
|---|---|---|
| A | shipped | Plugin-local SKILL.md reaches ## Active skill; Home query auto-binds default scenario per kind. |
| B | shipped (MVP) | Chip rail mirrors NewProject taxonomy + adds Figma / folder / template shortcuts. Secondary chip rows (model picker for image, inline figmaUrl input) deferred. |
| C | shipped (MVP) | Bundled od-media-generation scenario for image/video/audio; uses existing media-image / media-video / media-audio atoms rather than a new wrapper atom. |
| D | shipped (MVP) | apps/daemon/src/plugins/atoms/registry.ts + built-ins.ts introduce an atom worker registry; firePipelineForRun now drives stages through runStageWithRegistry. Set OD_PIPELINE_RUNNER=stub to fall back to the v1 canned-signal runner. Real workers only ship for critique-theater today (reads run_devloop_iterations.critique_summary for the latest parseable score); every other FIRST_PARTY_ATOM registers a permissive worker so happy-path convergence matches v1. |
| E | shipped (MVP) | pnpm guard + pnpm typecheck green; daemon plugin suites (atom-registry, pipeline-runner, local-skill, apply, bundled-scenarios, headless-run, bundled, pipeline, simulate) all green; web 720/720 green; full daemon suite 1847/1852 with the remaining 5 failures all reproduced on HEAD without these changes (3× finalize-design macOS-tmpdir symlink issue; 2× chat-route/origin-validation order-dependent flakes that pass in isolation). Cross-app browser e2e covering the bare-query / chip-click flows is deferred to a follow-up since the existing Playwright harness only ships packaged-app smoke. |
Stage A — Plugin actually injects, Home never runs naked
Smallest change with the largest stability win.
| # | File | Change |
|---|---|---|
| A1 | apps/daemon/src/plugins/apply.ts |
Replace pickFirstSkillId with pickFirstSkillBinding returning `{ kind: 'global', id } |
| A2 | apps/daemon/src/plugins/snapshots.ts + contracts AppliedPluginSnapshot |
Add optional pluginSkillBody. Persist alongside other JSON fields. |
| A3 | apps/daemon/src/server.ts#composeDaemonSystemPrompt |
When snapshot carries pluginSkillBody, prefer it over the global skill body for the ## Active skill block. |
| A4 | apps/web/src/components/EntryShell.tsx |
Split DEFAULT_SCENARIO_PLUGIN_BY_KIND to map per kind; add migration kinds. |
| A5 | apps/daemon/src/server.ts (/api/projects, /api/runs) |
When the body carries no pluginId/appliedPluginSnapshotId and no project pin exists, look up the bundled scenario for taskKind and apply it. |
| A6 | apps/daemon/tests |
New tests cover local skill injection + default-by-kind binding. |
Exit criteria
- Picking a plugin → run logs show
## Active skill — <plugin>block with the plugin's SKILL.md contents. - Submitting a Home query with no plugin → snapshot row exists and
pluginIdmatches the kind mapping. - Existing plugin / non-plugin tests still pass.
Stage B — Home intent rail (Lovart-style)
HomeHeroadds ahome-hero__railrow.- Primary chips: Prototype · Slide deck · Image · Video · Audio · From template · From Figma · From folder · Other.
- Secondary row appears after primary selection where it makes sense (e.g. Image surfaces a model chip: GPT Image 2 / Nano Banana Pro / Seedance 2.0).
- Selecting a chip pre-applies the matching scenario plugin via
applyPlugin()so Enter behaves the same as the explicitUse pluginpath.
Stage C — Media + migration scenario fill-in
- Add bundled scenario
od-media-generation. The pipeline reuses the already-shippedmedia-image/media-video/media-audioatoms; no dedicatedmedia-generatewrapper is needed and the original plan's mention of a separate atom is superseded by this note. - The scenario shares
taskKind: 'new-generation'withod-new-generation. The daemon'scollectBundledScenariosdedupes bytaskKind, preferring the canonicalod-<taskKind>id so the pipeline-fallback stays deterministic. - Surface "From Figma" / "From folder" chips on the Home rail.
- "From Figma" applies the
od-figma-migrationplugin (which carries thefigmaUrlinput). A dedicated inlinefigmaUrlfield is deferred to a follow-up; the chip's prompt-template substitution still surfaces{{figmaUrl}}so the user can edit before submit. - "From folder" prefers the Electron native picker (when available) and falls back to opening the existing modal-based import form.
- "From Figma" applies the
Stage D — Real stage / atom workers (replaces the stub)
As shipped (MVP):
apps/daemon/src/plugins/atoms/registry.tsowns the atom worker registry:registerAtomWorker,runStageWithRegistry, and the frozenPERMISSIVE_DEFAULT_SIGNALStable. Real-worker outputs replace permissive defaults wholesale so a real score of 5 never gets clipped to 4; cross-worker conflicts inside a single stage still pessimistically merge (false-wins / lowest-number-wins).apps/daemon/src/plugins/atoms/built-ins.tsregisters a worker for everyFIRST_PARTY_ATOMSentry on first use. Onlycritique-theaterships a real watcher today — it readsrun_devloop_iterations.critique_summaryfor the latest parseablescore=Ntoken. Every other atom registers a permissive worker that returns no signals (so the defaults flow through) — that keeps backwards-compat with the v1 stub while documenting the worker surface for future migrations.apps/daemon/src/server.ts#firePipelineForRunnow branches onOD_PIPELINE_RUNNER: default (registry) drives stages throughrunStageWithRegistry;OD_PIPELINE_RUNNER=stubfalls back to the canned signal pump for diagnostic bisection.
Deferred to a follow-up:
- Real watchers for
file-write,live-artifact,direction-picker,discovery-question-form,todo-write, etc. These require either an agent-write protocol that touches DB rows the daemon can observe, or a side-channel (artifacts table / genui surface responses) wired into worker reads. The registry shape is ready; only the workers themselves remain. - Strengthen
## Active stageblock so the agent has to acknowledge each atom's outputs. (Today the contracts-siderenderActiveStageBlockalready exists; we still need to gate stage progression on the agent's structured acknowledgement.)
Stage E — Verification gate
pnpm guard+pnpm typecheckgreen.- Daemon + web package tests green.
- New e2e covering: bare query, plugin pick, Figma chip, folder chip — all four end with a
pipeline_stage_completedSSE.
File map
New (planned across stages)
plugins/_official/scenarios/od-media-generation/{open-design.json,SKILL.md}plugins/_official/atoms/media-generate/{open-design.json,SKILL.md}apps/daemon/src/plugins/atoms/media-generate.tsapps/daemon/src/plugins/atoms/registry.tsapps/web/src/components/home-hero/chips.tsapps/web/tests/components/home-hero-chips.test.tsxapps/daemon/tests/plugins-default-binding.test.tsapps/daemon/tests/plugins-local-skill.test.ts
Modified
apps/daemon/src/plugins/apply.tsapps/daemon/src/plugins/snapshots.tsapps/daemon/src/server.tsapps/daemon/src/plugins/pipeline.tsapps/daemon/src/prompts/system.tsapps/web/src/components/EntryShell.tsxapps/web/src/components/HomeHero.tsxapps/web/src/components/HomeView.tsxapps/web/src/styles/home/index.csspackages/contracts/src/plugins/apply.ts(AppliedPluginSnapshot adds optionalpluginSkillBody)
Risks
- R1 — Plugin SKILL.md may conflict with a project-pinned skill. Resolution order: plugin > project skill > kind default.
- R2 — Media surface already drives prompts through the
media generateCLI;od-media-generationmust not double-inject. - R3 — Stage D changes runtime behaviour. Keep
OD_BUNDLED_ATOM_PROMPTS=0and an explicitOD_PIPELINE_RUNNER=stubescape hatch until e2e stabilises.
Open questions
- Q1 — Persist the Home secondary chip selection (model picker) to user config or keep session-only? Session-only for v1.
- Q2 — Should "From folder" be a top-level chip or live behind a "More" submenu? Top-level for first release; revisit after telemetry.
Verification commands
pnpm guard
pnpm typecheck
pnpm --filter @open-design/daemon test
pnpm --filter @open-design/web test