- Shell decision changed from Electron + Code-OSS to Tauri (Rust) + Monaco Editor - Fast startup (~0.3-0.8s), low memory (100-200MB) via system webview - Rust backend for tool bus, knowledge graph parser, permission engine, runtime manager - Rationale documented vs Electron and Zed - Model changed from Claude to Grok (xAI), BYO API key - IMPLEMENTATION.md rewritten for Rust/Tauri stack: crates layout, Rust TDD tests, cargo-based CI - Phase 1 roadmap, test strategy, decisions section updated to match
38 KiB
Implementation Guide — Wursor v1
Version: 1.1
Source: PRD.md v1.3
Method: Test-driven development (TDD) — every module is written against its tests before its implementation.
Table of Contents
- Architecture Overview
- Project Structure
- Build Phases
- Phase 1 — Foundation (Weeks 1–8)
- Phase 2 — Intelligence (Weeks 9–16)
- TDD Rules
- CI/CD Pipeline
- Glossary
1. Architecture Overview
┌──────────────────────────────────────────────────────────┐
│ Tauri Shell (Rust native process) │
│ ┌──────────────────────┐ ┌───────────────────────────┐ │
│ │ System Webview (TS) │ │ Rust Backend │ │
│ │ ┌────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ Monaco Editor │ │ │ │ Tool bus (spawn, │ │ │
│ │ │ (editor core) │ │ │ │ exec, stream) │ │ │
│ │ ├────────────────┤ │ │ ├─────────────────────┤ │ │
│ │ │ Wursor panels │ │ │ │ Knowledge graph │ │ │
│ │ │ - preview │ │ │ │ parser (PHP/JSON │ │ │
│ │ │ - diff │ │ │ │ scan, 10k files │ │ │
│ │ │ - state diff │ │ │ │ in ~200ms) │ │ │
│ │ │ - chat │ │ │ ├─────────────────────┤ │ │
│ │ │ - status bar │ │ │ │ Permission engine │ │ │
│ │ └────────────────┘ │ │ │ + secret redaction │ │ │
│ │ Web frontend │ │ ├─────────────────────┤ │ │
│ │ (TypeScript/HTML) │ │ │ Runtime manager │ │ │
│ └──────────────────────┘ │ │ (Docker socket, │ │ │
│ │ │ wp-env lifecycle) │ │ │
│ │ ├─────────────────────┤ │ │
│ │ │ State Diff engine │ │ │
│ │ │ (create, evaluate, │ │ │
│ │ │ rollback, persist) │ │ │
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Site Runtime (wp-env / Docker) │
│ WordPress + MySQL + WP-CLI │
└──────────────────────────────────────────────────────────┘
Key structural decisions:
- Monorepo with a
crates/directory for Rust packages and awebview/directory for the TypeScript frontend - Rust crates communicate via
tauri::commandIPC to the webview - Each Rust crate has its own
tests/directory (integration) and inline#[cfg(test)]unit tests - Integration tests use fixture-based WordPress repos in CI
- E2E tests use Playwright against the Tauri webview
2. Project Structure
wursor/
├── src-tauri/ # Tauri Rust backend
│ ├── src/
│ │ ├── main.rs # Tauri app entry
│ │ ├── lib.rs # Plugin registration
│ │ ├── commands/ # #[tauri::command] IPC handlers
│ │ │ ├── fs.rs # File read/write
│ │ │ ├── project.rs # Project open/detect
│ │ │ ├── runtime.rs # wp-env start/stop/status
│ │ │ ├── tools.rs # Tool bus dispatch
│ │ │ ├── permissions.rs # Policy query
│ │ │ └── verify.rs # Screenshot/HTTP check
│ │ ├── tool-bus/
│ │ │ ├── mod.rs # Tool registry
│ │ │ ├── registry.rs # Name → {schema, handler, category}
│ │ │ ├── tools/
│ │ │ │ ├── fs.rs # fs.read, fs.write, fs.apply_patch
│ │ │ │ ├── wpcli.rs # wpcli.run (categorized)
│ │ │ │ ├── site.rs # site.browse, site.screenshot, site.request
│ │ │ │ ├── db.rs # db.query (read-only)
│ │ │ │ ├── lint.rs # lint.phpcs
│ │ │ │ ├── test_runner.rs# test.phpunit
│ │ │ │ └── index.rs # index.search, index.graph_lookup (stub)
│ │ │ └── executor.rs # Shell spawn, timeout, stream
│ │ ├── knowledge-index/
│ │ │ ├── mod.rs
│ │ │ ├── scanner.rs # Static PHP/JSON scan (Rust-native parser)
│ │ │ ├── enricher.rs # WP-CLI runtime enrichment
│ │ │ ├── graph.rs # Node, edge, store
│ │ │ ├── freshness.rs # Staleness tracking
│ │ │ └── queries.rs # Graph query API
│ │ ├── state-diff/
│ │ │ ├── mod.rs
│ │ │ ├── lifecycle.rs # create → review → stage → apply → verify → commit
│ │ │ ├── evaluator.rs # Blast radius, intent
│ │ │ ├── rollback.rs # Inverse generation
│ │ │ └── serializer.rs # .state-diff.json format
│ │ ├── runtime-manager/
│ │ │ ├── mod.rs
│ │ │ ├── interface.rs # RuntimeAdapter trait
│ │ │ ├── adapters/
│ │ │ │ └── wp_env.rs # wp-env adapter (v1 only)
│ │ │ ├── lifecycle.rs # State machine
│ │ │ └── logs.rs # Log tailing
│ │ ├── permission-engine/
│ │ │ ├── mod.rs
│ │ │ ├── tiers.rs # Permission tiers enum
│ │ │ ├── evaluator.rs # Tool call → policy check
│ │ │ ├── redactor.rs # Secret redaction (.env, wp-config)
│ │ │ └── config.rs # User policy (.wursor/policy.json)
│ │ ├── verify/
│ │ │ ├── mod.rs
│ │ │ ├── screenshot.rs # Screenshot capture
│ │ │ ├── http_check.rs # URL load + status + error sniff
│ │ │ ├── editor_check.rs # Block editor route check
│ │ │ └── reporter.rs # Result formatting
│ │ ├── agent-bridge/
│ │ │ ├── mod.rs
│ │ │ ├── client.rs # Grok API client (BYO key)
│ │ │ ├── tool_schemas.rs # Tool schemas → Grok format
│ │ │ ├── context.rs # System prompt + WP context
│ │ │ └── fallback.rs # Error handling + retry
│ │ └── playbooks/
│ │ ├── mod.rs
│ │ ├── registry.rs # Playbook registry
│ │ ├── dynamic_block.rs
│ │ ├── child_theme.rs
│ │ ├── cpt.rs
│ │ └── plugin.rs
│ ├── tests/ # Integration tests
│ │ ├── tool_bus_test.rs
│ │ ├── runtime_manager_test.rs
│ │ ├── knowledge_index_test.rs
│ │ ├── state_diff_test.rs
│ │ ├── permission_engine_test.rs
│ │ └── playbooks_test.rs
│ ├── fixtures/ # Test WP repos
│ │ ├── classic-theme/
│ │ ├── block-theme/
│ │ └── single-plugin/
│ ├── Cargo.toml
│ └── tauri.conf.json
│
├── webview/ # Tauri webview frontend (TypeScript)
│ ├── src/
│ │ ├── main.tsx # App entry
│ │ ├── App.tsx # Root component
│ │ ├── components/
│ │ │ ├── Editor.tsx # Monaco Editor wrapper
│ │ │ ├── Preview.tsx # Site preview iframe
│ │ │ ├── ChatPanel.tsx # Agent chat
│ │ │ ├── DiffPanel.tsx # Side-by-side file diff
│ │ │ ├── StateDiffPanel.tsx # State Diff lifecycle UI
│ │ │ ├── StatusBar.tsx # Site status, permissions
│ │ │ ├── Terminal.tsx # Integrated terminal
│ │ │ └── FirstRun.tsx # Onboarding wizard
│ │ ├── hooks/
│ │ │ ├── useToolBus.ts # Invoke Rust commands
│ │ │ ├── useRuntime.ts # Runtime state
│ │ │ └── useAgent.ts # Agent chat state
│ │ ├── utils/
│ │ │ ├── monaco-setup.ts # Monaco theme, WP stubs
│ │ │ └── tauri-api.ts # @tauri-apps/api wrappers
│ │ └── styles/
│ │ └── global.css
│ ├── __tests__/
│ │ ├── App.test.tsx
│ │ ├── ChatPanel.test.tsx
│ │ ├── DiffPanel.test.tsx
│ │ └── FirstRun.test.tsx
│ ├── index.html
│ ├── vite.config.ts
│ ├── tsconfig.json
│ └── package.json
│
├── e2e/ # End-to-end tests
│ ├── tauri/
│ │ ├── open-project.test.ts
│ │ ├── detect-wp.test.ts
│ │ ├── boot-preview.test.ts
│ │ ├── playbook-dynamic-block.test.ts
│ │ └── verify-preview.test.ts
│ ├── fixtures/
│ │ ├── sample-theme/
│ │ └── sample-plugin/
│ └── playwright.config.ts
│
├── Cargo.toml # Workspace root
├── package.json # Scripts, dev tooling
├── pnpm-workspace.yaml
├── rust-toolchain.toml
└── .github/workflows/
├── ci.yml # Rust tests + webview tests on PR
└── e2e.yml # E2E on release branch
3. Build Phases
| Phase | Weeks | Output | Exit criteria |
|---|---|---|---|
| Phase 1 | 1–8 | Tauri shell, Monaco Editor, Rust tool bus, wp-env, chat, playbooks, first-run | Clean machine → live preview ≤10 min; P0 playbook completes |
| Phase 2 | 9–16 | Knowledge graph, State Diffs, quality gates, staging pull, closed alpha | All §11 baselines collected; State Diff lifecycle demoed |
| Phase 3 | 17–28 | Block/FSE workshop, deploy connectors, team playbooks, paid beta | — |
| Phase 4 | 29+ | WooCommerce, multisite, maintenance agents, ecosystem | — |
4. Phase 1 — Foundation (Weeks 1–8)
8 sprints, one per week. Every sprint produces a passing integration test.
Sprint 1: Tauri shell + Monaco Editor + project scaffold
Goal: A working Tauri window with Monaco Editor that opens a folder and shows a Wursor sidebar.
TDD sequence
Step 1 — Write the test that defines "done"
// e2e/tauri/open-project.test.ts
import { test, expect } from '@playwright/test';
import { _electron as tauri } from 'tauri-playwright';
test('opens a folder and shows Wursor sidebar', async () => {
const app = await tauri.launch({
args: ['--project', '/path/to/fixtures/sample-theme'],
});
const window = await app.webview();
await expect(window.locator('.wursor-sidebar')).toBeVisible();
await expect(window.locator('.monaco-editor')).toBeVisible();
await app.close();
});
Step 2 — Write the Rust unit tests
// src-tauri/src/commands/project.rs (tests module)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_valid_project() {
let result = open_project("/path/to/fixtures/sample-theme");
assert!(result.is_ok());
assert_eq!(result.unwrap().project_type, "classic-theme");
}
#[test]
fn test_open_invalid_path() {
let result = open_project("/nonexistent");
assert!(result.is_err());
}
}
Step 3 — Write the webview unit tests
// webview/__tests__/App.test.tsx
describe('App', () => {
it('renders Monaco Editor after project open', () => { /* ... */ });
it('shows Wursor sidebar', () => { /* ... */ });
it('displays project name in the title bar', () => { /* ... */ });
});
Step 4 — Implement
src-tauri/src/main.rs— Tauri builder, register commandssrc-tauri/src/commands/project.rs—open_projectcommandwebview/src/App.tsx— Root layout: editor + sidebarwebview/src/components/Editor.tsx— Monaco Editor wrapper (@monaco-editor/react)webview/src/components/ChatPanel.tsx— Sidebar (placeholder)webview/src/utils/monaco-setup.ts— Dark theme, PHP stubs,block.jsonschemassrc-tauri/tauri.conf.json— Window config, permissionssrc-tauri/Cargo.toml— Dependencies: tauri, serde, tokio, etc.
Deliverables
- Tauri app that opens a folder and shows Monaco Editor + sidebar
e2e/tauri/open-project.test.tspassing- Rust unit tests for
open_projectcommand - Webview unit tests for
Appcomponent
Sprint 2: Rust tool bus
Goal: All tools from §8.3 are registered Rust commands with schemas, handlers, and shell execution.
TDD sequence
Step 1 — Write the Rust unit tests
// src-tauri/src/tool-bus/registry.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_tool() {
let mut registry = ToolRegistry::new();
registry.register("fs.read", fs_schema(), fs_handler());
assert!(registry.get("fs.read").is_some());
}
#[test]
fn test_duplicate_tool_name_errors() {
let mut registry = ToolRegistry::new();
registry.register("fs.read", fs_schema(), fs_handler());
assert!(registry.register("fs.read", fs_schema(), fs_handler()).is_err());
}
}
// src-tauri/src/tool-bus/tools/fs.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_fs_read_workspace_file() {
let result = fs_read(FsReadArgs { path: "fixtures/sample.txt".into() }).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().content, "hello");
}
#[tokio::test]
async fn test_fs_read_rejects_path_traversal() {
let result = fs_read(FsReadArgs { path: "../../etc/passwd".into() }).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_read_missing_file() {
let result = fs_read(FsReadArgs { path: "nonexistent.txt".into() }).await;
assert!(result.is_err());
}
}
// src-tauri/src/tool-bus/tools/wpcli.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_wpcli_run_safe_command() {
let result = wpcli_run(WpCliArgs {
command: "wp option get blogname".into(),
category: "safe".into(),
}).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().exit_code, 0);
}
#[tokio::test]
async fn test_wpcli_rejects_destructive_without_confirm() {
let result = wpcli_run(WpCliArgs {
command: "wp db drop".into(),
category: "destructive".into(),
}).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("confirmation required"));
}
}
// src-tauri/src/tool-bus/executor.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_execute_shell_command() {
let output = execute("echo hello", None).await.unwrap();
assert_eq!(output.stdout.trim(), "hello");
}
#[tokio::test]
async fn test_execute_timeout() {
let result = execute("sleep 60", Some(Duration::from_millis(100))).await;
assert!(result.is_err());
}
}
Step 2 — Implement
src-tauri/src/tool-bus/registry.rs—HashMap<String, Tool>with schema + handlersrc-tauri/src/tool-bus/tools/fs.rs— Path-scoped file operationssrc-tauri/src/tool-bus/tools/wpcli.rs— WP-CLI spawn with categorized allowlistsrc-tauri/src/tool-bus/tools/site.rs— HTTP fetch (reqwest) + screenshot via headless webviewsrc-tauri/src/tool-bus/tools/db.rs— MySQL read-only query via mysql cratesrc-tauri/src/tool-bus/tools/lint.rs— PHPCS spawnsrc-tauri/src/tool-bus/tools/test_runner.rs— PHPUnit spawnsrc-tauri/src/tool-bus/tools/index.rs— Stub (throws "not implemented" until Phase 2)src-tauri/src/tool-bus/executor.rs—tokio::process::Commandwrapper with timeout + streaming
Deliverables
- Rust tool registry with all 9 tools
- Each tool has unit tests for happy path, error path, and security boundary
cargo test --package tool-buspasses
Sprint 3: wp-env runtime manager
Goal: Start/stop/reset a WordPress site via wp-env from Rust, show status in the webview.
TDD sequence
Step 1 — Write the tests
// src-tauri/src/runtime-manager/lifecycle.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_start_wp_env() {
let manager = RuntimeManager::new(WpEnvAdapter::new());
let status = manager.start().await.unwrap();
assert_eq!(status, RuntimeStatus::Running);
}
#[tokio::test]
async fn test_stop_wp_env() {
let manager = RuntimeManager::new(WpEnvAdapter::new());
manager.start().await.unwrap();
let status = manager.stop().await.unwrap();
assert_eq!(status, RuntimeStatus::Stopped);
}
#[tokio::test]
async fn test_status_when_stopped() {
let manager = RuntimeManager::new(WpEnvAdapter::new());
assert_eq!(manager.status().await, RuntimeStatus::Stopped);
}
#[tokio::test]
async fn test_log_streaming() {
let manager = RuntimeManager::new(WpEnvAdapter::new());
let mut logs = manager.stream_logs().await.unwrap();
let entry = logs.next().await;
assert!(entry.is_some());
}
}
// src-tauri/src/runtime-manager/adapters/wp_env.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_spawn_wp_env_start() {
let adapter = WpEnvAdapter::new();
let result = adapter.start().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_parse_wp_env_output() {
let output = "WordPress development site.\n\nℹ http://localhost:8888\n\n✔ Okay!";
let parsed = WpEnvAdapter::parse_output(output);
assert_eq!(parsed.url, "http://localhost:8888");
}
#[tokio::test]
async fn test_handle_wp_env_not_found() {
let adapter = WpEnvAdapter::new();
// Simulate missing wp-env by clearing PATH
let result = adapter.start().await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("wp-env not found"));
}
}
// webview/__tests__/Preview.test.tsx
describe('Preview', () => {
it('shows a loading state while runtime starts', () => { /* ... */ });
it('renders the site iframe when running', () => { /* ... */ });
it('shows error state when runtime fails', () => { /* ... */ });
});
Step 2 — Implement
src-tauri/src/runtime-manager/interface.rs—RuntimeAdaptertraitsrc-tauri/src/runtime-manager/adapters/wp_env.rs— Spawnsnpx wp-env start, parses URL/credssrc-tauri/src/runtime-manager/lifecycle.rs— State machine: Stopped → Starting → Running → Stopping → Stoppedsrc-tauri/src/runtime-manager/logs.rs— Tailwp-env logsvia tokio processsrc-tauri/src/commands/runtime.rs—start_runtime,stop_runtime,runtime_statusIPC commandswebview/src/components/Preview.tsx— iframe pointing to site URLwebview/src/components/StatusBar.tsx— Runtime status indicatorwebview/src/hooks/useRuntime.ts— React hook subscribing to runtime state
Deliverables
- Rust runtime manager with wp-env adapter
- Preview panel showing the live site
- Status bar showing runtime state
cargo test --package runtime-managerpassing
Sprint 4: Agent chat + diff review
Goal: Chat panel that sends tasks to Grok, receives tool calls, dispatches through the Rust tool bus, and shows diffs.
TDD sequence
Step 1 — Write the tests
// src-tauri/src/agent-bridge/client.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_send_message_to_grok() {
let client = GrokClient::new("test-key");
let response = client.send("Add a paragraph to index.php").await.unwrap();
assert_eq!(response.message_type, "tool_call");
}
#[tokio::test]
async fn test_handle_api_error() {
let client = GrokClient::new("invalid-key");
let result = client.send("hello").await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("API error"));
}
}
// src-tauri/src/agent-bridge/tool_schemas.rs
#[cfg(test)]
mod tests {
#[test]
fn test_converts_tool_registry_to_grok_format() {
let schemas = to_grok_format(®istry);
assert_eq!(schemas[0].name, "fs.read");
assert!(schemas[0].input_schema.is_object());
}
}
// src-tauri/src/agent-bridge/context.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_builds_system_prompt_with_wp_semantics() {
let ctx = ContextBuilder::new()
.with_project_rules("fixtures/WORDPRESS.md")
.build();
assert!(ctx.contains("WordPress"));
assert!(ctx.contains("template hierarchy"));
}
}
// webview/__tests__/ChatPanel.test.tsx
describe('ChatPanel', () => {
it('sends a message and displays the response', () => { /* ... */ });
it('shows tool calls as expandable cards', () => { /* ... */ });
it('shows diffs in a side-by-side view', () => { /* ... */ });
it('shows error state when Grok is unreachable', () => { /* ... */ });
});
Step 2 — Implement
src-tauri/src/agent-bridge/client.rs— Grok API client (messages API, tool use, streaming)src-tauri/src/agent-bridge/tool_schemas.rs— Convert tool registry → Groktoolsarraysrc-tauri/src/agent-bridge/context.rs— Build system prompt with WP semantics, project rules, graph statesrc-tauri/src/agent-bridge/fallback.rs— Error handling, retry with exponential backoffwebview/src/components/ChatPanel.tsx— Chat UI (message list, input, tool call cards, streaming)webview/src/components/DiffPanel.tsx— Side-by-side diff view (Monaco diff editor)webview/src/hooks/useAgent.ts— React hook for agent state
Deliverables
- Working chat panel that sends to Grok and executes tool calls
- Diff view showing file changes
cargo test --package agent-bridgepassing- Chat panel unit tests passing
Sprint 5: Permission engine + WP-CLI
Goal: WP-CLI commands categorized and gated by permission tiers. Secrets redacted from agent context.
TDD sequence
Step 1 — Write the tests
// src-tauri/src/permission-engine/tiers.rs
#[cfg(test)]
mod tests {
#[test]
fn test_tiers_ordered() {
assert!(Tier::ReadFs < Tier::EditFs);
assert!(Tier::EditFs < Tier::WpCliSafe);
assert!(Tier::WpCliSafe < Tier::WpCliDestructive);
}
#[test]
fn test_all_tiers_defined() {
assert_eq!(Tier::variants().len(), 7);
}
}
// src-tauri/src/permission-engine/evaluator.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_allows_call_within_tier() {
let policy = Policy::default();
assert!(policy.evaluate("fs.read", Tier::ReadFs).await.is_ok());
}
#[tokio::test]
async fn test_blocks_call_above_tier() {
let policy = Policy::default().with_tier(Tier::ReadFs);
assert!(policy.evaluate("wpcli.run", Tier::WpCliSafe).await.is_err());
}
#[tokio::test]
async fn test_requires_confirm_for_destructive() {
let policy = Policy::default().with_tier(Tier::WpCliDestructive);
let result = policy.evaluate("wpcli.run", Tier::WpCliDestructive).await.unwrap();
assert!(result.confirmation_required);
}
#[tokio::test]
async fn test_blocks_production_writes() {
let policy = Policy::default().with_environment(Environment::Production);
assert!(policy.evaluate("fs.write", Tier::EditFs).await.is_err());
}
}
// src-tauri/src/permission-engine/redactor.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_redacts_env_values() {
let text = "DB_PASSWORD=secret123";
let secrets = vec!["secret123".to_string()];
assert!(!redact(text, &secrets).contains("secret123"));
}
#[tokio::test]
async fn test_redacts_wp_config_constants() {
let text = "define('DB_PASSWORD', 'secret123');";
let result = redact(text, &[]).await;
assert!(result.contains("[REDACTED]"));
}
#[tokio::test]
async fn test_preserves_variable_names() {
let text = "DB_PASSWORD=secret123";
let result = redact(text, &["secret123".to_string()]);
assert!(result.contains("DB_PASSWORD"));
}
}
Step 2 — Implement
src-tauri/src/permission-engine/tiers.rs— Ordered enum, 7 tierssrc-tauri/src/permission-engine/evaluator.rs— Policy evaluator: current tier, environment, confirmation flagsrc-tauri/src/permission-engine/redactor.rs— Scan text for secrets, redact before sending to agentsrc-tauri/src/permission-engine/config.rs— User-defined policy from.wursor/policy.json- Wire into
src-tauri/src/tool-bus/tools/wpcli.rs— check permission before executing
Deliverables
- Rust permission engine with all 7 tiers
- Secret redaction for
.envandwp-config.php - WP-CLI tool categorized and gated
cargo test --package permission-enginepassing
Sprint 6: P0 playbooks + first-run
Goal: Four P0 playbooks executable from the chat panel. First-run experience guides project open.
TDD sequence
Step 1 — Write the tests
// src-tauri/src/playbooks/dynamic_block.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_detects_build_setup() {
let playbook = DynamicBlockPlaybook::new();
let config = playbook.detect("fixtures/sample-theme").await.unwrap();
assert_eq!(config.build_tool, "@wordpress/scripts");
}
#[tokio::test]
async fn test_scaffolds_block_with_metadata() {
let playbook = DynamicBlockPlaybook::new();
let result = playbook.scaffold("testimonial").await.unwrap();
assert!(result.files.contains("block.json"));
assert!(result.files.contains("render.php"));
}
#[tokio::test]
async fn test_verifies_block_in_editor() {
let playbook = DynamicBlockPlaybook::new();
assert!(playbook.verify("testimonial").await.unwrap());
}
}
// src-tauri/src/playbooks/child_theme.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_creates_style_css_with_template_header() {
let playbook = ChildThemePlaybook::new();
let result = playbook.scaffold("twentytwentyfour", "my-child").await.unwrap();
assert!(result.style_css.contains("Template: twentytwentyfour"));
}
}
// src-tauri/src/playbooks/cpt.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_registers_cpt_with_rest_support() {
let playbook = CptPlaybook::new();
let result = playbook.register("book", "books").await.unwrap();
assert!(result.php.contains("show_in_rest"));
}
#[tokio::test]
async fn test_flushes_rewrite_rules() {
let playbook = CptPlaybook::new();
assert!(playbook.flush_rewrites().await.is_ok());
}
#[tokio::test]
async fn test_verifies_rest_endpoint() {
let playbook = CptPlaybook::new();
let result = playbook.verify_rest("book").await.unwrap();
assert!(result.status_ok);
}
}
// src-tauri/src/playbooks/plugin.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_creates_plugin_headers() {
let playbook = PluginPlaybook::new();
let result = playbook.scaffold("my-plugin", "My Plugin").await.unwrap();
assert!(result.main_php.contains("Plugin Name: My Plugin"));
}
}
// webview/__tests__/FirstRun.test.tsx
describe('FirstRun', () => {
it('shows three project paths on first launch', () => { /* ... */ });
it('detects Docker and wp-env, guides install if missing', () => { /* ... */ });
it('opens a project and shows the workspace within 10 minutes', () => { /* ... */ });
});
Step 2 — Implement
src-tauri/src/playbooks/registry.rs— Playbook registrationsrc-tauri/src/playbooks/dynamic_block.rs— detect → scaffold → register → build → verify → diffsrc-tauri/src/playbooks/child_theme.rs— scaffold → enqueue → override → screenshotsrc-tauri/src/playbooks/cpt.rs— register → flush → seed → REST check → diffsrc-tauri/src/playbooks/plugin.rs— headers → optional Composer/PHPUnitwebview/src/components/FirstRun.tsx— Onboarding wizard (3 paths: WP repo, folder, sample)src-tauri/src/commands/project.rs— Dependency check command
Deliverables
- 4 P0 playbooks with Rust unit tests
- First-run dialog (3 paths)
cargo test --package playbookspassing
Sprint 7: Integration + exit criteria
Goal: All Phase 1 pieces work together. Exit criteria tests pass end-to-end.
Integration tests
// e2e/tauri/phase1-exit-criteria.test.ts
import { test, expect } from '@playwright/test';
test('clean machine → live preview in ≤10 min', async () => {
const app = await tauri.launch({ args: [] });
const window = await app.webview();
const startTime = Date.now();
// Follow first-run dialog → install Docker → install wp-env → open project → boot
await window.locator('.wursor-first-run-open-repo').click();
await window.locator('.wursor-project-picker').fill('/path/to/fixtures/sample-theme');
await window.locator('.wursor-confirm-open').click();
// Wait for runtime to boot
await expect(window.locator('.wursor-status-indicator')).toHaveText('running', { timeout: 600000 });
const elapsed = Date.now() - startTime;
expect(elapsed).toBeLessThan(10 * 60 * 1000);
await app.close();
});
test('P0 playbook completes with verified preview + accepted diff', async () => {
const app = await tauri.launch({ args: ['/path/to/fixtures/sample-theme'] });
const window = await app.webview();
// Wait for runtime
await expect(window.locator('.wursor-status-indicator')).toHaveText('running', { timeout: 60000 });
// Run playbook
await window.locator('.wursor-chat-input').fill('Create a dynamic block named "testimonial"');
await window.locator('.wursor-chat-send').click();
// Wait for diff
await expect(window.locator('.wursor-diff-view')).toBeVisible({ timeout: 120000 });
// Wait for verify
await expect(window.locator('.wursor-verify-result')).toBeVisible({ timeout: 60000 });
// Accept
await window.locator('.wursor-accept-diff').click();
await expect(window.locator('.wursor-accepted-badge')).toBeVisible();
await app.close();
});
// src-tauri/tests/tool_bus_test.rs
#[cfg(test)]
mod integration_tests {
#[tokio::test]
async fn test_tool_bus_integration() {
let registry = build_registry();
let tool = registry.get("fs.read").unwrap();
let result = (tool.handler)(serde_json::json!({"path": "fixtures/sample.txt"})).await;
assert!(result.is_ok());
}
}
Deliverables
- Both exit criteria tests passing
cargo testpassing (all Rust unit + integration tests)pnpm test:webviewpassing (all webview tests)
Sprint 8: Polish + alpha readiness
Goal: Error states from §8.5 handled, app packaging works, build ready for internal alpha.
Tasks
- Error states — Wire each state from PRD §8.5 into the webview UI
- App packaging —
tauri buildproduces signed.dmg(macOS) and.msi(Windows) - Auto-update — Tauri updater with GitHub releases
- Telemetry — Minimal events (preview load time, playbook run, verify result) with consent dialog
- Documentation —
README.mdwith install instructions and quickstart - Bug bash — Internal team runs through first-run + playbook flow
Deliverables
- Signed app bundles for macOS + Windows
- Auto-update mechanism
- Error states all wired in the webview
- Minimal telemetry with consent
README.mdupdated for alpha users
5. Phase 2 — Intelligence (Weeks 9–16)
High-level outline — full breakdown follows Phase 1 exit.
| Sprint | Focus | Crates |
|---|---|---|
| 9 | Knowledge graph static scanner | src-tauri/src/knowledge-index/scanner.rs |
| 10 | Knowledge graph runtime enricher | src-tauri/src/knowledge-index/enricher.rs |
| 11 | Knowledge graph queries + UI | src-tauri/src/knowledge-index/queries.rs, tree view in webview |
| 12 | State Diff lifecycle | src-tauri/src/state-diff/lifecycle.rs |
| 13 | State Diff UI + rollback | src-tauri/src/state-diff/rollback.rs, StateDiffPanel |
| 14 | Quality gates (PHPCS, PHPUnit) | src-tauri/src/tool-bus/tools/lint.rs, test_runner.rs |
| 15 | Staging pull connector | src-tauri/src/tool-bus/tools/staging.rs |
| 16 | Closed alpha ship + baseline collection | Telemetry review, §11 baselines |
6. TDD Rules
These rules apply to every sprint:
- Write the test first. No implementation code is written without a failing test.
- One assertion per test. Each test verifies exactly one behavior.
- Tests are deterministic. No network calls in unit tests (mock Grok API, mock wp-env).
- Integration tests use fixtures. Sample WP repos live in
src-tauri/fixtures/ande2e/fixtures/. - Red → Green → Refactor. Write the failing test (red), make it pass (green), then clean up (refactor).
- Coverage floor. Rust:
cargo-tarpaulinenforces ≥ 90% line coverage. Webview: vitest enforces ≥ 90%. - No skipped tests in main.
#[ignore]andtest.onlyonly in feature branches.
Test naming convention
Rust: {module}_{behavior} (e.g. fs_read_rejects_path_traversal)
TypeScript: {module}.{behavior}.test.ts (e.g. fs.read-workspace-file.test.ts)
7. CI/CD Pipeline
# .github/workflows/ci.yml — runs on every PR
name: CI
on: [pull_request]
jobs:
rust:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo test --all-features
- run: cargo tarpaulin --out Xml --skip-clean
# Enforces 90% coverage floor
webview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: pnpm test -- --coverage # Enforces 90% coverage
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo clippy --all-targets -- -D warnings
- uses: pnpm/action-setup@v2
- run: pnpm install && pnpm lint
# .github/workflows/e2e.yml — runs on release branch
name: E2E
on:
push:
branches: [release/*]
jobs:
e2e:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: cargo build --release
- run: pnpm test:e2e
8. Glossary
| Term | Definition |
|---|---|
| Tool bus | Rust registry and executor for all agent-callable tools (fs, wpcli, site, db, etc.) |
| Runtime adapter | Rust trait that abstracts wp-env (v1) behind a common interface for future backends |
| Playbook | A reusable, multi-step agent workflow (scaffold block, create CPT, etc.) |
| State Diff | A reviewable mutation plan for WP content/state (CLI commands, SQL, or migration) |
| Permission tier | A capability level (read FS → edit FS → WP-CLI safe → destructive → etc.) |
| Verify | Required proof step: screenshot, HTTP check, or editor route confirmation |
| Knowledge graph | Rust-indexed map of themes, plugins, blocks, hooks, and REST routes |
End of Implementation Guide v1.1 — Wursor